본문으로 건너뛰기
개발 머꼬
개발 노트Next.js
hohyeon.dev37

한글 제목의 글만 저장 직후 죽고, 열면 404가 난 이유 — 주소 인코딩은 두 방향이다

  • #Common Pitfall
  • #Engineering Note
  • #Next.js

문제 발생

글 주소에 한글 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가 됩니다.

해결 방안

  1. 경로를 만들고 읽는 함수를 한 곳에 둡니다. 호출부마다 템플릿 문자열로 만들면 한 군데씩 빠집니다.
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()합니다.

  1. decodeURIComponent는 감쌉니다. MDN 문서대로 % 뒤에 16진수 두 자리가 없거나 올바른 UTF-8이 아니면 URIError를 던집니다(%E0%A4%A 같은 입력). 잘못된 주소가 500이 되면 안 됩니다.

  2. 실제 한글 제목으로 브라우저에서 확인합니다. 두 버그 모두 타입 검사와 빌드를 통과합니다. 인코딩 round-trip은 테스트로 남깁니다.

assert.equal(decodePostSlug(postPath("한글-제목").replace("/posts/", "")), "한글-제목");
  1. revalidatePath()는 무관합니다. 내부 API라 헤더를 거치지 않습니다. 인코딩이 필요한 것은 브라우저로 나가는 값입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.