next-intl로 만드는 진짜 다국어 Next.js 사이트
지역화 라우팅, 정적 렌더링, hreflang, 그리고 언어 전환: 성능도 SEO도 희생하지 않고 Next.js 사이트를 열 개 언어로 구성하는 방법.
왜 다국어가 진짜 아키텍처 문제인가
인터페이스를 번역하는 것은 쉽습니다. 제대로 된 다국어 사이트——색인 가능하고, 빠르며, 유지보수되는——를 만드는 것은 끝이 아니라 처음에 내리는 아키텍처 결정입니다. 이 포트폴리오는 열 개 언어로 존재하며, 규모에서 버티는 선택들을 소개합니다.
로케일 세그먼트 기반 라우팅
App Router에서 가장 견고한 접근: 루트에 동적 [locale] 세그먼트를 두는 것. 모든 페이지는 app/[locale]/… 아래에 있고, 언어는 일급 URL 데이터가 됩니다(/fr/projects, /en/projects).
next-intl에서는 이 세그먼트가 렌더링 시 검증됩니다:
- 지원 로케일은 중앙
routing설정에 나열합니다. - 알 수 없는 로케일은 깨진 페이지가 아니라 깔끔한
notFound()를 일으킵니다. - 내비게이션은 올바른 언어를 자동으로 접두하는 지역화된
Link를 거칩니다.
이점: 컴포넌트가 로케일을 「손으로」 다루는 일은 결코 없습니다. 번역을 요청하면, 나머지는 시스템이 맡습니다.
정적 렌더링의 함정
프로덕션에서 가장 값비싼 실수입니다. 먼저 로케일을 고정하지 않고 번역을 호출하면, next-intl은 요청 헤더를 읽습니다. 결과: 페이지가 동적 렌더링으로 뒤집히고, 정적 프리렌더링을 잃습니다——때로는 프로덕션에서 DYNAMIC_SERVER_USAGE 오류와 함께.
해법은 각 페이지 맨 위의 한 줄에 담깁니다:
setRequestLocale(locale);
어떤 getTranslations보다 먼저 호출하면, 완전한 정적 렌더링을 보장합니다. locale × slug를 교차하는 generateStaticParams와 결합하면, 빌드 시 각 페이지를 각 언어로 프리렌더링합니다.
정적 다국어는 단일 언어보다 느리지 않습니다. 다만 호출 순서에 더 까다로울 뿐입니다.
hreflang, 혹은 자기 자신과 경쟁하지 않는 법
아무런 힌트가 없으면, Google은 당신의 열 개 언어 버전을 같은 키워드를 두고 다투는 열 개의 경쟁 페이지로 취급합니다. 해법은 hreflang: 각 페이지가 자신의 변형 전체를 선언합니다.
두 곳이 동기화되어야 합니다:
- 각 페이지의 메타데이터 —
alternate태그를 생성하는generateAlternates헬퍼를 통해. - 사이트맵 — 각 URL이 거기에 자신의 언어들과
x-default를 나열합니다.
이 두 출처 사이의 일관성이야말로, 스페인어 검색을 /fr가 아니라 /es에 착지시키는 것입니다.
언어 전환, 사용자 쪽에서
언어 선택기는 현재 페이지를 잃지 않고 로케일을 바꿔야 합니다. 순진한 반사——사용자를 홈으로 돌려보내기——는 답답합니다. next-intl의 지역화된 usePathname을 쓰면, 접두사 없는 경로를 얻어 선택한 언어로 다시 내보냅니다. 사용자는 있던 자리에 그대로 머뭅니다.
내가 새기는 것
- 루트의
[locale]세그먼트가 토대입니다; 모든 것이 거기서 흘러나옵니다. - 번역 전에
setRequestLocale: 정적을 지키기 위한, 협상 불가한 규칙. hreflang는 두 곳에서 관리합니다(메타데이터 와 사이트맵), 늘 동기화된 채로.- 텍스트 방향(아랍어의 RTL)은 페이지마다가 아니라
<html dir>레벨에서 제어합니다.
잘 해내면, 다국어는 보이지 않게 됩니다: 모든 방문자가 이 사이트가 자기를 위해 쓰였다고 느끼고, Google은 어느 문을 열어 줄지 정확히 압니다.