Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 i18n 구현하기: Next.js 다국어 실전 가이드

Claude Code로 Next.js i18n을 구현하는 방법을 next-intl 설정, 번역 검증, 실전 사례와 함정까지 정리했습니다.

Claude Code로 i18n 구현하기: Next.js 다국어 실전 가이드

i18n은 번역 파일 추가가 아니라 구조 설계다

i18n은 internationalization, 즉 국제화를 뜻합니다. 실무에서는 단순히 JSON 파일을 여러 언어로 만드는 일이 아닙니다. URL 구조, locale 판정, SEO metadata, 날짜와 통화 형식, 복수형, 내부 링크, 번역 누락 검증까지 함께 설계해야 공개 가능한 품질이 됩니다. Claude Code는 여러 파일을 읽고 수정하며 명령까지 실행할 수 있어서 잘 맞지만, 목표가 흐릿하면 그럴듯한 번역 파일만 늘어나고 운영은 어려워집니다.

Masa가 콘텐츠 사이트를 운영하며 효과를 본 방식은 Claude Code에 “번역해줘”라고 말하지 않는 것입니다. 대신 “라우팅 기반, 메시지 파일, 페이지 이전, 검사 스크립트, 리뷰 관점까지 한 번에 만들어줘”라고 요청합니다. 기반이란 다국어 기능을 지탱하는 발판입니다. 이 발판이 안정적이면 영어, 독일어, 인도네시아어를 나중에 추가해도 변경 범위가 작고 리뷰가 쉬워집니다.

이 글은 Next.js App Router와 next-intl을 기준으로 설명합니다. 리뷰할 때는 Claude Code 공식 문서, next-intl routing setup, Next.js internationalization 가이드, MDN의 Intl 문서를 함께 확인하세요. 특히 최신 Next.js에서는 proxy.ts와 이전 middleware.ts의 차이를 확인해야 합니다.

flowchart LR
  A[요구사항] --> B[라우팅]
  B --> C[번역 JSON]
  C --> D[페이지와 metadata]
  D --> E[누락 검사]
  E --> F[SEO와 공개 확인]

Next.js App Router와 next-intl 기본 구성

예시는 ja를 기본 언어로 두고 en, de를 추가합니다. Next.js 16 문서는 라우팅 가로채기 파일로 proxy.ts를 사용합니다. 오래된 프로젝트에는 같은 역할의 middleware.ts가 남아 있을 수 있으므로, Claude Code에 먼저 Next.js 버전을 확인하게 하세요.

src/
  app/
    [locale]/
      layout.tsx
      page.tsx
  i18n/
    navigation.ts
    request.ts
    routing.ts
  messages/
    ja.json
    en.json
    de.json
  proxy.ts

routing.ts에 지원 언어와 경로를 모읍니다. 문자열 치환으로 /ja/en을 곳곳에 넣으면 나중에 URL 정책이 바뀔 때 위험합니다.

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['ja', 'en', 'de'],
  defaultLocale: 'ja',
  pathnames: {
    '/': '/',
    '/pricing': {
      ja: '/pricing',
      en: '/pricing',
      de: '/preise',
    },
    '/docs': {
      ja: '/docs',
      en: '/docs',
      de: '/dokumentation',
    },
  },
});

export type Locale = (typeof routing.locales)[number];
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';

export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

요청 단위 설정은 request.ts에 둡니다. 지원하지 않는 locale이 들어오면 기본 언어로 되돌려, 존재하지 않는 JSON import를 막습니다.

// src/i18n/request.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};

번역 키를 실제 페이지에 연결하기

번역 파일은 화면 단위로 나누는 편이 좋습니다. common에는 내비게이션, 언어 선택, 공통 버튼만 넣고, 페이지 전용 문구는 HomePage, PricingPage, DocsPage처럼 분리합니다. 이렇게 해야 Claude Code가 새 문구를 추가할 때도 위치가 분명하고, 리뷰어가 문맥을 잃지 않습니다.

{
  "common": {
    "language": {
      "label": "표시 언어",
      "ja": "日本語",
      "en": "English",
      "de": "Deutsch"
    },
    "nav": {
      "docs": "문서",
      "pricing": "요금"
    }
  },
  "HomePage": {
    "title": "팀 지식을 여러 언어로 전달하기",
    "lead": "독자의 언어에 맞춰 {count}개의 글을 보여줍니다.",
    "cta": "도입 상담하기"
  }
}
// src/app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server';

type Props = {
  params: Promise<{ locale: string }>;
};

export default async function HomePage({ params }: Props) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations({ locale, namespace: 'HomePage' });

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('lead', { count: 42 })}</p>
      <a href={`/${locale}/pricing`}>{t('cta')}</a>
    </main>
  );
}

