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