요즘의 모던하고 글로벌한 웹에는 국제화(i18n)와 지역화(i10n)라는 개념이 등장합니다.
쉽게 정리하면 다음과 같습니다.
✅ 국제화(i18n, Internationalization):
→ 다국어 지원을 위해 코드 구조를 준비하는 과정
→ 예: 문자열을 하드코딩하지 않고 번역 파일을 사용
✅ 지역화(l10n, Localization):
→ 특정 언어와 문화에 맞게 UI와 콘텐츠를 변경하는 과정
→ 예: 날짜/시간 형식 변경, 통화 기호 적용 ($, ¥ 등) , 번역된 텍스트 사용
🚀 쉽게 말하면 i18n은 "준비", l10n은 "적용"하는 과정이다.
이번에 저희 회사에서 신규 백오피스 프로젝트에 일본어 지원이 필요해
국제화를 적용하게 되었습니다.
Next.js 15의 앱라우터에서 국제화를 도입한 경험을 공유합니다.
함께 알아봅시다. 😎
📝 라이브러리 선택
⦁국제화 라이브러리
Next.js는 v10.0.0부터 기본적으로 국제화 라우팅을 지원합니다.
하지만 이는 URL을 locale과 동기화 상태로 유지하는 것뿐이며 실제 번역 기능 자체를 처리하지는 않습니다.
모든 라이브러리가 그렇듯 직접 구현하기 번거로운 작업들을 처리해 주기 때문에
저는 먼저 Next.js의 국제화 라이브러리들을 리서치 해봤습니다.
프론트엔드는 특히 라이브러리의 생태계가 엄청 활발하기 때문에 알아보는 것만으로도
어느정도 공통된 컨벤션과 관련된 개념을 배우기 좋다는 장점도 있습니다. 😀
⦁npm trends 비교
1. next-i18next
페이지 라우터를 사용하는 경우에 엄청난 기능을 활용할 수 있다고 합니다. (뭔지는 몰라요)
커뮤니티도 활발합니다. 하지만 4.25MB나 되는 무거운 패키지 사이즈에 앱 라우터를 지원하지 않는 것은 치명적입니다.
앱 라우터를 사용하는 경우 i18next를 사용하라고 설명되어 있습니다.
이름이 비슷해서 똑같은 라이브러리인 줄 알았습니다.
2. i18next
주간 다운로드 수가 무려 700만인 공룡입니다.
처음 차트에 포함시키지 않은 것은 차트가 뭉개져서 비교가 안되기 때문입니다.
만들어진 지 13년이 지났고 업데이트도 활발합니다.
패키지 사이즈도 830kb로 훌륭합니다.
재미있는 점은 next-intl과 라이벌인 듯합니다. 공식 문서에 next-intl을 언급하고 있습니다. 🤔
재미있으니 한번 읽어보세요 !
3. next-intl
85.6kb로 가장 가벼운 패키지 사이즈이며, 문서가 잘 정리되어 있어 설정이 간단하고 배우기 쉽습니다.
튜토리얼만 진행해도 어렵지 않게 프로젝트에 국제화를 적용할 수 있었습니다.
CSR과 SSR 모두 지원하며, Next.js의 최신 버전과 호환성이 상당히 좋습니다.
예를 들어 useRouter와 Link를 래핑한 컴포넌트를 제공해
import만 바꿔줘도 기존 코드 변경 없이 locale에 맞는 경로 탐색이 가능합니다.
이 외에도 VSCode 확장 플러그인 등의 편의 기능도 존재합니다.
단점은 프로젝트가 커질 경우 언어별 메시지 json을 관리하기 힘들 수 있습니다.
이런 조건들을 비교한 끝에 저희 프로젝트에 next-intl이 가장 사용하기 적합하다고 판단했습니다.
여러분도 프로젝트 상황에 맞게 선택하세요. 😎
📝 next-intl 시작하기
https://next-intl.dev/docs/getting-started/app-router
Next.js App Router Internationalization (i18n) – Internationalization (i18n) for Next.js
next-intl.dev
✅ 공식 문서의 가이드를 따라가는 것을 추천드리긴 하지만, 버리긴 아까우니 저도 적어보겠습니다.
⦁설치
npm install next-intl
# or
yarn add next-intl
⦁파일 구조 만들기
├── messages
│ ├── en.json (1)
│ └── ...
├── next.config.mjs (2)
└── src
├── i18n
│ ├── routing.ts (3)
│ └── request.ts (5)
├── middleware.ts (4)
└── app
└── [locale]
├── layout.tsx (6)
└── page.tsx (7)
✅ 위와 같이 총 7개의 파일을 생성해야 합니다.
파일이 모두 생성되기 전에 프로젝트를 실행하면 필요한 파일이 존재하지 않는다는 에러가 발생합니다.
모두 생성하고 테스트 하는 것을 추천드립니다.
⦁(1) messages/en.json
// messages/en.json
{
"MainPage": {
"title": "Welcome to the Main Page",
"description": "Welcome!"
}
}
// messages/ko.json
{
"MainPage": {
"title": "메인페이지에 어서오세요",
"description": "하이"
}
}
...
✅ 언어별로 사용할 번역 텍스트를 json 파일에 저장합니다.
⦁(2) next.config.ts 수정
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
✅ 이 플러그인을 적용하면 서버 컴포넌트에 요청 별 별칭(alias)을 생성합니다.
⦁(3) src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
export const routing = defineRouting({
// 지원되는 언어를 입력하세요 !
locales: ['en', 'ko', 'jp'],
// 기본 언어 설정
defaultLocale: 'en'
});
// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
✅ 폴더명과 파일이름이 중요하니 꼭 확인해서 맞춰주세요 !
여기서 export된 컴포넌트를 사용하면 언어에 맞는 경로로 작업할 수 있습니다.
❗️따라서 기존 컴포넌트를 다음과 같이 대체해서 사용해야 합니다.
import Link From 'next/link'
->
import { Link } from "@/i18n/routing"
or
import { Link as I18nLink } from "@/i18n/routing"
⦁(4) src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
✅ matcher를 통해 특정 경로에 대해서만 미들웨어가 실행되도록 설정합니다.
⦁(5) src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
// 이것은 일반적으로 '[locale]' 세그먼트에 해당합니다
let locale = await requestLocale;
// 유효한 locale이 사용되었는지 확인합니다
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
✅ locale을 설정하고 해당 locale에 맞는 메시지 파일을 불러옵니다.
⦁(6) src/app/[locale]/layout.tsx
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
export default async function RootLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
// 들어오는 'locale'이 유효한지 확인합니다.
if (!routing.locales.includes(locale as any)) {
notFound();
}
// 클라이언트에게 모든 메시지를 제공합니다.
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
✅ 프로젝트의 provider를 넣어주는 곳에 NextIntlClientProvider를 감싸줍니다. 보통 RootLayout 인가요?
❗️should be awaited before using itsproperties. 에러 발생 시
Error: Route "/[locale]" used `params.locale`. `params` should be awaited before using its properties.
코드를 이런 식으로 바꿔보세요.
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: Locale }>;
}) {
const locale = (await params).locale;
...
⦁(7) src/app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
import {Link} from '@/i18n/routing';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<Link href="/about">{t('about')}</Link>
</div>
);
}
✅ 이제 페이지에서 useTranslations 훅을 사용해서 번역 기능을 사용해보세요 !
+ 추가
⦁언어 선택 Dropdown 예제
"use client";
import React from "react";
import { usePathname, useRouter } from "@/i18n/routing";
import { useLocale } from "next-intl";
export default function LocaleDropdown() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const locale = e.target.value;
router.push({ pathname }, { locale });
};
return (
<select value={locale} onChange={handleChange}>
<option value="ko">한국어</option>
<option value="en">English</option>
<option value="jp">日本語</option>
</select>
);
}
참고 문헌 :
'Front-End > Next' 카테고리의 다른 글
[Next.js] 넥스트 빌드파일 html id="__next_error__" 해결방법 (useSearchParams) (0) | 2025.01.21 |
---|---|
[Next.js] TypeError: jsxDEV is not a function 해결 방법 (캐시 에러) (3) | 2024.10.07 |