언어 전환은 현재 경로를 유지한 채 locale만 바꾸는 방식이 안전합니다. 단순 문자열 치환은 /enquiry 같은 경로를 망가뜨릴 수 있습니다.

CI에서 번역 누락을 막기

Claude Code가 메시지 파일을 만들면 사람은 어색한 표현은 잘 보지만 키 누락은 놓치기 쉽습니다. 아래 스크립트는 기준 언어와 다른 언어의 키를 비교합니다.

// scripts/check-translations.mjs
import { readdir, readFile } from 'node:fs/promises';

const messagesDir = new URL('../src/messages/', import.meta.url);
const baseLocale = 'ja';

function flattenKeys(value, prefix = '') {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    return [prefix];
  }

  return Object.entries(value).flatMap(([key, child]) => {
    const nextPrefix = prefix ? `${prefix}.${key}` : key;
    return flattenKeys(child, nextPrefix);
  });
}

async function readMessages(locale) {
  const file = new URL(`${locale}.json`, messagesDir);
  return JSON.parse(await readFile(file, 'utf8'));
}

const files = await readdir(messagesDir);
const locales = files.filter((file) => file.endsWith('.json')).map((file) => file.replace(/\.json$/, ''));
const baseKeys = new Set(flattenKeys(await readMessages(baseLocale)));
let hasError = false;

for (const locale of locales.filter((item) => item !== baseLocale)) {
  const targetKeys = new Set(flattenKeys(await readMessages(locale)));
  const missing = [...baseKeys].filter((key) => !targetKeys.has(key));
  const extra = [...targetKeys].filter((key) => !baseKeys.has(key));

  if (missing.length || extra.length) {
    hasError = true;
    console.error(`\n${locale}.json has translation key drift`);
    if (missing.length) console.error('Missing:', missing.join(', '));
    if (extra.length) console.error('Extra:', extra.join(', '));
  }
}

if (hasError) process.exit(1);
console.log(`Translation keys are aligned for ${locales.length} locales.`);

이 검사는 번역의 자연스러움까지 보장하지 않습니다. 하지만 버튼이 특정 언어에서 사라지는 문제는 CI에서 막을 수 있습니다. Claude Code에는 스크립트 추가 후 package.jsonlint:i18n과 CI 연결까지 요청하는 것이 좋습니다.

세 가지 실전 사용 사례

첫 번째는 SaaS 요금 페이지입니다. 가격, 세금, 결제 주기, 플랜 이름은 국가마다 표현이 달라집니다. 문자열을 이어 붙이지 말고 Intl.NumberFormat이나 next-intl formatter를 쓰게 해야 합니다.

두 번째는 문서 사이트입니다. middleware, proxy, locale, namespace 같은 용어는 모두 번역하지 않는 편이 더 검색 친화적입니다. 첫 등장에서는 “locale은 언어 코드”처럼 쉽게 설명하고, 이후에는 원어를 유지하는 방식이 읽기 좋습니다.

세 번째는 관리자 화면입니다. 여기서는 SEO보다 실수를 막는 문구가 중요합니다. “삭제”, “비활성화”, “초대 취소”는 짧게 번역하다가 의미가 바뀌면 위험합니다. 버튼, 확인 모달, toast, 감사 로그까지 같은 키 체계에 넣어야 합니다.

블로그나 미디어 사이트라면 본문뿐 아니라 title, description, canonical, alternate links, OGP 이미지도 확인해야 합니다. 이 규칙은 CLAUDE.md 베스트 프랙티스에 적어 두면 Claude Code가 반복 작업에서 덜 흔들립니다.

흔한 함정과 검증 결과

함정문제대응
URL 설계를 미룸공개 후 redirect가 복잡해짐prefix, subdomain, domain 방식을 먼저 결정
키 이름이 추상적title2 같은 키가 늘어남화면명과 의미로 이름 짓기
fallback에 의존누락이 운영에서 숨겨짐CI에서 누락을 실패로 처리
날짜와 통화를 직접 조합어순과 구분자가 깨짐Intl 또는 formatter 사용
metadata 미번역검색 결과가 기본 언어로 남음generateMetadata도 번역

검증용 메시지 파일에서 영어의 HomePage.cta를 삭제해 보니 위 스크립트가 누락 키를 정확히 보고했습니다. 다만 “도입 상담하기”가 모든 시장에서 자연스러운지는 판단하지 못합니다. 그래서 Claude Code와 CI는 기계적 누락을 잡고, 사람은 CTA, 가격, 법무 문구, 문화적 어색함을 검토하는 역할 분담이 현실적입니다. 다음 단계로는 Claude Code 리뷰 워크플로ClaudeCodeLab 상담을 함께 활용하세요.

#Claude Code #i18n #국제화 #다국어 #Next.js
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.