# 설계서: i18n 뼈대 — ko/en/ja/es (#352) > **상태**: Approved > **작성**: [AI] Architect · **최종수정**: 2026-06-15 > **추적성** — Redmine: #352 > · 구현 파일: `frontend/package.json`, `frontend/next.config.ts`, `frontend/src/i18n/*` (신규), `frontend/src/messages/{ko,en,ja,es}.json` (신규), `frontend/src/components/LanguageSwitcher.tsx` (신규) > · 테스트: 수동 (언어 전환 + ko fallback) ## 1. 목적 장기적으로 영어/일본어/스페인어 시장으로 확장 가능하도록 i18n 뼈대 구축. 본 이슈는 *뼈대*만 — 약 30개 키 초기 번역. ## 2. 범위 - **포함** - `next-intl` 라이브러리 도입 (Next.js 16 App Router 권장). - 4개 로케일: `ko, en, ja, es`. 기본 `ko`, fallback `ko`. - 메시지 디렉토리 `src/messages/{ko,en,ja,es}.json`. - Provider (``) 루트 layout 적용. - `LanguageSwitcher` 컴포넌트 (헤더 우측). - 로케일 저장: localStorage `tasteby_locale`. - 초기 번역 키 ~30개: 헤더(로그인/검색/메뉴) + 주요 액션(저장/취소/삭제/확인) + 페이지 제목. - **제외** - URL 라우팅 i18n (`/ko/...`). - SEO meta tags i18n. - 식당명/리뷰 등 사용자 콘텐츠 번역. - 어드민 페이지(운영자만 사용, 한국어 유지). - 식당 카드/상세 시트 전체 키 추출 (점진). ## 3. 인수조건 - [ ] `npm i next-intl` + 4개 메시지 파일 생성. - [ ] `tasteby_locale`이 localStorage에 있으면 사용, 없으면 브라우저 언어 감지(`navigator.language`) → 매칭 안되면 `ko`. - [ ] 헤더 우측 LanguageSwitcher 드롭다운(국기 + 코드). - [ ] 초기 번역 키 약 30개 — 4개 언어 모두 채움. - [ ] 미번역 키는 `ko` fallback (에러 없이). - [ ] 빌드/배포 회귀 없음. ## 4. 컨텍스트 & 제약 - Next.js 16 + App Router + `"use client"` 컴포넌트 다수. - 기존 `auth-context`처럼 i18n도 React Context 패턴. - `next-intl`은 server/client 모두 지원, 본 프로젝트는 client-side switching이라 `NextIntlClientProvider` 중심. - URL 라우팅 변경 없이 단순 메시지만 교체(낮은 비용). - 폰트: Pretendard Variable이 한국어/영어 잘 표시, 일본어는 시스템 폰트 fallback OK, 스페인어는 라틴 문자라 OK. ## 5. 아키텍처 개요 ``` frontend/ ├── src/ │ ├── i18n/ │ │ ├── config.ts ← 로케일 목록/기본값 상수 │ │ ├── LocaleProvider.tsx ← NextIntlClientProvider wrap + localStorage 저장 │ │ └── useTranslations.ts ← (next-intl 재export) │ ├── messages/ │ │ ├── ko.json ← 기본 │ │ ├── en.json │ │ ├── ja.json │ │ └── es.json │ ├── components/ │ │ └── LanguageSwitcher.tsx ← 헤더용 │ └── app/ │ └── layout.tsx ← LocaleProvider로 감싸기 ``` ## 6. 데이터 모델 ### 메시지 키 (초기 ~30) ```json { "header": { "search": "검색", "login": "로그인", "logout": "로그아웃", "menu": "메뉴", "myReviews": "내 리뷰", "favorites": "즐겨찾기" }, "actions": { "save": "저장", "cancel": "취소", "delete": "삭제", "edit": "수정", "confirm": "확인", "close": "닫기", "loading": "로딩 중...", "submit": "제출" }, "filter": { "title": "필터", "cuisine": "음식 종류", "price": "가격대", "region": "지역", "channel": "채널", "reset": "초기화" }, "restaurant": { "rating": "평점", "address": "주소", "phone": "전화", "website": "웹사이트", "closed": "폐업", "tempClosed": "임시휴업" }, "review": { "title": "리뷰", "write": "리뷰 작성", "noReviews": "아직 리뷰가 없습니다" } } ``` 총 5개 카테고리 × 평균 6개 = 30개 키. ## 7. 함수 명세 | 함수/컴포넌트 | 책임 | 비고 | |---|---|---| | `i18n/config.ts` | LOCALES, DEFAULT_LOCALE 상수 | 단순 | | `LocaleProvider.tsx` | NextIntlClientProvider wrap + 메시지 동적 로딩 + localStorage 동기화 | client | | `LanguageSwitcher.tsx` | 헤더 드롭다운 (국기 + 코드) | client, 44px 터치 | | `messages/.json` | 키 → 텍스트 | flat or nested | ## 8. 흐름 1. 사용자 첫 방문 → `tasteby_locale` 없음 → `navigator.language.split('-')[0]`이 LOCALES에 있으면 사용, 아니면 `ko`. 2. LocaleProvider가 해당 로케일 메시지 파일 import → NextIntlClientProvider에 전달. 3. 컴포넌트는 `useTranslations('header')` 등으로 호출. 4. LanguageSwitcher에서 변경 → localStorage 저장 → 페이지 새로고침 또는 state 업데이트. ## 9. 엣지케이스 - **메시지 파일 누락 키**: next-intl 기본 동작은 키 자체 표시 + 콘솔 경고. fallback 처리는 messages 명시. - **localStorage 비활성/SSR**: typeof window 체크. - **로케일 코드 대소문자**: 항상 소문자 정규화. - **placeholder 변수**: next-intl ICU 메시지 형식 지원 (`{name}` 등). 초기 키에는 미적용. ## 10. 테스트 - 수동: - 한국어 첫 방문 → "검색" 표시. - LanguageSwitcher에서 English → "Search" 표시. - 새로고침 후 영어 유지. - 메시지 누락 키 → 콘솔 경고 + 키 표시. ## 11. 리스크 & 대안 - **선택**: `next-intl` (Next.js 16 App Router 권장, ICU 메시지, 활발 유지보수). - **대안 A**: `react-i18next` — 더 일반적이지만 App Router 통합 next-intl이 더 매끄러움. - **대안 B**: 자체 구현 + Context — 의존성 ↓ but 기능/표준화 ↓. - **트레이드오프**: 30개 키는 단순하지만 ICU 메시지(복수형, 성별 등) 필요 시 next-intl 가치 큼. ## 12. 미해결 질문 - 식당명/리뷰 콘텐츠 번역 — 사용자 작성이라 자동 번역(LLM)? 별도 정책 결정. - URL 라우팅 i18n (`/en/`) — 후속. - SEO meta tags i18n — 후속. - 어드민 페이지는 운영자 한국어 유지 — 확정. - 통화/날짜 포맷(Intl API) — 후속.