요즘의 모던하고 글로벌한 웹에는 국제화(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>
);
}
+ 추가
⦁타입스크립트 자동 완성 사용하기
// global.d.ts
import en from "./messages/en.json";
type Messages = typeof en;
declare global {
interface IntlMessages extends Messages {}
}
next-env.d.ts와 같은 위치에 global.d.ts를 추가해줍니다.
// tsconfig.json
{
"compilerOptions": {
...
},
"include": ["src/**/*", "next-env.d.ts", "global.d.ts"] // global.d.ts 추가
}
그리고 타입스크립트가 인식할 수 있도록 경로를 지정해줍니다.
✅ 이제 타입스크립트의 자동 완성을 사용할 수 있습니다!
+ 추가 (3/14)
⦁ 404 페이지
next-intl 구조에서는 layout과 page가 [locale] 경로 세그먼트의 하위로 배치됩니다.
따라서 Next.js에서 기본적으로 제공되는 not-found 페이지가 보이지 않는 현상이 발생합니다.
따라서 [locale]의 상위인 /app의 루트 폴더에 다음과 같은 파일을 추가해야합니다.
자세한 내용은 🔗공식문서를 참고하세요 !
// app/not-found.tsx
'use client';
import Error from 'next/error';
export default function NotFound() {
return (
<html lang="en">
<body>
<Error statusCode={404} />
</body>
</html>
);
}
// -------------------------
// 이런 식으로 커스텀 할 수도 있어요
// -------------------------
export default function GlobalNotFound() {
return (
<html lang="en">
<body style={{ textAlign: "center", padding: "50px" }}>
<h1>404 - Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
<Link href="/" style={{ textDecoration: "none", color: "blue" }}>
🔙 Go to Home
</Link>
</body>
</html>
);
}
⨯ not-found.tsx doesn't have a root layout. To fix this error, make sure every page has a root layout.
Next.js의 모든 페이지는 루트 레이아웃으로 감싸져 있어야합니다.
따라서 /app에서 not-fund 페이지를 사용하기 위해서는 layout 파일이 필요합니다.
// app/layout.tsx
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export default function GlobalLayout({ children }: Props) {
return children;
}
저는 [locale] 폴더에 layout 파일을 RootLayout으로 사용하고 있기 때문에 GlobalLayout으로 만들고
바로 children을 리턴하는 형식으로 만들었습니다.
layout 파일이 중첩되는 게 마음에 안 들긴 하는데 공식 문서의 깃허브 샘플 코드도 이런 식으로 구현이 되어있습니다.
더 좋은 방법이 있다면 알려주세요 🥲
* 폴더 구조 예시
app/
├── layout.tsx // global layout (루트 레이아웃)
├── not-found.tsx // not-found 페이지
├── [locale]/ // 다국어(locale) 폴더
│ ├── layout.tsx // [locale]의 root layout
│ └── page.tsx // [locale]에 대한 페이지
참고 문헌 :
'Front-End > Next' 카테고리의 다른 글
[Next.js] 구글 스프레드시트로 i18n 메시지 관리하기 (Apps Script json 추출) (0) | 2025.03.13 |
---|---|
[Next.js] 넥스트 빌드파일 html id="__next_error__" 해결방법 (useSearchParams) (0) | 2025.01.21 |
[Next.js] TypeError: jsxDEV is not a function 해결 방법 (캐시 에러) (3) | 2024.10.07 |