본문 바로가기

바이브코딩

09. 한국어/일본어 지원-next-intl 다국어 설정

next-intl 다국어 설정 - 09편. 한국어/일본어 지원

next-intl로 한국어/일본어 다국어 지원을 구현합니다.
이 글에서는 메시지 파일 구조, 서버/클라이언트 사용법, 언어 전환, DB 다국어 처리를 다룹니다.


🌐 일본 고객도 있었다

처음엔 한국어만 생각했습니다.

그런데 타겟 고객 중 일본 업체도 있었습니다.

"나중에 하지 뭐" 하고 넘어가려 했는데, Claude가 이렇게 말했습니다.

다국어 지원은 프로젝트 초기에 설정하는 게 좋습니다.
나중에 추가하면 모든 텍스트를 찾아서 바꿔야 해요.
지금 설정해드릴까요?

맞는 말이었습니다.

모든 하드코딩된 텍스트를 나중에 찾아서 바꾸는 건 악몽입니다.

초반에 설정하기로 했습니다.


📦 next-intl 설치

npm install next-intl

Next.js와 가장 잘 맞는 i18n 라이브러리라고 했습니다.


📁 메시지 파일 생성

번역할 텍스트를 JSON 파일로 관리합니다.

messages/
├── ko.json    ← 한국어
└── ja.json    ← 일본어

ko.json

{
  "common": {
    "siteName": "케이메타로",
    "login": "로그인",
    "logout": "로그아웃",
    "signup": "회원가입"
  },
  "home": {
    "title": "B2B 견적 플랫폼",
    "subtitle": "빠르고 정확한 견적 서비스",
    "cta": "문의하기"
  },
  "inquiry": {
    "title": "문의하기",
    "form": {
      "name": "이름",
      "email": "이메일",
      "phone": "전화번호",
      "content": "문의 내용",
      "submit": "문의 접수"
    },
    "success": "문의가 접수되었습니다"
  }
}

ja.json

{
  "common": {
    "siteName": "Kmetaro",
    "login": "ログイン",
    "logout": "ログアウト",
    "signup": "会員登録"
  },
  "home": {
    "title": "B2B見積もりプラットフォーム",
    "subtitle": "迅速で正確な見積もりサービス",
    "cta": "お問い合わせ"
  },
  "inquiry": {
    "title": "お問い合わせ",
    "form": {
      "name": "お名前",
      "email": "メールアドレス",
      "phone": "電話番号",
      "content": "お問い合わせ内容",
      "submit": "送信"
    },
    "success": "お問い合わせを受け付けました"
  }
}

구조가 동일해야 합니다. 키는 같고 값만 다르게.


⚙️ 설정 파일들

Claude에게 설정 파일들을 요청했습니다.

설정 파일

// i18n/config.ts
export const locales = ['ko', 'ja'] as const
export const defaultLocale = 'ko' as const

export type Locale = (typeof locales)[number]

request 설정

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { locales, defaultLocale } from './config'

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale

  if (!locale || !locales.includes(locale as any)) {
    locale = defaultLocale
  }

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default
  }
})

미들웨어

// middleware.ts
import createMiddleware from 'next-intl/middleware'
import { locales, defaultLocale } from './i18n/config'

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'as-needed'
})

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)']
}

localePrefix: 'as-needed'는 기본 언어(한국어)일 때 URL에 /ko를 안 붙입니다.

/products는 한국어, /ja/products는 일본어.


📂 폴더 구조 변경

다국어를 적용하려면 폴더 구조를 바꿔야 합니다.

app/
├── [locale]/           ← 모든 페이지를 여기로!
│   ├── layout.tsx
│   ├── page.tsx        → /ko 또는 /ja
│   ├── products/
│   │   └── page.tsx    → /ko/products 또는 /ja/products
│   └── ...

[locale] 동적 폴더 안에 모든 페이지가 들어갑니다.

기존 페이지들을 전부 옮겼습니다.


📝 실제 사용법

서버 컴포넌트에서

// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server'

export default async function HomePage() {
  const t = await getTranslations('home')

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
      <button>{t('cta')}</button>
    </div>
  )
}

t('title')이 언어에 따라 "B2B 견적 플랫폼" 또는 "B2B見積もりプラットフォーム"을 반환합니다.

클라이언트 컴포넌트에서

'use client'

import { useTranslations } from 'next-intl'

