v0에서 v1, 번역 키가 쌓일 때

April 13, 2026

v0에서 v1으로 넘어가면서 번역 키가 눈에 띄게 불어났습니다.
화면은 바뀌는데 JSON 리소스에 남은 옛 키는 아무도 안 지우니까요.

이 시리즈는 그 미사용 키를 안전하게 정리하는 자동화를 만들기까지의 과정을, 다섯 편에 걸쳐 풀어 봅니다.

공식 CLI를 버리고 직접 만들었다

저희는 공식 i18next-cli를 도입하려다 포기했습니다.

대신 ts-morph로 직접 만든 스크립트로 미사용 키를 탐지하고, GitHub Actions 주간 cron이 자동으로 PR을 올리는 구조를 택했어요.
사람이 하는 일은 diff 확인과 스모크 테스트뿐입니다.

왜 그렇게 됐는지, 순서대로 풀어 보겠습니다.

리뷰 부담과 혼동이 동시에 터졌다

저희 프로젝트는 react-i18next와 JSON 리소스 구조로 번역을 관리합니다.

v0→v1 전환을 거치면서 네임스페이스 18개, ko.json 기준 리프 키 1,000개 이상 규모로 불어났는데, 처음엔 크게 신경 쓰지 않았어요.

키 정리 도구 같은 건 없었습니다.
계속 바뀌는 화면을 따라가며 키가 쌓이는 걸 지켜봐야 했는데, 안 그래도 부족한 리소스에서 이걸 하나하나 대응하기엔 부담이 너무 컸어요.

잘못 지웠을 때 되돌리기도 쉽지 않았고요.

키가 조금씩 쌓이는 건 어느 프로젝트에서나 일어납니다.

그런데 v0에서 v1으로 넘어갈 때는 규모가 달랐어요.

기능과 라우트가 한꺼번에 빠지면서 t() 호출은 사라지는데, JSON에 남은 키는 아무도 안 지웁니다.
동시에 새 화면에 맞춘 키가 추가되니까, 지워야 할 키넣어야 할 키가 동시에 불어났어요.

한두 개씩 손으로 지우는 걸로는 감당이 안 됐습니다.

그래서 자동으로 정리하는 방법을 찾아보기 시작했어요.

우리 팀의 네임스페이스 패턴

자동화 이야기를 하기 전에 배경을 하나 짚어 두겠습니다.

저희 레포에서는 **I18N_KEYS**라는 상수 객체로 네임스페이스 문자열을 모아두고, useTranslation(I18N_KEYS.FEATURE) 형태로 쓰고 있었습니다.

useTranslation('feature') 같은 문자열 리터럴 대신 상수를 쓰는 거죠.

왜 이렇게 두었는지는 다음 편에서 따로 다룹니다.

이 패턴이 공식 추출 도구와 충돌하는 핵심 원인이었어요.
지금은 이 점만 기억해 주세요.

”안전한 삭제”부터 정의했다

자동화를 논의하면서 팀에서 가장 먼저 합의한 건, 무엇이 “안전한 삭제”인가였어요.

도구를 고르기 전에 이걸 정의해두지 않으면, 스크립트가 뭘 지워도 되는지 판단할 수가 없으니까요.

저희가 정한 기준은 세 가지였습니다.

기준의미
런타임 안전사용자에게 번역 키 문자열(feature-detail.legacy.oldField 같은)이 그대로 노출되지 않는다
로케일 쌍 유지ko.jsonen.json에서 같은 키 트리를 유지한다. 한쪽만 지우지 않는다
동적 키 예외t(`prefix.${variable}`)이나 Zod 콜백의 t(key) 같은 패턴은 일괄 삭제하지 않는다

저희 상황에서 가장 까다로운 부분은 세 번째 기준이었습니다.
동적으로 조립되는 키는 정적 분석만으로 사용 여부를 확정할 수 없으니까요.

결국 이런 키들은 화이트리스트로 관리하기로 했어요.

[화이트리스트]
“자동 삭제에서 제외할 키”를 명시적으로 등록해 둔 목록.
스크립트가 미사용으로 판정하더라도 이 목록에 있으면 건드리지 않아요

이 세 기준은 이후에 만들 스크립트와 자동화 파이프라인의 설계 원칙이 되었어요.

키 폭증과 삭제 원칙

  • v0에서 v1으로 넘어가면서 미사용 키와 새 키가 동시에 늘어났고, 손으로 정리하기 어려운 규모가 됐다
  • 키 정리는 기술 부채가 아니라 리뷰 부담, 혼동, 대응 리소스 부족이라는 운영 비용 문제다
  • 자동 삭제를 시도하기 전에 “안전하게 지운다”를 먼저 정의해야 한다. 런타임 안전, 로케일 쌍 유지, 동적 키 예외

다음 편에서는 I18N_KEYS라는 상수 객체를 쓰는 이유와, 이 패턴이 공식 추천 플러그인과 왜 충돌하는지 정리합니다.

On this page