Next.js App Routerμμ next-intlμ μ¬μ©ν΄ λ€κ΅μ΄λ₯Ό μ§μνλ λ°©λ²μ λν΄ μμλ³΄κ² μ΅λλ€.
λ¨Όμ , Next.js 곡μλ¬Έμ Routing > Internationalization νμ΄μ§λ₯Ό μ΄ν΄λ³΄λ©΄, Accept-Language ν€λμ λ―Έλ€μ¨μ΄, json νμΌ λ±μ μ¬μ©νμ¬ λ€κ΅μ΄λ₯Ό μ§μνλ λ°©λ²μ μκ°νκ³ μμ΅λλ€.
νμ§λ§ μ λ λ νΈλ¦¬ν κ°λ°κ³Ό μμ μ μΈ μλΉμ€λ₯Ό μν΄ μΈλΆ λΌμ΄λΈλ¬λ¦¬λ₯Ό λμ
νκΈ°λ‘ κ²°μ νκ³ , μλμ κ°μ μ΄μ λ‘ next-intlμ μ¬μ©νκ² λμμ΅λλ€.
next-intlμ μ νν μ΄μ κ·Έλ¬λ©΄ 본격μ μΌλ‘ next-intlμ μ¬μ©νλ λ°©λ²μ μκ°νκ² μ΅λλ€.
Tipπ‘ 곡μλ¬Έμμ λ°©λ²μ΄ μ λμ μμ΄μ λ¬Έμλ₯Ό 보면μ λ§λ€μ΄ 보μκ³ , μ΄ν΄κ° μ λκ±°λ μλ¬κ° λ°μνλ κ²½μ°μλ§ μ κΈμ μ°Έκ³ νμλ©΄ μ’μ κ² κ°μ΅λλ€.
λ¨Όμ , νμΌ κ΅¬μ‘°λ μλμ κ°μ΅λλ€.
npm i -D next-intl 컀맨λλ‘ ν¨ν€μ§λ₯Ό μ€μΉνμκ³ , μλμ νμΌκ³Ό ν΄λλ₯Ό λ§λ€μ΄ μ£ΌμΈμ.
βββ messages (1)
β βββ en.json
β βββ ...
βββ next.config.mjs (2)
βββ src
βββ i18n.ts (3)
βββ middleware.ts (4)
βββ app
βββ [locale]
βββ layout.tsx (5)
βββ page.tsx (6)
root/messages ν΄λ μμ μ§μνμ€ μΈμ΄λ§λ€ json νμΌμ λ§λ€μ΄ μ£ΌμΈμ.
λμ€μ μ μλνλμ§ νμΈν μ μλλ‘ κ°λ¨ν ν
μ€νΈ λ΄μ©μ μμ±νμλ κ²λ μ’μ΅λλ€.
μ λ μ΄λ κ² μμ±ν΄ 보μμ΅λλ€.
// messages/en.json
{
"Home": {
"title": "Hi! I'm Hailey Kim.",
"subtitle": "I'm a front-end developer."
}
}
// messages/ko.json
{
"Home": {
"title": "μλ
νμΈμ!",
"subtitle": "νλ‘ νΈμλ κ°λ°μ κΉνμμ
λλ€."
}
}
μλ μλ next.config.mjs νμΌμ μλμ μ½λλ₯Ό μμ±ν΄ μ£ΌμΈμ. (next.config.js νμΌμ μ¬μ©νμ λ€λ©΄ 곡μλ¬Έμλ₯Ό μ°Έκ³ ν΄ μ£ΌμΈμ.)
// next.config.mjs
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
src ν΄λ μμ λ§λ i18n.ts νμΌμ μλμ μ½λλ₯Ό μμ±ν΄ μ£ΌμΈμ.
// src/i18n.ts
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
// μ΄ λΆλΆμ λ€λ₯Έ νμΌμμ μν¬νΈν΄ μ¬ μ μμ΅λλ€.
const locales = ['en', 'de'];
export default getRequestConfig(async ({locale}) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default
};
});
localesλ₯Ό configλ‘ κ΄λ¦¬νκΈ° (μ ν μ¬ν)μ λ μμ localesλ₯Ό config νμΌμ λ°λ‘ λ΄μμ κ΄λ¦¬νκΈ°λ‘ κ²°μ νμ΅λλ€.
κ·Έλμ μλμ κ°μ΄ intl.config.ts(νμΌλͺ
μ μκ΄μμ΅λλ€)λ₯Ό rootμ μμ±ν΄μ localesλ₯Ό export νκ³ , i18n.tsμμ import νλλ‘ μ½λλ₯Ό μμ±νμ΅λλ€.
// intl.config.ts
export const locales: string[] = ['en', 'ko'];
// src/i18n.ts
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
import { locales } from '../intl.config';
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
src ν΄λ μμ λ§λ middleware.ts νμΌμ μλμ μ½λλ₯Ό μμ±ν΄ μ£ΌμΈμ.
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'ko'],
defaultLocale: 'en'
});
export const config = {
matcher: ['/', '/(en|ko)/:path*']
};
localesλ₯Ό λ€λ₯Έ νμΌμμ λΆλ¬μ€λ κ²½μ°// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales } from '../intl.config';
export default createMiddleware({
locales,
defaultLocale: locales[0],
});
export const config = {
matcher: ['/', '/(en|ko)/:path*'],
};
config.matcher λΆλΆλ '/(en|ko)/:path*'λ₯Ό μ§μ μ°λ κ² μλλΌ localesλ₯Ό μ΄μ©ν΄ λ³΄λ €κ³ νμ§λ§, array.mapμ μ¬μ©νκ±°λ μΈλΆμμ matcherλ₯Ό λ§λ€μ΄ μ¬μ©νλ λ±μ λ°©λ²μ λͺ¨λ 404 μλ¬κ° λ°μν΄μ en|koλ₯Ό μ§μ μμ±ν μλ°μ μμμ΅λλ€.
λ§μ½ μ΄κ² μ λ§ μ«λ€λ©΄, Next.js 곡μλ¬Έμμμ μ¬μ©ν κ²κ³Ό κ°μ΄ matcher: ['/((?!_next).*)']λ₯Ό μ°λ λ°©λ²λ μμ΅λλ€. (μ€μ λ‘ ν΄λ΄€κ³ , μλ¬ μμ΄ μ μλν©λλ€.)
src/app μμ [locale] ν΄λλ₯Ό μμ±νκ³ , ν΄λ μμ layout.tsxμ page.tsxλ₯Ό μμ±ν΄ μ£ΌμΈμ. μλμ μ½λλ₯Ό μμ±ν΄ μ£ΌμΈμ.
// app/[locale]/layout.tsx
export default function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}
// app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
μ λ μλμ μ½λλ₯Ό μμ±ν΄ static renderingμ΄ λκ³ (λΉλ νμμ 미리 μμ± κ°λ₯), μ ν΄μ§ locale κ°μΌλ‘λ§ /[locale]μ μ κ·Όν μ μλλ‘ μ€μ νμ΅λλ€.
// app/[locale]/layout.tsx
export const dynamicParams = false;
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
μ λλμ§ μνν΄ λ³΄κΈ° μν΄ page.tsxλ₯Ό λ€μκ³Ό κ°μ΄ μμ±ν΄ λ΄€μ΅λλ€.
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function Home() {
const t = useTranslations('Home');
return (
<>
<h1 className="mt-20 w-72 mx-auto">{t('title')}</h1>
<h2 className="w-72 mx-auto">{t('subtitle')}</h2>
</>
);
}
μλμ μμ° μμμ 보μλ©΄, / μ£Όμλ‘ μ κ·Όνμ λ λ°λ‘ μ¬μ©μμ λΈλΌμ°μ μ€μ μΈμ΄λ‘ (/en) redirect λκ³ , κ·Έμ λ§κ² ν
μ€νΈλ μ λ λλ§ λλ κ²μ λ³Ό μ μμ΅λλ€. /koλ‘ μ κ·Όνλ©΄ νκ΅μ΄λ‘ μ λ³νλκ³ , μ΄ μνμμ λ€μ /λ‘ λ€μ΄κ°λ©΄ λ§μ§λ§μΌλ‘ μ€μ νλ μΈμ΄λ₯Ό κΈ°μ΅ν΄μ /koλ‘ redirect λλ κ²μ λ³Ό μ μμ΅λλ€.

μμ λ΄μ©λ§ λ°λΌνμ
λ useTranslationsλ₯Ό μ¬μ©ν΄ μ΄λμλ json νμΌμ μμ± λ ν
μ€νΈλ₯Ό μΈμ΄μ λ§κ² λΆλ¬μ¬ μ μμ΅λλ€.
νμ§λ§ λ λ€μνκ² μ¬μ©νκ³ μΆμΌμλ€λ©΄ μλ λ΄μ©μ μ°Έκ³ ν΄ μ£ΌμκΈ° λ°λλλ€.
λ©νλ°μ΄ν°λ μΈμ΄μ λ°λΌ λ€λ₯΄κ² μμ±ν μ μμ΅λλ€.
π Next.js Metadataμ λ€κ΅μ΄ μ§μνκΈ° (next-intl)
λ¨μν λ©μΈμ§λ₯Ό κ·Έλλ‘ λΆλ¬μ€λ t('title') μΈμλ λ€μν μ¬μ© λ°©λ²μ΄ μμ΅λλ€.
π next-intlμ ICU messages μ¬μ©νκΈ°