
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원.
🎯 다국어의 이점
초반에 설정하길 잘했습니다.
- SEO:
/ja/products로 일본 검색엔진 최적화 - 신뢰도: 현지 언어는 고객 신뢰를 높입니다
- 확장성: 나중에 영어 추가도 쉽습니다
언어 추가는 JSON 파일 하나만 더 만들면 됩니다.
📋 이번 편 요약
| 항목 | 내용 |
|---|---|
| 라이브러리 | next-intl |
| 지원 언어 | 한국어(ko), 일본어(ja) |
| 폴더 구조 | app/[locale]/ 아래에 모든 페이지 |
| 서버 컴포넌트 | getTranslations() |
| 클라이언트 | useTranslations() |
| DB 다국어 | name_ko, name_ja 컬럼 분리 |
다음 편에서는 Supabase Auth로 로그인을 구현합니다.
💬 다국어 관련 질문 있으면 댓글로 남겨주세요!
'바이브코딩' 카테고리의 다른 글
| 10. 회원가입부터 로그아웃까지 - Supabase Auth 로그인 구현 (0) | 2026.01.17 |
|---|---|
| 16. 제품 선택부터 이메일 발송까지 - React Hook Form 문의 폼 구현 (0) | 2026.01.17 |
| 08. 타입 안전 폼 검증 - React Hook Form + Zod (0) | 2026.01.15 |
| 07. 빠른 스타일링 설정 Tailwind CSS + shadcn/ui (0) | 2026.01.14 |
| 06. Supabase CLI 활용법 -TypeScript 타입 자동 생성 (0) | 2026.01.13 |