
인플루언서와 팬들이 소통할 수 있는 커뮤니티 사이드 프로젝트를 시작하면서, 영어와 한국어를 지원하는 다국어 환경을 구축하기로 했다. 해외에서도 팬 문화가 크게 발달했기 때문에, 다국어 지원은 필수적이었다.
Next.js 14의 App Router를 사용해 프로젝트를 진행했지만, 다국어 지원을 추가하는 과정이 예상보다 복잡했다. 다양한 i18n 라이브러리들이 있었지만, 각각의 장단점이 달라서 선택에 고민이 많았다.
이에, 한번 세팅을 했던 과정과 개념들을 정리해보고자 한다!
리액트와 Next.js는 다국어 처리 방식에서 큰 차이를 보인다. 리액트는 클라이언트 중심으로 동작하는 반면, Next.js는 서버사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 지원하며 서버에서 다국어를 처리할 수 있다.
1. 리액트의 다국어 처리
클라이언트 중심: 다국어 처리는 주로 브라우저에서 이루어진다. react-i18next 같은 라이브러리를 사용하며, 로케일 변경은 브라우저 설정이나 사용자 UI로 처리된다.
로케일 관리: 페이지 이동 시 로케일 정보를 유지하려면 상태나 URL을 직접 관리해야 하며, 새로고침 시 설정을 다시 적용해야 하는 불편함이 있다.
2. Next.js의 다국어 처리
주요 차이점 요약
리액트는 클라이언트에서 주로 번역 파일 로딩과 상태 관리를 처리하며, Next.js는 서버 중심의 다국어 처리와 URL 라우팅을 지원해 더 효율적이다.
Next.js 프로젝트에서 다양한 다국어 라이브러리를 검토한 결과, next-intl을 선택한 이유는 다음과 같다:
Next.js와의 원활한 통합: next-intl은 Next.js의 App Router와 긴밀하게 통합되며, 공식 문서에서 통합 방법이 자세히 설명되어 있어 호환성 문제를 최소화했다. 덕분에 설정이 쉽고 빠르게 이루어졌다.
타입스크립트 지원: 번역 키에 대한 타입 정의를 통해 잘못된 키 사용을 컴파일 타임에 방지할 수 있어, 대규모 프로젝트의 유지보수성이 크게 향상되었다.
유연한 라우팅 옵션: withRouting과 withoutRouting 두 가지 라우팅 옵션을 제공해, 프로젝트에 맞는 라우팅 설정이 가능했다. 특히 withRouting을 통해 URL로 로케일을 변경할 수 있어 사용자 경험을 개선했다.
경량화 및 성능 최적화: 불필요한 의존성을 줄이고 번역 파일을 효율적으로 로드해, 성능에 미치는 영향을 최소화했다.
react-i18next나 next-i18next와 비교했을 때, next-intl은 Next.js와의 통합성, 타입 안정성, 설정의 간편함에서 특히 강점이 있었으며, 최신 App Router와의 호환성에서도 다른 라이브러리들보다 앞섰다.
Next.js 프로젝트에서 다국어 지원을 위해 흔히 사용되는 라이브러리들이 몇 가지 있다. 그 중 대표적인 것들과 next-intl을 비교했을 때, App Router와의 호환성과 타입스크립트 지원에서 확실한 차이가 있었다.
| 라이브러리 | App Router 완벽 지원 | 장점 | 단점 |
|---|---|---|---|
| Next.js 내장 i18n | ✅ | 간단한 설정으로 다국어 라우팅 및 자동 리다이렉트 지원 | 기능이 제한적이고, 고급 다국어 처리에는 부족할 수 있음 |
| react-i18next | ❌ | 커뮤니티 지원 강력, Next.js에서도 사용 가능 | App Router 통합 부족, SSR 설정 번거로움, URL 로케일 관리 어려움 |
| next-i18next | ❌ | Next.js 최적화, SSR 및 페이지 라우팅과의 우수한 통합 | App Router와 완전한 호환성 부족, 타입스크립트 통합 미흡 |
| lingui.js | ❌ | 번역 파일 최적화, 성능 극대화 | App Router 통합 미흡, 다국어 라우팅 기능 수동 구현 필요, 번역 파일 관리 복잡 |
| formatjs/react-intl | ❌ | 국제화 표준 준수, 다양한 언어 및 지역별 포맷 지원 | Next.js 통합 부족, 다국어 라우팅 지원 미흡, 페이지 간 언어 전환 시 추가 설정 필요 |
| next-intl | ✅ | App Router와 완벽 통합, SSR 및 타입스크립트 통합, 간편한 라우팅 설정 | 비교적 신생 라이브러리, 커뮤니티 및 리소스 부족 |
react-i18next와 next-i18next는 널리 사용되지만, 최신 App Router를 사용하는 Next.js 13 이상에서는 next-intl이 유일하게 완벽한 통합을 제공하는 라이브러리다. 특히 라우팅 관리와 타입스크립트 지원이 중요한 프로젝트라면 next-intl이 가장 적합한 선택이라고 생각한다.
next-intl을 설정할 때, withRouting과 withoutRouting 두 가지 주요 옵션이 있다.
withRouting: 로케일 정보를 URL에 포함시키는 방식이다. 예를 들어, /en/about, /ko/about과 같이 각 로케일별로 다른 URL 경로를 가진다. 이 방식의 장점은 사용자가 URL을 통해 로케일을 명시적으로 변경할 수 있다는 점이다.
withoutRouting: 로케일 정보를 URL에 포함시키지 않고, 다른 방법(예: 쿠키, 세션 등)을 통해 로케일을 관리하는 방식이다.
나는 withRouting을 선택했다. 그 이유는 사용자가 URL을 통해 직접 로케일을 변경할 수 있어, 다양한 언어 페이지로의 접근성을 높일 수 있었기 때문이다. 또한, SEO 측면에서도 로케일별 URL을 제공함으로써 검색 엔진 최적화에 유리했다.
yarn add next-intl
사용자의 브라우저 설정에 따라 적절한 로케일로 리다이렉트하는 미들웨어를 설정하였다. Next.js의 Middleware를 사용하여 다음과 같이 구현하였다.
next.js 설정
next.config.mjs 파일에서 next-intl 플러그인을 Next.js 설정에 통합하여 다국어 기능을 추가한다.
next.config.mjs
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
const nextConfig = {
// 기타 Next.js 설정
};
export default withNextIntl(nextConfig);
다국어 처리 및 라우팅 설정
요청된 로케일이 유효한지 확인하고, 해당 로케일의 번역 메시지를 로드한다.
src/i18n/request.ts
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
import { routing, type Locale } from './routing';
export default getRequestConfig(async ({ locale }) => {
// 유효한 로케일인지 검증
if (!routing.locales.includes(locale as Locale)) notFound();
return {
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
다국어 지원을 위한 라우팅 설정과 Next.js 내비게이션 API의 경량 래퍼를 정의
src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { createSharedPathnamesNavigation } from 'next-intl/navigation';
export const supportedLocales = ['ko', 'en']; // 지원 언어
export type Locale = (typeof supportedLocales)[number];
export const routing = defineRouting({
locales: supportedLocales,
defaultLocale: supportedLocales[0], // 기본 설정 언어
});
// Next.js 내비게이션 API를 고려한 경량 래퍼
export const { Link, redirect, usePathname, useRouter } = createSharedPathnamesNavigation(routing);
미들웨어 설정
요청 URL에 따라 적절한 로케일로 리다이렉트하고, 이전 로케일 정보를 쿠키에 저장하는 미들웨어를 설정한다.
src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: [
// 루트에서 로케일에 맞는 페이지로 리다이렉트
'/',
// 로케일 접두사가 있는 모든 요청에 대해 이전 로케일을 기억하기 위한 쿠키 설정
'/(ko|en)/:path*',
// 누락된 로케일을 추가하는 리다이렉트 활성화
// (예: `/pathnames` -> `/en/pathnames`)
'/((?!_next|_vercel|.*\\..*).*)',
],
};
다국어 파일을 JSON 형식으로 작성하고, 타입스크립트를 사용하여 번역 키의 타입을 정의했다. 이는 잘못된 번역 키 사용을 방지하는 데 도움을 주었다.
키 값에 대해서는 고민을 많이 했는데, 한국어로 설정하는 것이 추후 유지보수에 훨씬 편할 것이라 생각했다.
다국어 파일 만들기
messages/en.json
{
"loginPage": {
"회원가입 완료": "Registration Complete",
"나중에 하기": "Do it Later",
"닉네임 설정하러 가기": "Set Your Nickname",
},
"mainPage": {
"{nickName}님 안녕하세요": "Hello, {nickName}!"
}
}
messages/ko.json
{
"loginPage": {
"회원가입 완료": "회원가입 완료",
"나중에 하기": "나중에 하기",
"닉네임 설정하러 가기": "닉네임 설정하러 가기",
},
"mainPage": {
"{nickName}님 안녕하세요": "{nickName}님 안녕하세요!"
}
}
타입 정의
번역 파일의 타입을 정의하여, 잘못된 키 사용을 방지한다. 이 타입 정의는 모든 번역 키가 올바르게 사용되도록 보장하며, 타입스크립트에서 자동으로 타입 검사를 수행할 수 있게 한다.
global.d.ts
import ko from './messages/en.json';
type Messages = typeof ko;
declare global {
type IntlMessages = Messages;
}
로케일별로 다른 루트를 관리하기 위해 Next.js의 App Router를 활용했다.
각 로케일에 맞는 메시지 파일을 자동으로 로드하고, 전체 애플리케이션에 번역을 제공하기 위해 app/[locale]/layout.tsx 파일을 생성하여 레이아웃을 설정했다.
이렇게 설정함으로써, 각 로케일에 맞는 메시지 파일을 자동으로 로드하고, 전체 애플리케이션에 번역을 제공할 수 있었다.
app/[locale]/layout.tsx
import { getMessages } from 'next-intl/server';
import { NextIntlClientProvider } from 'next-intl';
// ...메타태그, 폰트 등 생략
export default async function RootLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale}>
<AppHead />
<body className={inter.className}>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}
next-intl의 createSharedPathnamesNavigation을 사용하여 라우팅 관련 API를 제공받고, ESLint 설정을 통해 직접 next/navigation이나 next/link에서 가져오는 것을 방지했다.
createSharedPathnamesNavigation 기능
1. 라우팅 API 생성
라우팅 설정(routing)을 기반으로 Link, redirect, usePathname, useRouter와 같은 네비게이션 관련 API를 생성
생성된 API는 라우팅 설정과 일관성 있게 동작하며, URL 변경 시 로케일 정보도 유지
2. URL 내 로케일 관리
createSharedPathnamesNavigation을 사용하면 URL에 로케일 정보를 포함시키는 방식(withRouting)을 쉽게 관리할 수 있다.
사용자가 로케일을 변경할 때, URL에 로케일을 포함시켜 자동으로 페이지를 리다이렉트하거나 로케일을 유지할 수 있다.
3. 일관된 네비게이션
Link 컴포넌트와 라우팅 관련 훅(useRouter, usePathname)을 일관되게 사용하여, 애플리케이션 전체에서 네비게이션 처리를 통일
예를 들어, Link를 클릭하면 로케일이 포함된 URL로 이동하게 되며, 이로 인해 사용자가 다른 로케일의 페이지로 원활하게 이동할 수 있다.
이렇게 설정하면 next-intl의 라우팅 API를 통해 일관성 있는 다국어 처리를 할 수 있으며, 코드의 유지보수성도 높아진다.
EsLint 설정
next/link와 next/navigation 대신 @/i18n/routing에서 Link, redirect, usePathname, useRouter를 임포트하도록 강제한다.
.eslintrc.json
{
"rules": {
...
"no-restricted-imports": [
"error",
{
"name": "next/link",
"message": "Please import from `@/i18n/routing` instead."
},
{
"name": "next/navigation",
"importNames": ["redirect", "permanentRedirect", "useRouter", "usePathname"],
"message": "Please import from `@/i18n/routing` instead."
}
]
}
}
useTranslations 훅을 사용하여 다국어 JSON 파일에서 정의한 번역 키를 가져오고, 번역된 문자열을 쉽게 사용할 수 있다. 중괄호 {}로 감싸진 부분은 동적으로 값을 치환할 수 있다.
import { useTranslations } from 'next-intl';
import { Link, usePathname } from '@/i18n/routing';
export default function HomePage() {
const t = useTranslations('LoginPage');
const t2 = useTranslations('MainPage');
const pathname = usePathname(); // 현재 페이지의 경로를 가져오기
return (
<div>
<h1>{t('회원가입 완료')}</h1>
<h1>{t('나중에 하기')}</h1>
{/* 중괄호로 감싼 부분은 값으로 대체 */}
<h1>{t2('{nickName}님 안녕하세요', { nickName: '닉네임' })}</h1>
<hr />
{/* 경로는 유지하면서 언어만 바꾸기 */}
<Link href={pathname} locale="ko">KR</Link>
<Link href={pathname} locale="en">EN</Link>
</div>
);
}
다국어 파일 관리를 보다 효율적으로 하기 위해 i18n-ally VSCode 확장 프로그램을 사용했다. i18n-ally는 다국어 키 관리, 자동 완성, 오류 검출 등 다양한 기능을 제공하여 번역 파일 작업을 크게 단순화했다. Sherlock과 함께 공식 문서에서도 추천하는 도구로, 사용하면 번역 파일 관리가 더욱 편리해진다.
그리고 팀원들과 통일성 있는 사용을 위해 다음 세팅을 추가했다.
.vscode/settings.json
{
"i18n-ally.localesPaths": ["messages"], // 다국어 파일이 위치한 경로를 설정
"i18n-ally.keystyle": "nested", // 번역 키의 스타일을 설정 (nested: 중첩)
"i18n-ally.sourceLanguage": "ko", // 기본 소스 언어를 설정
"i18n-ally.displayLanguage": "ko" // VSCode에서 표시할 언어를 설정
}
다국어 파일의 컨벤션을 설정할 때, 일관된 네이밍 규칙과 파일 구조를 유지하는 것이 중요했다. 이를 위해 다음과 같은 방식을 채택했다.
이 부분은 프로젝트의 규모나 성격, 그리고 팀원들과의 의견에 따라 관리 방식이 천차만별일 것 같다.
{
"common_word" :{ // 비문장형 단일 단어들
"확인" : "확인"
},
"page1" :{
"page1에 국한되는 단어" : "page1에 국한되는 단어",
"문장1" : "문장1"
},
"page2" :{
"page2에 국한되는 단어" : "page2에 국한되는 단어",
"문장2" : "문장2",
},
}
중첩 구조 사용: 번역 키를 논리적인 섹션별로 중첩된 객체 구조로 작성하여 관리의 용이성을 높였다.
공통 단어에 대한 섹션 나누기: "확인", "취소", "예", "아니오", "뒤로가기" 등 여러 곳에서 사용될 수 있는 단어는 common_word로 분류하여 중복 번역되지 않게 했다. 이는 번역 파일의 일관성을 유지하고 중복을 줄이는 데 도움을 준다고 생각한다.
키 명명 규칙 설정: 최상단 키는 소문자와 밑줄을 사용하여 일관되게 작성했다. 문장이나 단어의 키값은 한국어를 사용하여, 번역 작업 중 혼동을 줄이고 직관적인 네이밍을 제공한다.
Next.js의 App Router와 함께 next-intl을 사용하여 다국어 지원을 구현한 것은 타입 안전성, 설정의 간편함, 성능 최적화 등 여러 면에서 만족스러운 선택이었다.
공식 문서에서 제공하는 가이드라인이 매우 친절하게 설명되어 있어, 다양한 커스터마이징 옵션을 활용함으로써 프로젝트의 요구사항에 맞춘 효율적인 다국어 지원을 구현할 수 있었고. i18n-ally와 Sherlock과 같은 확장 프로그램을 함께 사용하여 번역 파일 관리를 체계적으로 할 수 있었다.
https://next-intl-docs.vercel.app/docs/getting-started/app-router/with-i18n-routing
https://next-intl-docs.vercel.app/docs/workflows/vscode-integration
https://github.com/lokalise/i18n-ally/wiki/Configurations