다국어 번역, 제대로 관리하고 계신가요?
웹 애플리케이션을 운영하다 보면 화면마다 문구가 계속 추가되고 수정되죠. 이때 다국어 번역 파일을 일일이 관리하는 건 정말 번거롭습니다. 언어가 많아질수록 번역 키를 누락하거나 파일이 꼬이는 일도 생기고요.
그래서 저는 next-intl, i18next-parser 를 활용하여 자동화된 다국어 처리 구조를 만들었습니다. 이 글에서는 그 구조를 차근차근 소개해 드릴게요. 😊
1. 시스템 아키텍처: 전체 플로우 한눈에 보기
1-1. 번역 흐름 요약
- i18next-parser가 코드에서 번역 키를 자동으로 추출 → JSON 파일로 저장
- 번역 키 파일을 빌드 시점에 백엔드 API로 전송 → 자동 번역 수행 (DeepL)
- 어드민에서 수동으로 번역 수정 가능 (자동 번역 보완)
- 프론트엔드에서 번역본을 API로 받아오고 캐싱 → next-intl에 맞게 변환
- 번역본이 변경되면 캐시 무효화 요청으로 최신 번역을 반영
1-2. 시퀀스 다이어그램
2. 번역 키 관리: i18next-parser로 자동화하기
2-1. 번역 키 자동 추출하기
i18next-parser는 코드에서 번역 키를 자동으로 추출해서 네임스페이스별 JSON 파일로 만들어줍니다. 설정 파일 예시는 이렇게 되어 있어요:
export default {
keySeparator: ":",
defaultValue: (_, namespace, key) => `${namespace}:${key}`,
lexers: {
tsx: [
{ lexer: "JsxLexer", functions: ["t", "t.rich"], namespaceFunctions: ["useTranslations"] },
{ lexer: "JsxLexer", functions: ["commonT"], namespaceFunctions: ["useCommonTranslations"] }
]
},
output: "public/translations/$NAMESPACE.json",
input: ["apps/client/src/**/*.{js,jsx,ts,tsx}"],
};
2-2. 네임스페이스와 파일 구조
translations/
├── 000.공통.json
...
├── 006.게시판.json
...
- 숫자 prefix: 어드민에서 번역 키 그룹의 노출 순서를 제어
- 한글 네임스페이스: 어떤 기능에서 쓰이는 번역인지 쉽게 파악
2-3. 번역 키 규칙
{ordering}.{namespace}:{content}
- ordering: 어드민에서 주제 별 노출될 순서를 관리하기 위함
- namespace: 주제
- content: 내용
2-4. 한글 키값을 사용하게 된 배경
처음에 영어로 번역 키를 쓸지 고민했었어요.
근데 이런 대화가 오갔죠:
이렇게 해서 한글 키값을 네임스페이스와 함께 사용하기로 했어요.
이 덕분에 기획자, 디자이너, 번역가도 키값만 봐도 무슨 의미인지 쉽게 알 수 있답니다. 😊
또한, 이렇게 한글로 만든 키 값 덕분에 번역 본 수정 시 원문을 바로 알 수 있어 수정에 용이해요.
3. 번역 키를 백엔드로 보내는 과정
빌드 시점에 번역 키를 백엔드로 보내서 번역본을 관리할 수 있어요.
이 과정을 담당하는 스크립트가 바로 export-translation-file.js입니다!
3-1. 번역 파일 예시
000.공통.json
{
"검색": "000.공통:검색"
}
006.게시판.json
{
"게시글": "006.게시판:게시글",
"최신기사": "006.게시판:최신기사",
"제목을 입력해 주세요": "006.게시판:제목을 입력해 주세요",
"첨부파일 {num}개 ({capacity}KB)": "006.게시판:첨부파일 {num}개 ({capacity}KB)"
}
이런 식으로 번역 키를 추출해서 JSON 파일로 저장하고, 빌드 시에 이 파일들을 백엔드로 전송합니다.
어드민에서는 이 번역본을 불러와서 수동으로 번역을 수정할 수도 있어요.
자동 번역과 수동 수정이 자연스럽게 조화를 이루는 구조랍니다.
3-2. 스크립트 흐름 (export-translation-file.js)
const fs = require('fs');
const path = require('path');
const translationsDir = path.join(__dirname, '../../public/translations');
// 백엔드에 원하는 형태로 보내기 위한 전처리 과정
const getAllTranslationFiles = () => {
const files = fs.readdirSync(translationsDir);
const translations = files
.filter((file) => file.endsWith('.json'))
.reduce((acc, file) => {
const filePath = path.join(translationsDir, file);
const fileContents = fs.readFileSync(filePath, 'utf-8');
const parsedData = JSON.parse(fileContents);
const formattedTranslations = Object.entries(parsedData).map(
([_, value]) => ({
name: value,
}),
);
acc.push(...formattedTranslations);
return acc;
}, []);
return translations;
};
// 번역 데이터 전송
const translations = getAllTranslationFiles();
fetch(`<api-base-url>/<end-point>`, {
method: 'POST',
headers: {
'X-API-KEY': ENV.X_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(translations),
});
3-3. 이 스크립트가 하는 일:
- 번역 키 파일을 모두 읽고,
- 백엔드에 번역 키를 전송해 번역 키를 저장하고 써드파티 번역 API를 통해 번역본을 저장해요.
이렇게 하면 번역 키를 코드에서 작성하는 것만으로 백엔드와 어드민까지 자동으로 연결돼요!
4. 프론트엔드에서 번역본 불러오기: getRequestConfig
프론트엔드에서는 next-intl을 사용해 번역본을 적용합니다.
이때 서버에서 번역본을 받아오고, 캐시를 활용해 성능을 높여요.
import { getRequestConfig } from 'next-intl/server';
import { cookies } from 'next/headers';
import { notFound } from 'next/navigation';
import { COOKIE_KEYS } from '@/constants/cookie-keys';
import { languageApi } from '@/generated/static-apis/Language/Language.query';
// 번역 본을 받아서 next-intl가 이해할 수 있는 형태로 변환
const transformData = (data) => {
const result = {};
for (const { key, value } of data) {
const parts = key.split(/[:.]/);
let obj = result;
for (let i = 0; i < parts.length - 1; i++) {
obj = obj[parts[i]] = obj[parts[i]] || {};
}
obj[parts[parts.length - 1]] = value;
}
return result;
};
export default getRequestConfig(async () => {
const cookieLocale = cookies().get(COOKIE_KEYS.LOCALE)?.value || 'ko';
const [languageList, languageTransList] = await Promise.all([
languageApi.languageList({ params: { cache: 'force-cache' } }),
languageApi.languageTransList({ code: cookieLocale, params: { cache: 'force-cache' } }),
]);
const messages = transformData(languageTransList);
const locales = languageList.map(({ code }) => code);
if (!locales.includes(cookieLocale)) notFound();
return { messages, locale: cookieLocale };
});
4-1. 흐름 설명:
- 쿠키에서 **사용자의 언어(locale)**를 가져오고,
- 백엔드 API에서 번역 데이터를 받아와서,
- next-intl이 사용하는 messages 형태로 변환합니다.
- **캐시(cache)**를 사용해 반복 호출 시 성능도 챙겼어요.
5. 변수 치환 문제: {name}도 번역된다고요?
자동 번역 API를 사용하면 변수도 같이 번역돼버릴 수 있어요.
예시:
{name}님, 안녕하세요!
→ 포르투갈어로 번역 시:
Olá, {nome}!
해결 방법:
- 번역 전에 내용에 사용되는 변수를 숫자 변수로 치환
- 번역이 끝난 후 다시 원래 변수로 복원
이렇게 하면 변수는 안전하게 보호할 수 있어요!
6. 캐시 전략: 읽기 성능 최적화
- 번역 데이터는 읽기(Read)가 많고, 쓰기(Write)는 적어요.
- 그래서 프론트엔드에서는 영구 캐시를 사용합니다.
- 번역본이 업데이트되면 캐시 무효화 API를 호출해 최신 데이터를 유지해요.
- 나중에는 번역본을 S3로 옮겨서 정적 파일로 서빙하는 것도 고려 중입니다.
다국어 관리 스트레스 탈출하기
✅ i18next-parser로 자동 번역 키 추출 → 누락 걱정 NO
- ✅ 백엔드 자동 번역 + 어드민 수동 수정 → 품질 보장
- ✅ 프론트엔드 캐시 + 무효화 → 성능과 신뢰성 확보
- ✅ 네임스페이스 구조화 → 확장성과 가독성 향상
'Next.js' 카테고리의 다른 글
무거워진 Next.js 앱, Micro Frontend Architecture로 가볍게 만들기 (3) | 2025.08.18 |
---|---|
Next.js에서 Server Action과 React hooks를 조합해서 사용자 경험 개선하기 (1) | 2024.06.26 |
Next.js에서 React Server Component의 렌더링 방식 알아보기 (0) | 2024.06.25 |
Next.js에서 fetch와 tanstack-query 효율적으로 사용하기 (0) | 2024.06.23 |
Next.js에서 Data Fetching & Caching 알아보기 (0) | 2024.06.19 |