← 전체 글로 돌아가기

Next.js

Next.js dynamic route에서 한글 slug를 다룰 때 생기는 문제

한글 URL은 브라우저가 퍼센트 인코딩하기 때문에 params.slug와 실제 저장된 슬러그가 일치하지 않을 수 있다.

블로그 글 slug를 한글로 쓰기로 했을 때 예상치 못한 곳에서 막혔다. 데이터베이스에는 리액트-훅-정리로 저장해뒀는데, 페이지에서 받는 params.slug%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%9B%85-%EC%A0%95%EB%A6%AC로 들어오는 경우가 있었다.

브라우저가 URL을 인코딩하는 방식

RFC 3986 기준으로 URL 경로에는 ASCII 범위 밖의 문자를 그대로 쓸 수 없다. 브라우저는 한글을 UTF-8로 인코딩한 뒤 각 바이트를 %XX 형식으로 변환한다. %EA%B0%80이 된다.

문제는 Next.js 버전과 환경에 따라 params.slug가 디코딩된 상태로 오기도 하고, 인코딩된 상태로 오기도 한다는 점이다. App Router(Next.js 13+)에서는 대체로 디코딩된 값을 넘겨주지만, 동적 경로를 직접 생성하거나 미들웨어를 거치는 경우 달라질 수 있다.

안전하게 처리하는 방법

decodeURIComponent를 한 번 감싸두면 인코딩 여부와 무관하게 같은 값을 얻을 수 있다.

// app/posts/[slug]/page.tsx
export default async function PostPage({
  params,
}: {
  params: { slug: string }
}) {
  const slug = decodeURIComponent(params.slug)
  const post = await getPostBySlug(slug)
  // ...
}

이미 디코딩된 값에 decodeURIComponent를 또 적용해도 문제없다. 단, %가 포함된 일반 문자열에는 에러가 날 수 있으므로 안전하게 처리하려면 아래처럼 쓴다.

function safeDecodeSlug(slug: string): string {
  try {
    return decodeURIComponent(slug)
  } catch {
    return slug
  }
}

generateStaticParams와 한글 slug

SSG 빌드 시 generateStaticParams에서 반환하는 slug가 인코딩되어 있어야 하는지 디코딩되어 있어야 하는지도 헷갈린다. Next.js는 이 값을 그대로 파일 경로에 쓰기 때문에, 한글 그대로 반환하는 게 낫다.

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({
    slug: post.slug, // '리액트-훅-정리' 형태 그대로
  }))
}

링크 생성 시 주의사항

<Link href={\/posts/${post.slug}`}>형태로 쓰면 Next.js가 자동으로 인코딩을 처리해준다. 하지만router.pushwindow.location을 직접 다룰 때는 encodeURIComponent`를 명시적으로 사용하는 게 안전하다.

router.push(`/posts/${encodeURIComponent(post.slug)}`)

일관성을 위해 DB에 저장할 때부터 slug를 영문 소문자 + 하이픈으로만 구성하는 방식도 있지만, 한글 제목을 그대로 slug로 쓰고 싶다면 디코딩 처리를 입구에서 한 번 확실히 해두는 게 핵심이다.