1편에서 “안전하게 지운다”의 기준을 세웠습니다.
자동화를 본격적으로 만들기 전에, 저희 팀의 네임스페이스 관리 방식부터 짚어둘 필요가 있어요.
이번 편에서는 I18N_KEYS 상수 객체로 네임스페이스를 묶은 이유와, 이 선택이 공식 추천 플러그인과 왜 충돌하는지 정리합니다.
JSON과 i18n.ts를 한 벌로 맞추기
1편에서 저희 레포가 I18N_KEYS라는 상수 객체로 네임스페이스를 관리하고 있다고 언급했어요.
자동화를 이어가기 전에 왜 이 구조를 선택했는지부터 짚어보면 좋을 것 같아요.
저희 프로젝트는 서비스 내부의 도메인마다 네임스페이스를 두고, 그에 맞춰 번역 키를 관리하고 있었어요.
ko.json과 en.json에는 gnb, dashboard, common처럼 하이픈이 섞인 네임스페이스 문자열이 최상위 키로 들어 있고, 앱에서는 이 키들을 resources[locale][namespace] 형태로 i18n.init에 넘기는 구조였습니다.
문제는 이 네임스페이스 문자열을 컴포넌트마다 직접 타이핑할 때 생겼어요.
- 오타 하나가 조용히 잘못된 네임스페이스로 이어질 수 있었어요
- JSON 키 이름을 바꿀 때 grep으로 전부 잡히리라는 보장도 없었어요
- 어떤 네임스페이스가 존재하는지 파악하려면 JSON 파일을 직접 열어봐야 했어요
이런 문제들을 해결할 수 있을 거라는 기대로, 네임스페이스 문자열을 한 파일에 상수 객체로 모아두는 방향을 택했습니다.
실제 정의는 이렇습니다.
export const I18N_KEYS = {
COMMON: 'common',
DASHBOARD: 'dashboard',
PROJECT: 'project',
// ... 18개 네임스페이스
} as const;
export type I18NKey = (typeof I18N_KEYS)[keyof typeof I18N_KEYS];
18개의 네임스페이스가 이 파일 하나로 관리됩니다.
i18n.ts에서는 Object.values(I18N_KEYS)를 ns 배열에 넘기고, resources를 translationKO[ns] 방식으로 조립해요.
JSON의 최상위 키, 초기화 코드, 컴포넌트의 useTranslation 호출이 모두 같은 원천을 바라보는 구조입니다.
useTranslation에서 상수를 쓰면 달라지는 것
저희는 useTranslation('common')이 아니라 useTranslation(I18N_KEYS.COMMON) 형태로 씁니다.
t(...) 인자에는 키 경로만 두고, 네임스페이스는 useTranslation의 인자로 분리하는 거죠.
// 문자열 리터럴 방식
const { t } = useTranslation('common');
// 상수 객체 방식: 저희 팀의 선택
const { t } = useTranslation(I18N_KEYS.COMMON);
사실 두 코드의 런타임 결과는 똑같아요.
차이는 에디터에서 코드를 작성할 때 드러났어요.
- 에디터 자동완성:
I18N_KEYS.을 치면 18개 네임스페이스가 전부 뜬다 - 심볼 리네임: 네임스페이스 이름을 바꿀 때 IDE 리네임 한 번이면 끝이다
- 참조 검색:
I18N_KEYS.GNB를 누가 어디서 쓰는지 한 번에 추적할 수 있다
그리고 한 가지 더.
저희 팀은 ns:key 콜론 표기를 쓰지 않습니다.
t('a.b')라고 쓰면 그건 현재 로드된 네임스페이스 안에서의 중첩 키(. 구분)예요.
“이 문자열이 네임스페이스인지 키인지”를 한 줄에 섞지 않는 것도 의도적인 선택이었습니다.
리터럴 vs 상수 객체, 트레이드오프
물론 상수 객체가 모든 면에서 유리한 건 아니었어요.
두 패턴을 나란히 놓고 보면 이렇습니다.
| 항목 | useTranslation('common') | useTranslation(I18N_KEYS.COMMON) |
|---|---|---|
| 오타 방지 | grep/lint에 의존 | 컴파일 타임 오류 |
| 리네임 | 문자열이라 전수 교체 필요 | 심볼 리네임 1회 |
| 참조 추적 | 텍스트 검색 | IDE “Find All References” |
| 공식 플러그인 호환 | 리터럴로 바로 인식 | MemberExpression으로 인식, 값 추적이 필요 |
위 세 줄까지는 상수 객체가 확실히 편합니다.
그런데 마지막 행이 문제였어요.
그런데 공식 플러그인이 이 패턴을 못 읽었다
i18next는 공식 문서의 플러그인 목록에서 정적 분석 도구를 추천하고 있어요.
대표적인 게 i18next-cli인데, 이 플러그인은 코드를 파일 단위로 스캔하면서 StringLiteral만 수집합니다.
useTranslation('gnb')는 바로 인식하지만, useTranslation(I18N_KEYS.GNB)는 MemberExpression이라는 다른 노드 타입으로 보여요.
“이게 곧 'gnb'다”를 알려면 I18N_KEYS가 정의된 다른 파일까지 따라가야 하는데, 대부분의 플러그인이 이 크로스 파일 추적을 지원하지 않습니다.
즉, 저희가 얻은 세 가지 이점(오타 방지, 리네임, 참조 추적)의 대가로, 공식 추천 플러그인이 네임스페이스를 아예 인식하지 못하는 상황이 생길 수 있는 거예요.
상수 객체 패턴의 득과 실
I18N_KEYS는 JSON 최상위 키,i18n.initresources 조립,useTranslation호출을 하나의 원천으로 묶는 상수 객체예요- 에디터 자동완성, 심볼 리네임, 참조 검색에서 문자열 리터럴보다 확실히 편했어요
- 다만 공식 추천 플러그인에서는
MemberExpression으로 보여서, 네임스페이스 값을 바로 알 수 없다는 트레이드오프가 있었어요
이 트레이드오프가 실제로 얼마나 심각한지 확인하기 위해, i18next-cli를 저희 레포에 직접 돌려봤어요.