네임스페이스를 상수 객체로 관리하자

April 13, 2026

1편에서 “안전하게 지운다”의 기준을 세웠습니다.

자동화를 본격적으로 만들기 전에, 저희 팀의 네임스페이스 관리 방식부터 짚어둘 필요가 있어요.

이번 편에서는 I18N_KEYS 상수 객체로 네임스페이스를 묶은 이유와, 이 선택이 공식 추천 플러그인과 왜 충돌하는지 정리합니다.

JSON과 i18n.ts를 한 벌로 맞추기

1편에서 저희 레포가 I18N_KEYS라는 상수 객체로 네임스페이스를 관리하고 있다고 언급했어요.

자동화를 이어가기 전에 왜 이 구조를 선택했는지부터 짚어보면 좋을 것 같아요.

저희 프로젝트는 서비스 내부의 도메인마다 네임스페이스를 두고, 그에 맞춰 번역 키를 관리하고 있었어요.

ko.jsonen.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 배열에 넘기고, resourcestranslationKO[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.init resources 조립, useTranslation 호출을 하나의 원천으로 묶는 상수 객체예요
  • 에디터 자동완성, 심볼 리네임, 참조 검색에서 문자열 리터럴보다 확실히 편했어요
  • 다만 공식 추천 플러그인에서는 MemberExpression으로 보여서, 네임스페이스 값을 바로 알 수 없다는 트레이드오프가 있었어요

이 트레이드오프가 실제로 얼마나 심각한지 확인하기 위해, i18next-cli를 저희 레포에 직접 돌려봤어요.

On this page