Next.js 다국어 경로 설계: 쿠키와 하위 경로
공개하지 않고 내부에서만 여는 제품 카탈로그 데모에 언어 전환을 붙였습니다. 한국어, 영어, 중국어, 일본어, 독일어, 프랑스어 여섯 개입니다. 고른 언어는 쿠키에 저장하고, 쿠키가 없으면 브라우저의 Accept-Language 헤더를 봅니다. 주소는 나누지 않았습니다. /products/<기종> 하나로 여섯 언어를 모두 보여 줍니다.
내부용 데모에서는 이 구성이 간단하고 잘 동작합니다. 그리고 이 사이트를 밖에 공개하는 순간 먼저 뒤집어야 하는 결정이기도 합니다.
언어 전환은 화면에 보이는 문제라는 생각
다국어를 붙이는 일은 흔히 화면의 문제로 이해됩니다. 문구를 사전으로 빼고, 언어를 고르는 메뉴를 달고, 고른 값을 저장하면 끝나는 것처럼 보입니다. 쿠키에 저장하면 주소도 깔끔하게 유지됩니다.
검색엔진 쪽에서 보면 다른 그림이 됩니다. 크롤러는 쿠키를 들고 오지 않습니다. 같은 주소로 요청하면 언제나 기본 언어의 페이지를 받습니다. 여섯 언어로 번역해 두었어도 검색엔진이 아는 것은 한국어 페이지 하나뿐입니다. 다국어의 목적이 판로 확대라면, 이 구성은 그 목적을 달성하지 못합니다.
문서가 말하는 두 가지 경로 방식
국제화 문서는 라우팅을 국제화하는 방법을 두 가지로 적습니다.
Routing can be internationalized by either the sub-path (
/fr/products) or domain (my-site.fr/products).
하위 경로 방식은 /ko/products, /en/products 처럼 주소 앞에 언어를 붙입니다. 도메인 방식은 언어마다 다른 도메인을 씁니다. 둘 다 언어가 주소에 드러나므로 검색엔진이 언어별 페이지를 따로 색인할 수 있습니다.
문서는 브라우저의 언어 설정을 참고하되, 그 판단을 Proxy 에서 하라고 안내합니다. Accept-Language 헤더로 언어를 고르고, 주소에 언어가 없으면 그 언어의 경로로 보내는 구조입니다.
export function proxy(request) {
const { pathname } = request.nextUrl;
const hasLocale = locales.some((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`);
if (hasLocale) return;
const locale = matchLocale(request.headers.get("accept-language"));
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}앱 쪽에서는 app/[lang]/... 처럼 언어를 동적 세그먼트로 받습니다. 세그먼트 값이 곧 사전을 고르는 키가 됩니다.
나중에 바꾸면 주소가 전부 바뀐다
쿠키 방식에서 경로 방식으로 옮기면 사이트의 모든 주소가 바뀝니다. /products/… 가 /ko/products/… 가 됩니다. 이미 색인된 주소, 공유된 링크, 외부에서 건 링크가 전부 리다이렉트를 타야 하고, 그 신호가 합쳐지는 데 시간이 걸립니다.
그래서 이 데모의 저장소 문서에는 경고를 적어 두었습니다. 내부용이라 지금은 쿠키 방식이 맞지만, 밖에 공개할 계획이 생기면 공개 전에 경로를 나누고 hreflang 을 걸어야 한다는 내용입니다. 공개한 뒤에 바꾸면 비용이 크게 늘어납니다.
언어 전환에서 바꾸기 쉬운 것과 어려운 것을 나누면 이렇습니다.
| 항목 | 나중에 바꾸기 |
|---|---|
| 문구 사전, 번역 품질 | 쉽다 |
| 언어 선택 UI | 쉽다 |
| 기본 언어 판단 규칙 | 쉽다 |
| 주소 구조 | 어렵다. 모든 URL 이 바뀐다 |
사전을 둘 자리와 클라이언트로 넘어가는 값
경로를 나누기로 했다면 다음 결정은 사전을 어디서 읽느냐입니다. 서버 컴포넌트에서 언어별 사전을 읽어 문구만 내려 주면, 사전 전체가 브라우저 번들에 실리지 않습니다. 여섯 언어를 지원하면서 한 언어 분량만 전송하는 구조가 됩니다.
클라이언트 컴포넌트가 문구를 써야 한다면 그 문구만 props 로 넘깁니다. 이때 넘기는 값은 직렬화할 수 있는 값이어야 하므로, 사전 값은 글자와 {n} 같은 자리 표시자로만 두고 함수는 넣지 않는 편이 안전합니다. 숫자나 날짜 형식은 Intl 로 화면에서 만듭니다.
번역하지 않는 값도 정해 둬야 합니다. 이 데모에서는 회사 이름의 한자 표기를 모르기 때문에 중국어와 일본어 화면에서도 로마자 표기를 그대로 씁니다. 모르는 것을 지어내지 않는 쪽이 낫다고 봤습니다. 데이터베이스에 한국어로 저장된 제조사 이름은 표시할 때만 사전으로 바꾸고, 저장은 원래 값으로 둡니다.
공개 전에 정할 것
| 결정 | 기본값으로 삼을 만한 것 |
|---|---|
| 주소 구조 | 공개할 사이트라면 하위 경로(/ko, /en) |
| 기본 언어 판단 | Proxy 에서 Accept-Language 를 보고 언어 경로로 리다이렉트 |
| 언어 간 관계 표시 | 각 페이지에 hreflang 로 다른 언어 주소를 알림 |
| 사전 위치 | 서버에서 읽고 필요한 문구만 클라이언트로 |
| 번역하지 않을 값 | 고유명사, 표기를 모르는 이름 |
내부 전용이거나 로그인 뒤에만 쓰는 화면이라면 쿠키 방식으로 시작해도 됩니다. 다만 그 선택을 기록해 두는 것이 중요하다고 봅니다. 나중에 공개 논의가 시작될 때 「주소부터 바꿔야 한다」는 사실이 함께 떠올라야 하기 때문입니다.
여기까지가 확실한 부분
하위 경로와 도메인이라는 두 가지 국제화 라우팅 방식, Accept-Language 를 참고해 Proxy 에서 리다이렉트하는 구조는 Next.js 16 문서 기준입니다. 쿠키로만 언어를 가르면 검색엔진이 한 언어만 색인하게 된다는 설명은 크롤러가 쿠키를 보내지 않는다는 성질에서 따라 나오는 판단이고, 특정 검색엔진의 동작을 문서로 확인한 것은 아닙니다. 데모의 구성과 공개 전 경고는 이 프로젝트의 저장소 문서에 적혀 있는 내용입니다.
함께 읽기
- Next.js 병렬 라우트와 인터셉트 라우트: 모달 패턴과 default.js사진 목록에서 사진을 누르면 모달로 크게 보여 주고, 같은 주소를 새 탭에서 열면 전체 페이지로 보여 주는 화면이 있습니다. 주소는 /photo/123 하나인데 보이는 모양이 둘입니다.
- Next.js 데이터 가져오기 순서: 순차와 병렬, 그리고 Suspense 위치서버 컴포넌트에서 데이터를 읽는 코드는 이렇게 생깁니다.
- Next.js 라우트 핸들러와 서버 함수 구분앱 라우터에서 서버로 무언가를 보내는 방법이 둘입니다. app/api/.../route.ts 에 라우트 핸들러를 만들거나, "use server" 를 붙인 서버 함수를 부르는 것입니다. 폼 하나를 붙일 때마다 어느 쪽을 쓸지 정해야 하는데, 「둘 다 서버에서 도니 아무거나」로 두면 나중에 갈립니다.
- Next.js 이미지 최적화: next/image와 빌드 때 굽기제품 카탈로그 데모를 만들면서 이미지 처리 방식을 정해야 했습니다. 화면에 나가는 사진은 제품 사진 몇 장과 로고뿐이고, 모두 저장소에 들어 있는 파일입니다. 요청마다 달라질 것이 없습니다.
- Next.js 환경 변수: NEXT_PUBLIC_ 인라인과 로드 순서같은 Docker 이미지를 스테이징과 운영에 함께 올리는 구성이 있습니다. 환경 변수는 각 환경의 설정으로 주입합니다. 서버 쪽 값은 환경에 맞게 잘 바뀌는데, 브라우저에서 쓰는 분석 도구 ID 만 두 환경에서 같은 값이 나옵니다. 스테이징의 방문 기록이 운영 통계에 섞입니다.