한글 제목의 글만 저장 직후 죽고, 열면 404가 난 이유 — 주소 인코딩은 두 방향이다
문제 발생
글 주소에 한글 slug를 쓰기로 했습니다. 영문 제목은 멀쩡한데 한글 제목만 두 곳에서 깨졌습니다.
첫째, 저장 뒤 상세로 보내는 Server Action이 죽었습니다.
TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["x-action-redirect"]둘째, 그 글을 주소로 직접 열면 404였습니다. DB에는 분명히 그 slug가 있었습니다.
export async function createPost(formData: FormData) {
const slug = await savePost(formData);
redirect(`/posts/${slug}`);
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
}lint, typecheck, build 전부 통과한 상태였습니다.
원인 분석
쓰는 방향 — 목적지가 헤더로 갑니다. Next.js 문서에 따르면 Server Action 안의 redirect()는 JavaScript가 있을 때 클라이언트 측 내비게이션을 수행합니다. 그 목적지를 브라우저에 알려 주는 수단이 응답 헤더(x-action-redirect)인데, Node는 헤더 값에 허용되지 않는 문자가 있으면 ERR_INVALID_CHAR("Invalid characters were detected in headers")를 던집니다. 한글이 그대로 들어간 것이 원인입니다. 같은 문자열이 <Link href>에서는 문제없는 이유는 HTML 속성이 UTF-8이기 때문입니다.
읽는 방향 — params는 인코딩된 채로 옵니다. 동적 세그먼트의 params는 URL에 적힌 percent-encoded 문자열을 그대로 줍니다. /posts/%ED%95%9C%EA%B8%80을 열면 slug는 %ED%95%9C%EA%B8%80이고, DB의 한글과 비교하면 당연히 없습니다. 조용히 404가 됩니다.
해결 방안
- 경로를 만들고 읽는 함수를 한 곳에 둡니다. 호출부마다 템플릿 문자열로 만들면 한 군데씩 빠집니다.
export function postPath(slug: string): string {
return `/posts/${encodeURIComponent(slug)}`;
}
export function decodePostSlug(raw: string): string | null {
try {
return decodeURIComponent(raw);
} catch {
return null;
}
}redirect(postPath(slug))처럼 항상 이 함수를 거치고, 페이지는 decodePostSlug(slug)가 null이면 notFound()합니다.
-
decodeURIComponent는 감쌉니다. MDN 문서대로%뒤에 16진수 두 자리가 없거나 올바른 UTF-8이 아니면URIError를 던집니다(%E0%A4%A같은 입력). 잘못된 주소가 500이 되면 안 됩니다. -
실제 한글 제목으로 브라우저에서 확인합니다. 두 버그 모두 타입 검사와 빌드를 통과합니다. 인코딩 round-trip은 테스트로 남깁니다.
assert.equal(decodePostSlug(postPath("한글-제목").replace("/posts/", "")), "한글-제목");revalidatePath()는 무관합니다. 내부 API라 헤더를 거치지 않습니다. 인코딩이 필요한 것은 브라우저로 나가는 값입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.