Next.js에서 redirect()를 이용할 때 주의할 점, input[disabled]의 FormData 포함 여부
1. 개요
무엇을 해야 할지 여러모로 방황하고 있었는데, 우선은 진행하다 방치해두었던 당근마켓 클론코딩 프로젝트를 대략 마무리하기로 하였다. 더 자세히는 상품 CRUD와 마이 페이지까지 완성하려고 했다. 그래서 상품 부분을 확인해보던 중, 상품 수정과 삭제 기능이 제대로 구현되어 있지 않은 것을 확인했고 작업을 하였다. 그리고 작업 도중 발견한 사실을 기록하려고 한다.
2. redirect()
2-1. redirect: NEXT_REDIRECT 에러
상품 삭제 버튼을 클릭할 경우, 다음과 같은 동작을 구현하려고 했다.
- 상품을 삭제한다.
- /home으로 이동한다.
그리고 상품을 삭제하는 함수의 코드를 다음과 같이 작성해보았다.
const deleteProduct = async () => {
"use server";
try {
await db.product.delete({
where: {
id,
},
});
revalidatePath("/home");
redirect("/home");
} catch (error) {
console.error(error);
}
};
deleteProduct는 삭제 버튼을 클릭하면 실행할 함수이다. 이 함수는 다음 순서로 동작한다.
- db.product.delete를 통해 상품을 삭제한다.
- /home 경로의 캐싱된 데이터를 업데이트한다.
- /home으로 이동한다.
하지만 위 파일처럼 에러가 발생하였다. 그래서 redirect("/home")을 return redirect("/home")으로 변경하는 등 별별 방법으로 변경해보았는데도 계속 에러가 발생하였다. 그리고 혹시나 하는 마음으로, redirect를 finally로 이동시켜보았다.
const deleteProduct = async () => {
"use server";
try {
await db.product.delete({
where: {
id,
},
});
} catch (error) {
console.error(error);
} finally {
revalidatePath("/home");
redirect("/home");
}
};
그랬더니 에러가 발생하지 않고 무사히 redirect되었다.
2-2. 서버 컴포넌트에서의 redirect
왜 finally로 이동을 시켰을 뿐인데 에러가 발생하지 않았는지 너무나 궁금했다. 그래서 공식 문서를 확인해보았다.
Server Component
Invoking the redirect() function throws a NEXT_REDIRECT error and terminates rendering of the route segment in which it was thrown.
즉, 서버 컴포넌트에서 redirect는 NEXT_REDIRECT라는 error를 throw한다. redirect()는 단순한 window.location 변경이 아닌, Next.js의 내부 동작을 멈추기 위해 NEXT_REDIRECT error를 throw한다. 그렇기에 try-catch 내부에서 사용할 경우, 위처럼 예상치 못한 동작이 발생할 수 있다.
3. input의 disabled 속성
상품을 업데이트힐 때 상품의 id가 필요했다. 그래서 처음에는 다음과 같이 코드를 작성했다.
// app/products/[id]/edit/EditForm.tsx
... 생략
export default function EditForm({ product }: IProps) {
const { title, price, description, id: productId } = product;
const [editedProduct, setEditedProduct] = useState<{
title: string;
price: number;
description: string;
}>({
title: title,
price: price,
description: description,
});
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [preview, setPreview] = useState(product.photo);
const [state, action] = useFormState(updateProduct, null);
... 생략
return (
<div>
<form action={action} className="p-5 flex flex-col gap-5">
...생략
<input type="hidden" name="productId" value={productId} disabled />
// app/products/add/actions.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function updateProduct(prevState: any, formData: FormData) {
const data = {
photo: formData.get("photo"),
existingPhoto: formData.get("existingPhoto"),
title: formData.get("title"),
price: formData.get("price"),
description: formData.get("description"),
};
const productId = Number(formData.get("productId"));
console.log({ originalProductId: formData.get("productId"), productId });
... 생략
이렇게 코드를 작성한 후 console.log를 통해 formData와 productId를 확인해보니, formData에는 productId라는 속성이 없었고, productId는 0이라는 값이 출력되었다. 왜 그런지 확인해보니, input을 disabled 상태로 설정한 경우, disabled 속성이 추가된 input은 FormData에 포함되지 않기 때문이었다. 이는 보안 및 접근성을 위한 HTML 기본 동작으로 인해 disabled 상태의 input은 FormData에 포함되지 않기 때문이다. 그래서 disabled를 readonly로 수정하는 것으로 해결할 수 있었다.
4. 정리
- Next.js의 redirect()는 내부적으로 error를 throw한다.
- redirect()는 단순한 URL 이동이 아닌 Next.js의 내부 렌더링을 멈추는 역할을 한다.
- try-catch 내에서 실행할 경우, 예기치 않은 에러가 발생할 수 있으므로 최종적으로 실행되도록 배치해야 한다.
- input[disabled]는 FormData에 포함되지 않는다.
- disabled 속성이 추가된 <input>은 폼 제출 시 FormData에서 제외됨.
- disabled를 사용해야 한다면 hidden input을 추가하여 값을 전달하는 방식이 필요.
- readOnly를 사용하면 값을 유지하면서 폼 제출 시 포함 가능.
5. 참고한 곳
- Next.js 공식 문서 redirect
- HTML disabled 속성