노트

next-intl과 서버 컴포넌트 친화 i18n

next-intl

프런트엔드#nextjs · 연결된 개념 8개

쉽게 말하면

next-intl은 번역 사전을 집집마다 나눠 주지 않고, 요청이 올 때마다 서버에서 꺼내 보게 하는 번역 라이브러리예요. 그래서 번역만 쓰는 컴포넌트가 클라이언트로 끌려가지 않고 서버 컴포넌트로 남아요.

비유가 깨지는 곳 클라이언트에도 사전이 필요한 곳은 있어요. 상호작용 부분만 NextIntlClientProvider로 감싸 쓰는 메시지만 넘기고, 로케일을 쿠키나 헤더로 정하면 cacheComponents를 켰을 때 정적 셸에서 빠져요.

next-intl은 Next.js App Router와 서버 컴포넌트를 전제로 설계된 국제화(Internationalization, i18n) 라이브러리다. 번역 메시지를 React Context가 아니라 요청 단위 설정 함수에서 읽어서, 번역만 쓰는 컴포넌트를 서버 컴포넌트로 남길 수 있다.

Context 기반 i18n의 문제

react-i18next 같은 라이브러리는 CSR(Client-Side Rendering) 시절에 만들어져 번역 데이터를 Context로 내려 준다. useTranslation이 내부에서 useContext를 부르고, Context Provider는 클라이언트 기능이다. 그래서 번역을 쓰는 컴포넌트가 줄줄이 클라이언트 경계 안으로 끌려 들어가 서버 컴포넌트의 이점(번들 0, 서버 전용 렌더)을 잃는다.

next-intl의 방식

// i18n/request.ts — 요청마다 로케일과 메시지를 정한다(메시지 출처는 파일·CDN·CMS 무엇이든)
export default getRequestConfig(async () => {
  const locale = 'ko' // 라우팅 없이 고정. 다국어라면 URL 세그먼트·쿠키 등에서 결정
  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  }
})
export default function HomePage() {          // 상호작용이 없으니 서버 컴포넌트
  const t = useTranslations('HomePage')
  return <h1>{t('title')}</h1>
}
  • 같은 API(Application Programming Interface)가 양쪽에서 동작한다. 패키지의 react-server 조건부 export 덕분에 서버에서는 요청 설정을, 클라이언트에서는 Context를 읽는다. async 컴포넌트에서는 훅 대신 await getTranslations()를 쓴다
  • 클라이언트로 보낼 메시지를 고른다. 상호작용하는 부분만 NextIntlClientProvider로 감싸고, 그 부분이 쓰는 메시지만 넘긴다. 넘긴 메시지는 HTML(HyperText Markup Language)에 실려 가므로 전부 넘기면 페이로드와 메인 스레드 비용이 커진다(RSC 페이로드(Flight))

i18next에서 옮길 때: 호환 레이어(shim)

번역 호출부가 수백 곳이면 한 번에 고치기 어렵다. 대신 i18next와 같은 시그니처(useTranslation('ns') → { t })를 흉내 내는 얇은 모듈을 만들고, 내부만 next-intl로 바꾼다. 호출부는 import 경로 한 줄만 바뀐다. 이를 병렬 수정 (팽창-수축)처럼 단계적으로 진행하면 리뷰 단위가 작아진다.

  • 서버용 모듈을 따로 둔다. 훅은 async 함수 안에서 못 부르므로 generateMetadata와 async 서버 컴포넌트는 await getTranslations() 쪽 래퍼가 필요하다. next-intl/server는 서버 전용이라 클라이언트 공용 모듈과 파일을 나눠야 클라이언트 번들로 끌려오지 않는다
  • 키 규칙 차이를 메운다. next-intl은 .을 중첩 구분자로 쓰고 키 안의 .을 허용하지 않는다(INVALID_KEY). 키에 점이 든 기존 메시지는 빌드 때 변환본을 만들거나 키를 바꿔야 한다. 보간 문법도 i18next의 {{name}}에서 ICU(International Components for Unicode) 메시지 형식의 {name}으로 바뀐다
  • 누락 키 동작이 다르다. i18next는 키를 찾지 못하면 키 문자열을 그대로 돌려준다. next-intl은 에러를 로그로 남기고 네임스페이스.키를 렌더한다. getMessageFallback·onError로 바꿀 수 있다
  • 래퍼가 렌더마다 새 t 함수를 만들어 반환하면, t를 의존성으로 둔 effect가 매번 다시 돌고 메모이제이션이 무력해진다. 래퍼에서 참조를 안정시킨다

