작가:
    생성:2025-11-01마지막 업데이트:2026-09-27

    2025년에 next-intl을 사용하여 Next.js 애플리케이션을 국제화하는 방법

    목차

    next-intl이란?

    next-intl은 Next.js App Router를 위해 특별히 설계된 인기 있는 국제화(i18n) 라이브러리입니다. 뛰어난 TypeScript 지원과 내장 최적화를 제공하여 다국어 Next.js 애플리케이션을 원활하게 구축할 수 있습니다.

    원하신다면 next-i18next 가이드를 참고하거나, 직접 Intlayer를 사용할 수도 있습니다.
    비교 내용은 next-i18next vs next-intl vs Intlayer에서 확인하세요.
    이러한 라이브러리가 어디에서 왔는지 이해하려면 JavaScript i18n의 역사를 읽어보세요.

    Next.js에서 next-intl에 대한 벤치마크 결과

    번역을 구현하기 전에 next-intl의 성능 특성을 이해하는 것이 중요합니다. i18n 벤치마크는 10개 페이지, 10개 로케일로 구성된 동일한 Next.js 애플리케이션을 여러 설정과 라이브러리로 평가하여 실제 번들 크기, 문자열 누출, 하이드레이션 오버헤드를 측정합니다.

    측정항목

    동적 JSON 로드

    런타임에 번역을 지연 로드합니다

    범위가 지정된 JSON (네임스페이싱)

    페이지별 번역 네임스페이스

    이 측정항목은 무엇인가요?

    국제화 라이브러리 번들의 총 gzip 압축 크기입니다. 여기에는 트리 쉐이킹 및 미니피케이션 후의 프로바이더 및 콘텐츠 검색 로직만 포함됩니다.

    왜 중요한가요?

    라이브러리 크기가 작으면 초기 JavaScript 페이로드가 줄어들어 클라이언트에서 다운로드 및 실행 시간이 빨라집니다.

    보기 형식

    Next.js에서 next-intl의 주요 수치 (gzip):

    설정라이브러리 크기페이지 JS 평균다른 로케일 누출다른 페이지 누출
    기본 (i18n 없음)-141.0 KB0.0%0.0%
    next-intl14.7 KB153.6 KB4.2%89.8%
    @intlayer/next-intl (compat)8.0 KB148.7 KB0.0%0.0%
    next-intlayer (네이티브 Intlayer)5.5 KB141.3 KB0.0%0.0%

    핵심 정리:

    • 전역 메시지 카탈로그를 피하세요: 모든 메시지를 루트 레이아웃에서 로드하는 표준 설정에서는 브라우저로 전송되는 번역 콘텐츠의 약 89.8%가 다른 페이지에 속합니다. 라우트마다 pick(messages, ['namespace'])를 사용하면 이 누출이 사라지지만 수동 유지보수가 필요합니다.
    • 런타임 무게: next-intl 런타임은 각 페이지에 약 14.7 KB (gzip)를 추가합니다. 기존 next-intl 앱의 경우 @intlayer/next-intl compat 어댑터가 동일한 훅(useTranslations, useFormatter 등)을 유지하면서 런타임 크기를 약 8.0 KB, 누출 0%로 줄입니다. 네이티브 next-intlayer는 크기를 5.5 KB까지 더 줄입니다.
    전체 데이터 보기: Next.js 벤치마크 보고서, 그리고 벤치마크 저장소.

    Next.js에서의 기능 비교

    Next.js App Router 프로젝트에 보통 필요한 기능을 기준으로 next-intl을 next-i18next 및 Intlayer와 비교합니다:

    기능next-intlayer (Intlayer)next-intlnext-i18next
    컴포넌트 가까이 있는 번역✅ 각 컴포넌트와 함께 배치된 콘텐츠❌ 중앙 집중식 JSON❌ 중앙 집중식 JSON
    TypeScript 통합✅ 자동 생성되는 엄격한 타입✅ 좋음, AppConfig 확장을 통해⚠️ 기본
    누락된 번역 감지✅ TypeScript 오류 및 빌드 시 경고⚠️ 런타임 폴백⚠️ 런타임 폴백
    리치 콘텐츠 (JSX, Markdown)✅ 직접 지원⚠️ t.rich를 통한 태그, Markdown 없음⚠️ <Trans>를 통한 태그
    AI 번역✅ 자체 제공자와 API 키 사용, 앱 컨텍스트 포함❌ 없음❌ 없음
    비주얼 에디터 / CMS✅ 로컬 비주얼 에디터 + 선택적 CMS❌ 외부 플랫폼을 통해❌ 외부 플랫폼을 통해
    현지화된 라우팅✅ 내장 (Next.js 및 Vite)✅ 내장 [locale] 세그먼트✅ 내장
    복수형 처리✅ 열거 기반✅ ICU✅ 접미사 기반 (_one, _other)
    포맷팅 (날짜, 숫자, 통화)✅ Intl 기반 포매터✅ useFormatter✅ Intl 기반
    콘텐츠 형식✅ .ts, .tsx, .js, .json, .md, .yaml✅ .json, .js, .ts⚠️ .json
    ICU MessageFormat✅ format: "icu"를 통해✅ 네이티브⚠️ i18next-icu를 통해
    SEO 헬퍼 (hreflang, sitemap)✅ 메타데이터, sitemap, robots.txt 헬퍼✅ 좋음✅ 좋음
    Server Components✅ 모든 Server Component에서 직접 접근⚠️ 컴포넌트마다 t 또는 await getTranslations() 전달⚠️ 컴포넌트 트리 아래로 t 전달
    컴포넌트 단위 tree-shaking✅ 빌드 시점 (Babel / SWC)⚠️ 수동, 라우트마다 pick() 사용⚠️ 수동, 라우트마다 네임스페이스 사용
    지연 로딩✅ 로케일별 및 딕셔너리별✅ 로케일별, 네임스페이스는 수동 관리✅ 로케일별, 네임스페이스는 수동 관리
    런타임 크기 (gzip, 벤치마크)4.9 KB14.7 KB19.7 KB
    CI에서 누락된 번역✅ npx intlayer test⚠️ 내장되지 않음⚠️ 내장되지 않음, 런타임에 saveMissing
    생태계 / 커뮤니티⚠️ 작지만 빠르게 성장 중✅ 큼✅ 매우 큼
    런타임 크기는 Next.js 벤치마크에서 가져왔습니다. 자세한 내용은 next-i18next vs next-intl vs Intlayer를 참고하세요.

    따라야 할 모범 사례

    구현에 들어가기 전에, 다음과 같은 모범 사례를 따라야 합니다:

    • HTML lang 및 dir 속성 설정 레이아웃에서 getLocaleDirection(locale)를 사용하여 dir을 계산하고, 올바른 접근성과 SEO를 위해 <html lang={locale} dir={dir}>를 설정하세요.
    • 네임스페이스별 메시지 분리 로케일과 네임스페이스별로 JSON 파일을 구성하여 (예: common.json, about.json) 필요한 것만 로드하도록 하세요.
    • 클라이언트 페이로드 최소화 페이지에서 NextIntlClientProvider에 필요한 네임스페이스만 전송하세요 (예: pick(messages, ['common', 'about'])).
    • 정적 페이지 선호 성능과 SEO 향상을 위해 가능한 한 정적 페이지를 사용하세요.
    • 서버 컴포넌트에서의 i18n
      서버 컴포넌트는 페이지나 client로 표시되지 않은 모든 컴포넌트처럼 정적이며 빌드 시 미리 렌더링할 수 있습니다. 따라서 번역 함수를 props로 전달해야 합니다.
    • TypeScript 타입 설정
      애플리케이션 전반에 걸쳐 타입 안전성을 보장하기 위해 로케일에 대한 타입을 설정하세요.
    • 리디렉션을 위한 프록시
      로케일 감지와 라우팅을 처리하고 사용자를 적절한 로케일 접두사가 붙은 URL로 리디렉션하기 위해 프록시를 사용하세요.
    • 메타데이터, 사이트맵, robots.txt의 국제화
      Next.js에서 제공하는 generateMetadata 함수를 사용하여 메타데이터, 사이트맵, robots.txt를 국제화하여 모든 로케일에서 검색 엔진이 더 잘 인식하도록 하세요.
    • 링크 현지화
      Link 컴포넌트를 사용하여 링크를 현지화하고 사용자를 적절한 로케일 접두사가 붙은 URL로 리디렉션하세요. 이는 모든 로케일에서 페이지의 검색 가능성을 보장하는 데 중요합니다.
    • 테스트 및 번역 자동화 테스트와 번역 자동화는 다국어 애플리케이션을 유지 관리하는 데 소요되는 시간을 줄이는 데 도움이 됩니다.
    국제화 및 SEO에 대해 알아야 할 모든 내용을 정리한 문서를 참고하세요: next-intl과 함께하는 국제화(i18n).

    Next.js 애플리케이션에서 next-intl 설정 단계별 가이드

    ide.intlayer.org
    GitHub에서 애플리케이션 템플릿을 참조하세요.

    다음은 우리가 생성할 프로젝트 구조입니다:

    bash
    .
    ├── global.ts
    ├── locales
    │   ├── en
    │   │  ├── common.json
    │   │  └── about.json
    │   ├── fr
    │   │  ├── common.json
    │   │  └── about.json
    │   └── es
    │      ├── common.json
    │      └── about.json
    └── src # Src는 선택 사항입니다
        ├── proxy.ts
        ├── app
        │   ├── i18n.ts
        │   └── [locale]
        │       ├── layout.tsx
        │       ├── (home) # / (홈 리소스로 모든 페이지를 오염시키지 않기 위한 라우트 그룹)
        │       │   ├── layout.tsx
        │       │   └── page.tsx
        │       └── about # /about
        │           ├── layout.tsx
        │           └── page.tsx
        └── components
            ├── ClientComponent.tsx
            └── ServerComponent.tsx
    
    1. 의존성 설치

      npm을 사용하여 필요한 패키지를 설치하세요:

      bash
      npm install next-intl
      
      • next-intl: Next.js App Router용 핵심 국제화 라이브러리로, 번역 관리를 위한 훅, 서버 함수, 클라이언트 프로바이더를 제공합니다.
    2. 프로젝트 구성

      지원하는 로케일을 정의하고 next-intl의 요청 구성을 설정하는 구성 파일을 만드세요. 이 파일은 i18n 설정의 단일 진실 소스로 작동하며 애플리케이션 전반에 걸쳐 타입 안전성을 보장합니다.

      로케일 구성을 중앙 집중화하면 불일치를 방지하고 향후 로케일을 추가하거나 제거하기가 더 쉬워집니다. getRequestConfig 함수는 모든 요청 시 실행되며 각 페이지에 필요한 번역만 로드하여 코드 분할을 가능하게 하고 번들 크기를 줄입니다.

      src/i18n.ts
      import { notFound } from "next/navigation";
      import createMiddleware from "next-intl/middleware";
      import { createNavigation } from "next-intl/navigation";
      
      // 타입 안전성을 갖춘 지원 로케일 정의
      export const locales = ["en", "fr", "es"] as const;
      export type Locale = (typeof locales)[number];
      export const defaultLocale: Locale = "en";
      
      export function isRTL(locale: Locale | (string & {})) {
        return /^(ar|fa|he|iw|ur|ps|sd|ug|yi|ckb|ku)(-|$)/i.test(locale);
      }
      
      // 코드 분할을 가능하게 하기 위해 로케일별로 메시지를 동적으로 로드합니다
      // Promise.all은 더 나은 성능을 위해 네임스페이스를 병렬로 로드합니다
      async function loadMessages(locale: Locale) {
        // 레이아웃/페이지에서 필요한 네임스페이스만 로드합니다
        const [common, home, about] = await Promise.all([
          import(`../locales/${locale}/common.json`).then((m) => m.default),
          import(`../locales/${locale}/home.json`).then((m) => m.default),
          import(`../locales/${locale}/about.json`).then((m) => m.default),
          // ... 향후 JSON 파일은 여기에 추가해야 합니다
        ]);
      
        return { common, home, about } as const;
      }
      
      // 지역화된 URL을 생성하는 헬퍼 함수 (예: /about vs /fr/about)
      export function localizedPath(locale: string, path: string) {
        return locale === defaultLocale ? path : `/${locale}${path}`;
      }
      
      // getRequestConfig는 모든 요청 시 실행되며 서버 컴포넌트에 메시지를 제공합니다
      // next-intl이 Next.js의 서버 사이드 렌더링에 연결되는 부분입니다
      export default async function getRequestConfig({
        requestLocale,
      }: {
        requestLocale: Promise<string | undefined>;
      }) {
        const requested: Locale = ((await requestLocale) as Locale) ?? defaultLocale;
      
        if (!locales.includes(requested)) notFound();
      
        return {
          locale: requested,
          messages: await loadMessages(requested),
        };
      }
      
      export function getCookie(locale: Locale) {
        return [
          `NEXT_LOCALE=${locale}`,
          "Path=/",
          `Max-Age=${60 * 60 * 24 * 365}`, // 1년
          "SameSite=Lax",
        ].join("; ");
      }
      
      const routingOptions = {
        locales,
        defaultLocale,
        localePrefix: "as-needed", // /en/... 경로를 /...로 변경
        // 선택 사항: 지역화된 경로명
        // pathnames: {
        //   '/': '/',
        //   '/about': {en: '/about', fr: '/a-propos', es: '/acerca-de'},
        //   '/blog/[slug]': '/blog/[slug]'
        // }
        //  localeDetection: true, // 쿠키로 인한 "/" -> "/en" 리디렉션 방지
      } as const;
      
      export const { Link, redirect, usePathname, useRouter, getPathname } =
        createNavigation(routingOptions);
      
      export const proxy = createMiddleware(routingOptions);
      
    3. 동적 로케일 라우트 정의

      앱 폴더에 [locale] 디렉토리를 생성하여 로케일 기반 동적 라우팅을 설정하세요. 이를 통해 Next.js는 각 로케일이 URL 세그먼트가 되는 로케일 기반 라우팅을 처리할 수 있습니다 (예: /en/about, /fr/about).

      동적 라우트를 사용하면 Next.js가 빌드 시 모든 로케일에 대해 정적 페이지를 생성할 수 있어 성능과 SEO가 향상됩니다. 레이아웃 컴포넌트는 로케일에 따라 HTML의 lang 및 dir 속성을 설정하는데, 이는 접근성과 검색 엔진 이해에 매우 중요합니다.

      src/app/[locale]/layout.tsx
      import type { ReactNode } from "react";
      import { locales } from "@/i18n";
      import { getLocaleDirection, setRequestLocale } from "next-intl/server";
      
      // 빌드 시 모든 로케일에 대해 정적 페이지를 미리 생성 (SSG)
      // 이는 성능과 SEO를 향상시킵니다
      export function generateStaticParams() {
        return locales.map((locale) => ({ locale }));
      }
      
      export default function LocaleLayout({
        children,
        params,
      }: {
        children: ReactNode;
        params: Promise<{ locale: string }>;
      }) {
        // Next.js App Router에서 params는 Promise입니다 (await 가능)
        // 이를 통해 동적 라우트 세그먼트를 비동기적으로 해결할 수 있습니다
        const { locale } = await params;
      
        // 중요: setRequestLocale은 next-intl에 이 요청에 사용할 로케일을 알려줍니다
        // 이것이 없으면 getTranslations()가 서버 컴포넌트에서 사용할 로케일을 알 수 없습니다
        setRequestLocale(locale);
      
        // 올바른 HTML 렌더링을 위해 텍스트 방향(LTR/RTL)을 가져옵니다
        const dir = getLocaleDirection(locale);
      
        return (
          <html lang={locale} dir={dir}>
            <body>{children}</body>
          </html>
        );
      }
      }
      
      src/app/[locale]/about/page.tsx
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";
      import { NextIntlClientProvider } from "next-intl";
      import pick from "lodash/pick";
      import ServerComponent from "@/components/ServerComponent";
      import ClientComponent from "@/components/ClientComponent";
      
      export default async function AboutPage({
        params,
      }: {
        params: Promise<{ locale: string }>;
      }) {
        const { locale } = await params;
      
        // 메시지는 서버 측에서 로드됩니다. 클라이언트에는 필요한 것만 전달하세요.
        // 이렇게 하면 브라우저로 전송되는 자바스크립트 번들이 최소화됩니다.
        const messages = await getMessages();
        const clientMessages = pick(messages, ["common", "about"]);
      
        // 엄격히 서버 측에서만 사용하는 번역/포맷팅
        // 이들은 서버에서 실행되며 컴포넌트에 props로 전달될 수 있습니다
        const tAbout = await getTranslations("about");
        const tCounter = await getTranslations("about.counter");
        const format = await getFormatter();
      
        const initialFormattedCount = format.number(0);
      
        return (
          // NextIntlClientProvider는 클라이언트 컴포넌트에서 번역을 사용할 수 있게 합니다
          // 클라이언트 컴포넌트가 실제로 사용하는 네임스페이스만 전달하세요
          <NextIntlClientProvider locale={locale} messages={clientMessages}>
            <main>
              <h1>{tAbout("title")}</h1>
              <ClientComponent />
              <ServerComponent
                formattedCount={initialFormattedCount}
                label={tCounter("label")}
                increment={tCounter("increment")}
              />
            </main>
          </NextIntlClientProvider>
        );
      }
      
    4. 번역 파일 생성하기

      각 로케일과 네임스페이스별로 JSON 파일을 생성하세요. 이 구조는 번역을 논리적으로 구성하고 각 페이지에 필요한 번역만 로드할 수 있게 해줍니다.

      네임스페이스별로 번역을 구성하는 것(e.g., common.json, about.json)은 코드 분할을 가능하게 하며 번들 크기를 줄여줍니다. 이렇게 하면 각 페이지에 필요한 번역만 로드하여 성능을 향상시킬 수 있습니다.

      locales/en/common.json
      {
        "welcome": "Welcome",
        "greeting": "Hello, world!"
      }
      
      locales/fr/common.json
      {
        "welcome": "Bienvenue",
        "greeting": "Bonjour le monde!"
      }
      
      locales/en/about.json
      {
        "title": "About",
        "description": "About page description",
        "counter": {
          "label": "Counter",
          "increment": "Increment"
        }
      }
      
      locales/fr/about.json
      {
        "title": "À propos",
        "description": "Description de la page À propos",
        "counter": {
          "label": "Compteur",
          "increment": "Incrémenter"
        }
      }
      
    5. 페이지에서 번역 활용하기

      서버에서 번역을 로드하고 이를 서버 및 클라이언트 컴포넌트 모두에 전달하는 페이지 컴포넌트를 만드세요. 이렇게 하면 렌더링 전에 번역이 로드되어 콘텐츠 깜박임을 방지할 수 있습니다.

      서버 측에서 번역을 로드하면 SEO가 향상되고 FOUC(번역되지 않은 콘텐츠 깜박임)를 방지할 수 있습니다. pick을 사용하여 필요한 네임스페이스만 클라이언트 프로바이더에 전달함으로써 브라우저에 전송되는 자바스크립트 번들 크기를 최소화합니다.

      src/app/[locale]/about/page.tsx
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";
      import { NextIntlClientProvider } from "next-intl";
      import pick from "lodash/pick";
      import ServerComponent from "@/components/ServerComponent";
      import ClientComponent from "@/components/ClientComponent";
      
      export default async function AboutPage({
        params,
      }: {
        params: Promise<{ locale: string }>;
      }) {
        const { locale } = await params;
      
        // 메시지는 서버 측에서 로드됩니다. 클라이언트에는 필요한 것만 전달하세요.
        // 이렇게 하면 브라우저로 전송되는 JavaScript 번들이 최소화됩니다.
        const messages = await getMessages();
        const clientMessages = pick(messages, ["common", "about"]);
      
        // 엄격히 서버 측 번역/포맷팅
        // 이들은 서버에서 실행되며 컴포넌트에 props로 전달될 수 있습니다
        const tAbout = await getTranslations("about");
        const tCounter = await getTranslations("about.counter");
        const format = await getFormatter();
      
        const initialFormattedCount = format.number(0);
      
        return (
          // NextIntlClientProvider는 클라이언트 컴포넌트에서 번역을 사용할 수 있게 합니다.
          // 클라이언트 컴포넌트가 실제로 사용하는 네임스페이스만 전달하세요.
          <NextIntlClientProvider locale={locale} messages={clientMessages}>
            <main>
              <h1>{tAbout("title")}</h1>
              <ClientComponent />
              <ServerComponent
                formattedCount={initialFormattedCount}
                label={tCounter("label")}
                increment={tCounter("increment")}
              />
            </main>
          </NextIntlClientProvider>
        );
      }
      
    6. 클라이언트 컴포넌트에서 번역 사용하기

      클라이언트 컴포넌트는 useTranslations 및 useFormatter 훅을 사용하여 번역 및 포맷팅 기능에 접근할 수 있습니다. 이 훅들은 NextIntlClientProvider 컨텍스트에서 값을 읽어옵니다.

      클라이언트 컴포넌트는 번역에 접근하기 위해 React 훅이 필요합니다. useTranslations와 useFormatter 훅은 next-intl과 원활하게 통합되며, 로케일이 변경될 때 반응형 업데이트를 제공합니다.

      페이지의 클라이언트 메시지에 필요한 네임스페이스를 추가하는 것을 잊지 마세요 (클라이언트 컴포넌트가 실제로 필요로 하는 네임스페이스만 포함하세요).
      src/components/ClientComponent.tsx
      "use client";
      
      import React, { useState } from "react";
      import { useTranslations, useFormatter } from "next-intl";
      
      const ClientComponent = () => {
        // 중첩된 객체에 직접 범위를 지정
        // useTranslations/useFormatter는 NextIntlClientProvider 컨텍스트에서 읽는 훅입니다.
        // 이 훅들은 컴포넌트가 NextIntlClientProvider로 감싸져 있을 때만 작동합니다.
        const t = useTranslations("about.counter");
        const format = useFormatter();
        const [count, setCount] = useState(0);
      
        return (
          <div>
            <p>{format.number(count)}</p>
            <button
              aria-label={t("label")}
              onClick={() => setCount((count) => count + 1)}
            >
              {t("increment")}
            </button>
          </div>
        );
      };
      
    7. 서버 컴포넌트에서 번역 사용하기

      서버 컴포넌트는 React 훅을 사용할 수 없으므로, 부모 컴포넌트로부터 props를 통해 번역과 포매터를 전달받습니다. 이 방법은 서버 컴포넌트를 동기적으로 유지하며 클라이언트 컴포넌트 내부에 중첩될 수 있게 합니다.

      클라이언트 경계 내에 중첩될 수 있는 서버 컴포넌트는 동기적이어야 합니다. 번역된 문자열과 포맷된 값을 props로 전달함으로써 비동기 작업을 피하고 올바른 렌더링을 보장합니다. 부모 페이지 컴포넌트에서 번역과 포맷을 미리 계산하세요.

      src/components/ServerComponent.tsx
      // 클라이언트 컴포넌트 내에 중첩된 서버 컴포넌트는 동기적이어야 합니다
      // React는 서버/클라이언트 경계 간에 비동기 함수를 직렬화할 수 없습니다
      // 해결책: 부모에서 번역/포맷을 미리 계산하고 props로 전달
      type ServerComponentProps = {
        formattedCount: string; // 포맷된 카운트 문자열
        label: string; // 버튼 라벨
        increment: string; // 증가 텍스트
      };
      
      const ServerComponent = ({
        formattedCount,
        label,
        increment,
      }: ServerComponentProps) => {
        return (
          <div>
            <p>{formattedCount}</p>
            <button aria-label={label}>{increment}</button>
          </div>
        );
      };
      
      페이지나 레이아웃에서 next-intl/server의 getTranslations와 getFormatter를 사용하여 번역과 포맷팅을 미리 계산한 후, 이를 props로 서버 컴포넌트에 전달하세요.
    8. 콘텐츠의 언어 변경하기

      선택사항

      next-intl을 사용하여 콘텐츠의 언어를 변경하려면, 동일한 경로명을 가리키면서 로케일을 전환하는 로케일 인식 링크를 렌더링하세요. 프로바이더가 URL을 자동으로 재작성하므로 현재 경로만 지정하면 됩니다.

      src/components/LocaleSwitcher.tsx
      "use client";
      
      import Link from "next/link";
      import { usePathname } from "next/navigation";
      import { useLocale } from "next-intl";
      import { defaultLocale, getCookie, type Locale, locales } from "@/i18n";
      
      const getLocaleLabel = (locale: Locale): string => {
        try {
          const displayNames = new Intl.DisplayNames([locale], { type: "language" });
          return displayNames.of(locale) ?? locale.toUpperCase();
        } catch {
          return locale.toUpperCase();
        }
      };
      
      const localeFlags: Record<Locale, string> = {
        en: "🇬🇧",
        fr: "🇫🇷",
        es: "🇪🇸",
      };
      
      export default function LocaleSwitcher() {
        const activeLocale = useLocale();
        const pathname = usePathname();
      
        // 경로에서 로케일 접두사를 제거하여 기본 경로를 가져옵니다
        const getBasePath = (path: string) => {
          for (const locale of locales) {
            if (path.startsWith(`/${locale}`)) {
              return path.slice(locale.length + 1) || "/";
            }
          }
          return path;
        };
      
        const basePath = getBasePath(pathname);
      
        return (
          <nav aria-label="언어 선택기">
            <div>
              {(locales as readonly Locale[]).map((locale) => {
                const isActive = locale === activeLocale;
                // 기본 로케일인지 여부에 따라 href를 생성합니다
                const href =
                  locale === defaultLocale ? basePath : `/${locale}${basePath}`;
                return (
                  <Link
                    key={locale}
                    href={href}
                    aria-current={isActive ? "page" : undefined}
                    onClick={() => {
                      document.cookie = getCookie(locale);
                    }}
                  >
                    <span>{localeFlags[locale]}</span>
                    <span>{getLocaleLabel(locale)}</span>
                    <span>{locale.toUpperCase()}</span>
                  </Link>
                );
              })}
            </div>
          </nav>
        );
      }
      
    9. 선택사항

      next-intl은 활성 로케일을 자동으로 적용하는 현지화된 링크 컴포넌트를 포함하는 서브패키지 next-intl/navigation을 제공합니다. 우리는 이미 @/i18n 파일에서 이를 추출해 두었으므로 다음과 같이 사용할 수 있습니다:

      src/components/MyComponent.tsx
      import { Link } from "@/i18n";
      
      return <Link href="/about">t("about.title")</Link>;
      
    10. 서버 액션 내에서 활성 로케일 접근하기

      선택사항

      서버 액션은 next-intl/server를 사용하여 현재 로케일을 읽을 수 있습니다. 이는 현지화된 이메일을 보내거나 제출된 데이터와 함께 언어 선호도를 저장하는 데 유용합니다.

      src/app/actions/get-current-locale.ts
      "use server";
      
      import { getLocale } from "next-intl/server";
      
      export async function getCurrentLocale() {
        return getLocale();
      }
      
      export async function handleContactForm(formData: FormData) {
        const locale = await getCurrentLocale();
      
        // 템플릿, 분석 레이블 등을 선택하기 위해 locale을 사용합니다.
        console.log(`locale ${locale}에서 받은 연락처 양식`);
      }
      
      getLocale는 next-intl 프록시가 설정한 locale을 읽기 때문에 서버 어디서나 작동합니다: Route Handlers, Server Actions, 그리고 edge functions.
    11. 메타데이터 국제화하기

      선택사항

      콘텐츠 번역도 중요하지만, 국제화의 주요 목표는 웹사이트를 전 세계에 더 잘 보이게 만드는 것입니다. I18n은 적절한 SEO를 통해 웹사이트 가시성을 향상시키는 놀라운 수단입니다.

      적절하게 국제화된 메타데이터는 검색 엔진이 페이지에서 어떤 언어가 사용 가능한지 이해하는 데 도움을 줍니다. 여기에는 hreflang 메타 태그 설정, 제목과 설명 번역, 각 로케일에 대해 정규화된 URL이 올바르게 설정되었는지 확인하는 작업이 포함됩니다.

      src/app/[locale]/about/layout.tsx
      import type { Metadata } from "next";
      import { locales, defaultLocale, localizedPath } from "@/i18n";
      import { getTranslations } from "next-intl/server";
      
      // generateMetadata는 각 로케일마다 실행되어 SEO 친화적인 메타데이터를 생성합니다
      // 이것은 검색 엔진이 대체 언어 버전을 이해하는 데 도움을 줍니다.
      export async function generateMetadata({
        params,
      }: {
        params: { locale: string };
      }): Promise<Metadata> {
        const { locale } = params;
        const t = await getTranslations({ locale, namespace: "about" });
      
        const url = "/about";
        const languages = Object.fromEntries(
          locales.map((locale) => [locale, localizedPath(locale, url)])
        );
      
        return {
          title: t("title"),
          description: t("description"),
          alternates: {
            canonical: localizedPath(locale, url),
            languages: { ...languages, "x-default": url },
          },
        };
      }
      
      // ... 페이지 나머지 코드
      
    12. 사이트맵 국제화하기

      선택사항

      모든 로케일 버전의 페이지를 포함하는 사이트맵을 생성하세요. 이는 검색 엔진이 모든 언어 버전의 콘텐츠를 발견하고 색인화하는 데 도움이 됩니다.

      적절하게 국제화된 사이트맵은 검색 엔진이 모든 언어 버전의 페이지를 찾고 색인화할 수 있도록 보장합니다. 이는 국제 검색 결과에서 가시성을 향상시킵니다.

      src/app/sitemap.ts
      import type { MetadataRoute } from "next";
      import { defaultLocale, locales } from "@/i18n";
      
      const origin = "https://example.com";
      
      const formatterLocalizedPath = (locale: string, path: string) =>
        locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;
      
      /**
       * 모든 로케일과 해당 로컬라이즈된 경로의 맵을 가져옵니다.
       *
       * 예시 출력:
       * {
       *   "en": "https://example.com",
       *   "fr": "https://example.com/fr",
       *   "es": "https://example.com/es",
       *   "x-default": "https://example.com"
       * }
       */
      const getLocalizedMap = (path: string) =>
        Object.fromEntries([
          ...locales.map((locale) => [locale, formatterLocalizedPath(locale, path)]),
          ["x-default", formatterLocalizedPath(defaultLocale, path)],
        ]);
      
      // 더 나은 SEO를 위해 모든 로케일 변형을 포함하는 사이트맵 생성
      // alternates 필드는 검색 엔진에 언어 버전을 알립니다
      export default function sitemap(): MetadataRoute.Sitemap {
        return [
          {
            url: formatterLocalizedPath(defaultLocale, "/"),
            lastModified: new Date(),
            changeFrequency: "monthly",
            priority: 1.0,
            alternates: { languages: getLocalizedMap("/") },
          },
          {
            url: formatterLocalizedPath(defaultLocale, "/about"),
            lastModified: new Date(),
            changeFrequency: "monthly",
            priority: 0.7,
            alternates: { languages: getLocalizedMap("/about") },
          },
        ];
      }
      
    13. robots.txt 국제화하기

      선택사항

      보호된 경로의 모든 로케일 버전을 적절히 처리하는 robots.txt 파일을 만드세요. 이를 통해 검색 엔진이 어떤 언어로든 관리자(admin)나 대시보드 페이지를 인덱싱하지 않도록 할 수 있습니다.

      모든 로케일에 대해 robots.txt를 올바르게 구성하면, 각 로케일별로 경로가 다를 때 민감한 페이지가 검색 엔진에 인덱싱되는 것을 방지할 수 있습니다.

      src/app/robots.ts
      import type { MetadataRoute } from "next";
      import { locales, defaultLocale } from "@/i18n";
      
      const origin = "https://example.com";
      // 모든 로케일에 대한 경로 생성 (예: /admin, /fr/admin, /es/admin)
      const withAllLocales = (path: string) => [
        path,
        ...locales
          .filter((locale) => locale !== defaultLocale)
          .map((locale) => "/" + locale + path),
      ];
      
      export default function robots(): MetadataRoute.Robots {
        const disallow = [
          ...withAllLocales("/dashboard"),
          ...withAllLocales("/admin"),
        ];
      
        return {
          rules: { userAgent: "*", allow: ["/"], disallow },
          host: origin,
          sitemap: origin + "/sitemap.xml",
        };
      }
      
    14. 로케일 라우팅을 위한 프록시 설정

      선택사항

      사용자의 선호 로케일을 자동으로 감지하고 적절한 로케일 접두사가 붙은 URL로 리디렉션하는 프록시를 만드세요. next-intl은 이를 자동으로 처리하는 편리한 프록시 함수를 제공합니다.

      프록시는 사용자가 사이트를 방문할 때 자동으로 선호하는 언어로 리디렉션되도록 보장합니다. 또한 사용자의 선호도를 저장하여 향후 방문 시 사용자 경험을 향상시킵니다.

      src/proxy.ts
      import { proxy } from "@/i18n";
      
      // 미들웨어는 라우트 이전에 실행되어 로케일 감지 및 라우팅을 처리합니다.
      // localeDetection: true는 Accept-Language 헤더를 사용하여 로케일을 자동 감지합니다.
      export default proxy;
      
      export const config = {
        // API, Next 내부, 정적 자산은 건너뜁니다.
        // 정규식: api, _next로 시작하거나 점(.)이 포함된 경로(파일)를 제외한 모든 경로와 매칭됩니다.
        matcher: ["/((?!api|_next|.*\\..*).*)"],
      };
      
    15. 로케일에 대한 TypeScript 타입 설정

      선택사항

      TypeScript 설정은 키에 대한 자동완성과 타입 안전성을 제공하는 데 도움이 됩니다.

      프로젝트 루트에 global.ts 파일을 생성하고 다음 코드를 추가할 수 있습니다:

      global.ts
      import type { locales } from "@/i18n";
      
      type Messages = {
        common: typeof import("./locales/en/common.json");
        home: typeof import("./locales/en/home.json");
        about: typeof import("./locales/en/about.json");
        // ... 향후 JSON 파일도 여기에 추가해야 합니다
      };
      
      declare module "next-intl" {
        interface AppConfig {
          Locale: (typeof locales)[number];
          Messages: Messages;
        }
      }
      

      이 코드는 모듈 확장(Module Augmentation)을 사용하여 locales와 messages를 next-intl의 AppConfig 타입에 추가합니다.

    16. Intlayer를 사용하여 번역 자동화하기

      선택사항

      Intlayer는 애플리케이션의 현지화 과정을 지원하기 위해 설계된 무료이자 오픈 소스 라이브러리입니다. next-intl이 번역 로딩과 관리를 담당하는 반면, Intlayer는 번역 워크플로우를 자동화하는 데 도움을 줍니다.

      번역을 수동으로 관리하는 것은 시간 소모가 크고 오류가 발생하기 쉽습니다. Intlayer는 번역 테스트, 생성 및 관리를 자동화하여 시간을 절약하고 애플리케이션 전반에 걸쳐 일관성을 보장합니다.

      Intlayer는 다음을 가능하게 합니다:

      • 코드베이스 내 원하는 위치에 콘텐츠 선언하기
        Intlayer는 .content.{ts|js|json} 파일을 사용하여 코드베이스 내 원하는 위치에 콘텐츠를 선언할 수 있게 합니다. 이를 통해 콘텐츠를 더 잘 조직할 수 있으며, 코드베이스의 가독성과 유지보수성을 향상시킵니다.

      • 누락된 번역 테스트하기
        Intlayer는 CI/CD 파이프라인이나 단위 테스트에 통합할 수 있는 테스트 기능을 제공합니다. 번역 테스트에 대해 자세히 알아보세요.

      • 번역 자동화 Intlayer는 번역을 자동화할 수 있는 CLI와 VSCode 확장 기능을 제공합니다. 이는 CI/CD 파이프라인에 통합할 수 있습니다. 번역 자동화에 대해 자세히 알아보세요. 사용자는 자신의 API 키와 원하는 AI 제공자를 사용할 수 있습니다. 또한 상황에 맞는 번역을 제공하므로, 내용 채우기를 참고하세요.

      • 외부 콘텐츠 연결 Intlayer는 외부 콘텐츠 관리 시스템(CMS)에 콘텐츠를 연결할 수 있도록 합니다. 이를 최적화된 방식으로 가져와 JSON 리소스에 삽입할 수 있습니다. 외부 콘텐츠 가져오기에서 자세히 알아보세요.

      • 비주얼 에디터
        Intlayer는 비주얼 에디터를 사용하여 콘텐츠를 편집할 수 있는 무료 비주얼 에디터를 제공합니다. 번역 비주얼 편집에서 자세히 알아보세요.

      그리고 더 많은 기능들이 있습니다. Intlayer가 제공하는 모든 기능을 확인하려면 Intlayer의 관심사 문서를 참조하세요.

      자세한 성능 벤치마크와 비교는 다음을 참고하세요:

    댓글

    아직 댓글이 없습니다. 첫 번째로 의견을 나눠보세요.

    관련 게시물

    최근 게시물