export function InquiryForm() {
  const t = useTranslations('inquiry.form')

  return (
    <form>
      <input placeholder={t('name')} />
      <input placeholder={t('email')} />
      <textarea placeholder={t('content')} />
      <button>{t('submit')}</button>
    </form>
  )
}

서버에서는 getTranslations, 클라이언트에서는 useTranslations.

변수 사용

{
  "greeting": "{name}님, 안녕하세요!",
  "items": "{count}개의 상품"
}
t('greeting', { name: '홍길동' })  // "홍길동님, 안녕하세요!"
t('items', { count: 5 })           // "5개의 상품"

동적 값도 넣을 수 있습니다.


🔄 언어 전환 컴포넌트

헤더에 언어 전환 버튼을 만들었습니다.

'use client'

import { useLocale } from 'next-intl'
import { useRouter, usePathname } from 'next/navigation'

export function LanguageSwitcher() {
  const locale = useLocale()
  const router = useRouter()
  const pathname = usePathname()

  const switchLocale = (newLocale: string) => {
    const newPath = pathname.replace(`/${locale}`, `/${newLocale}`)
    router.push(newPath)
  }

  return (
    <div className="flex gap-2">
      <button
        onClick={() => switchLocale('ko')}
        className={locale === 'ko' ? 'font-bold' : ''}
      >
        한국어
      </button>
      <button
        onClick={() => switchLocale('ja')}
        className={locale === 'ja' ? 'font-bold' : ''}
      >
        日本語
      </button>
    </div>
  )
}

현재 언어는 굵게, 클릭하면 언어가 바뀝니다.


📊 DB 데이터도 다국어로

UI 텍스트는 JSON 파일로 해결됐습니다.

그런데 제품명, 제품 설명 같은 DB 데이터는?

테이블에 다국어 컬럼을 만들었습니다.

create table products (
  id uuid primary key,
  name_ko text not null,
  name_ja text,
  description_ko text,
  description_ja text,
  -- ...
);

언어에 따라 데이터 표시

import { getLocale } from 'next-intl/server'

async function ProductCard({ product }: { product: Product }) {
  const locale = await getLocale()

  const name = locale === 'ja' ? product.name_ja : product.name_ko
  const description = locale === 'ja'
    ? product.description_ja
    : product.description_ko

  return (
    <div>
      <h2>{name || product.name_ko}</h2>
      <p>{description || product.description_ko}</p>
    </div>
  )
}

|| product.name_ko는 일본어 데이터가 없을 때 한국어로 폴백합니다.


⚠️ 삽질했던 것들

404 에러

페이지가 안 뜨고 404가 났습니다.

[locale] 폴더 구조가 잘못됐거나, 미들웨어 설정이 안 됐거나.

모든 페이지가 app/[locale]/ 아래에 있는지 확인했습니다.

번역이 안 나옴

메시지 키가 틀렸습니다.

// 디버깅
console.log(t.raw('home'))

이걸로 해당 네임스페이스의 전체 내용을 확인했습니다.


💡 번역 작업

번역도 Claude에게 맡겼습니다.

다음 한국어를 일본어로 번역해줘.
자연스러운 비즈니스 일본어로.

{
  "title": "견적서",
  "totalAmount": "합계 금액",
  "validUntil": "유효 기간",
  "acceptButton": "견적 수락"
}

Claude가 바로 JSON 형태로 번역해줬습니다.

{
  "title": "見積書",
  "totalAmount": "合計金額",
  "validUntil": "有効期限",
  "acceptButton": "見積承認"
}

번역 비용 0원.


🎯 다국어의 이점

초반에 설정하길 잘했습니다.

  1. SEO: /ja/products로 일본 검색엔진 최적화
  2. 신뢰도: 현지 언어는 고객 신뢰를 높입니다
  3. 확장성: 나중에 영어 추가도 쉽습니다

언어 추가는 JSON 파일 하나만 더 만들면 됩니다.


📋 이번 편 요약

항목 내용
라이브러리 next-intl
지원 언어 한국어(ko), 일본어(ja)
폴더 구조 app/[locale]/ 아래에 모든 페이지
서버 컴포넌트 getTranslations()
클라이언트 useTranslations()
DB 다국어 name_ko, name_ja 컬럼 분리

다음 편에서는 Supabase Auth로 로그인을 구현합니다.

💬 다국어 관련 질문 있으면 댓글로 남겨주세요!