cacheComponents와 로케일 결정

Next.js 16에서 cacheComponents를 켜면 cookies()·headers() 같은 요청 시점 API를 읽는 부분은 미리 렌더할 수 없고 요청 때 렌더된다. i18n/request.ts가 쿠키나 Accept-Language 헤더로 로케일을 정하면, 번역만 쓰는 컴포넌트까지 그 영향을 받아 정적 셸에서 빠진다(부분 사전 렌더링(PPR)).

  • 단일 로케일이면 요청을 보지 않고 로케일을 고정해 반환하는 것이 가장 단순하다. 정적·부분 프리렌더가 그대로 유지된다
  • 다국어라면 로케일을 app/[locale] 같은 URL 세그먼트로 옮겨, 로케일마다 미리 렌더하는 쪽이 캐시와 맞는다. 대신 빌드 산출물과 시간이 로케일 수만큼 늘고, 라우팅·링크도 로케일을 알아야 한다
  • Next.js 16.3에 추가된 next/root-params로 루트 세그먼트 값을 어디서든 읽을 수 있게 되면서, next-intl은 정적 렌더를 위해 쓰던 setRequestLocale을 지워도 된다고 안내한다. getRequestConfig의 requestLocale 인자도 레거시로 더 이상 권하지 않는다(2026 기준)

트레이드오프

i18next 생태계는 백엔드 플러그인, 누락 키 자동 저장, 번역 관리 도구 연동이 성숙했다. next-intl은 이런 편의 기능 일부를 직접 구성해야 한다. 서버 컴포넌트 친화성과 번들 감소를 얻는 대신이다.

언어별 URL(Uniform Resource Locator)과 라우팅은 Pages Router에서 App Router로, 번역 키 규칙을 타입으로 강제하는 방법은 템플릿 리터럴 타입.

출처: next-intl - Request configuration · next-intl - Next.js root params · Next.js - Caching: Working with runtime APIs

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • Next.js 캐시 계층

    Next.js 14~15의 App Router 문서는 캐시를 위치와 수명이 다른 네 계층으로 설명했다. Next.js 16에서 Cache Components를 켜면 캐싱을 'use cache'로 명시하는 모델로 바뀌고 문서도 이 이름들을 쓰지 않는다(use-cache). 그래도 옛 코드와 아티클을 읽으려면 이 그림이 필요하다(2026 기준).

  • "use cache"와 Cache Components

    'use cache'는 Next.js 16의 Cache Components 모델에서 async 함수나 컴포넌트의 반환값을 캐시하라고 명시하는 지시어(Directive)다. 아무것도 표시하지 않으면 매 요청 실행되고, 캐시하고 싶은 곳에만 직접 붙인다(옵트인, Opt-in).

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

  • Next.js output: 'standalone'

    output: 'standalone'은 next build가 프로덕션 실행에 필요한 파일만 골라 .next/standalone 폴더에 모아 주는 빌드 출력 모드다. 이 폴더는 node_modules를 다시 설치하지 않고 node server.js 하나로 실행된다.

  • 버전 스큐와 정적 자산 오프로드

    버전 스큐(Version Skew)는 새 버전을 배포한 뒤에도 브라우저에 떠 있는 이전 빌드의 클라이언트가 새 서버와 통신하면서 생기는 불일치다. 대표 증상은 이전 HTML이 참조하던 JS 청크를 늦게 불러오려다 404가 나는 ChunkLoadError다.

보기 옵션