번역 파일 동기화를 Codex CLI에 맡겨봤더니 — i18n 키 누락 자동 감지 파이프라인 구축기
다국어 서비스를 운영하다 보면 반드시 한 번쯤 겪는 악몽이 있습니다. 신규 기능을 배포한 다음 날 아침, 일본어 사용자로부터 "버튼에 이상한 글자가 나와요"라는 제보가 들어옵니다. 확인해보면 en.json에만 추가한 키가 ja.json에는 빠진 채로 폴백 없이 키 이름 그대로 노출된 것이죠. 저도 처음 이 문제를 마주했을 때 "이건 도구로 해결해야지, 사람이 검사하면 안 되겠다"는 생각이 들었습니다.
수천 개 문자열에 지원 로케일이 늘어나면 항목 수는 곱셈으로 불어납니다. 이 규모에서 t() 호출이 어느 파일에 있는지, 어느 로케일 파일에 빠졌는지를 수동으로 추적하는 건 사실상 불가능하고, 린터나 타입 생성만으로는 커버하기 어려운 경우가 많습니다.
이 글에서는 Codex CLI의 codex exec로 코드베이스를 스캔해 누락 키를 리포트하고, MCP 서버로 번역 파일을 일괄 동기화하고, GitHub Actions로 PR 단계에서 차단하는 파이프라인을 실제 팀에서 굴려본 경험을 바탕으로 정리합니다. 다만 저희 팀은 아직 통계를 공개할 만큼 오래 운영하진 못했으니, 수치 대신 실제로 걸렸던 함정 위주로 나눠드리겠습니다.
왜 지금 이 조합인가
Codex CLI가 반복 작업 도구로 자리잡기까지
Codex CLI는 OpenAI가 제공하는 터미널 기반 AI 코딩 에이전트입니다. 오픈소스로 공개되어 있고(저장소: github.com/openai/codex, TypeScript 기반), 파일 읽기·수정·명령 실행을 자연어 프롬프트로 처리합니다. 대화형 세션 외에 codex exec라는 비대화형 실행 명령을 제공한다는 점이 이 글의 핵심인데, 이 덕분에 CI/CD 파이프라인에서 사람 개입 없이 돌릴 수 있습니다. 모델은 --model 플래그로 지정하며, GPT-4o(범용) 또는 o1·o3 같은 추론 모델을 필요에 따라 붙일 수 있습니다. 두 계열은 성격이 다르니 뒤에서 다시 짚겠습니다.
MCP가 바꾼 지점
Codex CLI 혼자로는 i18n 자동화에 한계가 있습니다. MCP(Model Context Protocol) 는 AI 에이전트가 외부 도구·데이터 소스를 표준화된 방식으로 호출하도록 하는 프로토콜인데, 이 표준화 덕분에 번역 파일 스캔·TMS 연동·자동 번역 로직을 한 번 MCP 서버로 만들어두면 Claude Code, Cursor, Codex CLI 등 MCP 호환 클라이언트 전반에서 재사용할 수 있습니다. "게임 체인저"라기보다는, 매 클라이언트마다 다시 붙이던 접착 코드를 없앤 것이 실질적인 변화입니다.
2026년 9월 시점에서 i18n 관련 MCP 서버는 대부분 커뮤니티 오픈소스 프로젝트 형태로 유통되고 있습니다(뒤 표 참고). TMS 벤더들의 공식 지원 여부는 각 TMS 릴리스 노트로 직접 확인하는 것이 안전하고, 이 글에서는 커뮤니티 서버 기준으로 설명합니다.
전체 파이프라인 구조
한 가지 미리 강조하면, dry-run 이후 승인 단계는 codex exec 안이 아니라 리포트 파일을 사람이 열어보고 별도 명령으로 적용하는 형태입니다. 이유는 3단계에서 자세히 설명합니다.
단계별 설정
1단계: AGENTS.md로 프로젝트 규약 인코딩
Codex가 프로젝트 번역 구조를 이해하려면 명시적인 지침이 필요합니다. 프로젝트 루트에 AGENTS.md 파일을 만들고 번역 네이밍 컨벤션과 파일 위치를 기술합니다.
# AGENTS.md
## i18n 규약
### 파일 구조
번역 파일은 `src/locales/{locale}/` 디렉터리에 JSON 형식으로 저장됩니다.
- 기준 로케일: `en` (소스 오브 트루스)
- 지원 로케일: `ko`, `ja`, `zh-CN`, `fr`, `de`, `es`, `pt-BR`
### 키 네이밍 규칙
- 점 표기법으로 네임스페이스 구분: `namespace.section.key`
- 예: `common.button.submit`, `auth.error.invalid_credentials`
### 번역 함수 패턴
- React: `useTranslation` 훅의 `t()` 호출
- 파일: `*.tsx`, `*.ts` (테스트 파일 제외)
- 패턴: `t('key.name')`, `t('key.name', { variable })`
### 플레이스홀더 규칙
- ICU 메시지 포맷 사용: `{variable}`, `{count, plural, one {# item} other {# items}}`
- 번역 후에도 플레이스홀더가 정확히 보존되어야 합니다.
## 감사 태스크
누락 키 리포트 시 다음 형식을 따릅니다:
- 로케일별 누락 키 목록
- 소스 파일 경로 포함
- 기준 로케일 대비 커버리지 비율AGENTS.md 초기 작성이 가장 손이 많이 가는 부분입니다. 프로젝트 구조가 복잡할수록 여기에 투자한 시간이 이후 자동화 품질을 결정합니다.
2단계: codex exec로 감사 실행
AGENTS.md가 준비되면 codex exec로 코드베이스 스캔을 실행합니다. --sandbox read-only가 중요한데, 이 단계에서는 파일을 절대 수정하지 않겠다는 보장입니다.
codex exec \
--model gpt-4o \
--sandbox read-only \
"src/locales 디렉터리의 en.json을 기준으로 모든 로케일 파일의 누락 키를 감사하고, \
각 로케일별 커버리지 비율과 누락 키 목록을 audit-report.json 파일에 저장해줘. \
소스 코드에서 t() 호출도 스캔해서 locales 파일에 아예 정의되지 않은 키가 있으면 별도 섹션으로 분리해줘."여기서 주의할 점 하나. LLM 출력은 비결정론적이라 프롬프트만으로는 매번 같은 스키마가 나오지 않습니다. 다음 단계에서 이 파일을 프로그램적으로 소비하려면, 스키마를 프롬프트에 명시적으로 박아두고(예: JSON Schema를 프롬프트에 붙이기), 후처리 스크립트로 zod나 ajv 같은 검증기를 통과시켜야 합니다. 다음은 스키마가 안정화되었을 때 대략 이런 모양이 나온다는 개념적 예시입니다.
{
"coverage": {
"ko": { "ratio": 0.94, "total": 2000, "present": 1880, "missing": 120 },
"ja": { "ratio": 0.87, "total": 2000, "present": 1740, "missing": 260 },
"pt-BR": { "ratio": 0.72, "total": 2000, "present": 1440, "missing": 560 }
},
"missingKeys": {
"ko": ["feature.dashboard.new_widget", "settings.privacy.cookie_banner"],
"ja": ["feature.dashboard.new_widget", "auth.mfa.setup_prompt"]
},
"undefinedInSource": [
{ "key": "legacy.old_flow.button", "file": "src/components/OldModal.tsx", "line": 42 }
]
}수치와 파일 경로는 예시일 뿐 실제 프로젝트마다 다르고, 스키마가 프롬프트대로 나오지 않을 경우를 대비해 파서 단에서 실패 시 재시도 또는 사람 개입으로 떨어뜨리는 방어선이 필요합니다.
undefinedInSource 섹션이 특히 유용합니다. 로케일 파일에 정의되지 않은 키를 t()로 호출하는 코드를 잡아냅니다. 타입스크립트 타입 생성으로도 잡히지만, Codex는 소스 컨텍스트를 함께 읽기 때문에 "이 키는 실제로 삭제해도 되는 dead key"인지까지 후보로 제시할 수 있다는 점이 다릅니다(최종 판단은 사람이 해야 합니다).
3단계: MCP 서버로 번역 초안 생성과 적용 분리
감사 리포트가 나왔으면 이제 MCP 서버를 통해 누락 키를 채웁니다. i18n 관련 커뮤니티 MCP 서버가 여럿 있고, 프로젝트 요구사항에 따라 선택이 달라집니다(모두 오픈소스이며, 공식/유지보수 활성도는 사용 전 저장소에서 직접 확인하는 것을 권합니다).
| MCP 서버 | 주요 특징 | 적합한 상황 |
|---|---|---|
gtrias/i18next-mcp-server |
헬스체크, 누락 키 감지, 번역 초안 생성 | i18next 기반 프로젝트 |
Ret2Hell/i18n-mcp |
dry-run 패치 산출, dead-key 탐지 | 배치 처리 안전성 우선 |
dalisys/i18n-mcp |
하드코딩 문자열 분석, 파일 감시 | 레거시 마이그레이션 |
reinier-millo/i18n-mcp-server |
JSON i18n 파일 특화 | 단순 JSON 구조 |
Codex CLI의 설정 파일 포맷은 저장소마다 문서가 갱신되고 있어, MCP 서버 등록 방식은 사용 중인 Codex CLI 버전의 docs/config.md(또는 README)를 반드시 확인해 주세요. 여기서는 형식보다 개념 구조를 강조하기 위해, "MCP 서버 커맨드 + 환경 변수 + 서버별 옵션" 세 조각이 필요하다는 점만 기억하면 됩니다. 서버 실행 자체는 대개 다음처럼 단순합니다.
LOCALES_DIR=./src/locales SOURCE_LOCALE=en \
npx -y i18next-mcp-server그리고 여기서 초안의 흐름을 한 번 바로잡습니다. codex exec는 이름 그대로 비대화형이라, 실행 중에 "dry-run 보여주고 승인받기"를 프롬프트 안에서 실현할 수 없습니다. 이 단계는 두 개의 명령으로 분리하는 것이 정직한 설계입니다.
첫 명령은 read-only로 초안 파일만 만들어냅니다.
codex exec \
--model o3 \
--sandbox read-only \
"audit-report.json의 누락 키들에 대해 i18n MCP 서버로 번역 초안을 생성하고, \
결과를 translations.dryrun.json에 저장해줘. \
플레이스홀더 {variable}과 ICU 복수형 구문은 원형 그대로 보존해야 해."리뷰가 끝난 뒤 두 번째 명령은 파일 쓰기 권한을 부여합니다.
codex exec \
--model gpt-4o \
--sandbox workspace-write \
"translations.dryrun.json의 내용을 src/locales의 해당 로케일 파일에 병합해줘. \
기존 키의 값은 덮어쓰지 말고, 누락된 키만 새로 추가해줘."Codex의 판단이 개입하는 병합 대신, 단순한 병합이라면 이 두 번째 단계를 그냥 Node 스크립트로 처리하는 것이 훨씬 예측 가능합니다. 저는 실제로 이 부분은 스크립트로 뽑아 썼습니다. LLM에 맡길수록 예측 불가능한 편집이 섞일 여지가 늘어나기 때문입니다.
4단계: GitHub Actions로 PR 단계 차단과 자동 보완
로컬 자동화만으로는 부족합니다. 팀 전체가 번역 완결성 기준을 지키려면 CI에서 강제해야 합니다. 여기서는 두 개의 잡을 나눕니다. 감사 잡은 read-only로만 돌면서 커버리지 게이트를 검사하고, 보완 잡은 감사 실패 시 별도 브랜치에 번역 커밋을 밀어 넣도록 트리거됩니다. 이렇게 나눠야 다이어그램이 약속한 "감사 → 병렬 번역 → 커밋 → 재검사" 흐름을 실제 YAML로 구현할 수 있습니다.
# .github/workflows/i18n-check.yml
name: i18n Coverage Check
on:
pull_request:
paths:
- 'src/**/*.tsx'
- 'src/**/*.ts'
- 'src/locales/**'
jobs:
audit:
runs-on: ubuntu-latest
outputs:
needs_fill: ${{ steps.audit.outputs.needs_fill }}
steps:
- uses: actions/checkout@v4
- name: i18n 누락 키 감사 (read-only)
id: audit
uses: openai/codex-action@v1
with:
prompt: |
src/locales/en.json을 기준으로 모든 로케일 파일의 커버리지를 검사해줘.
90% 미만인 로케일이 있으면 GitHub Actions 어노테이션으로 누락 키를 출력하고
needs_fill=true를 GITHUB_OUTPUT에 기록한 뒤 exit 1로 종료해줘.
sandbox: read-only
model: o3
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
fill:
needs: audit
if: failure() && needs.audit.outputs.needs_fill == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: 번역 초안 생성 및 커밋
uses: openai/codex-action@v1
with:
prompt: |
누락 키에 대해 i18n MCP 서버로 번역 초안을 생성하고
src/locales의 해당 로케일 파일에 병합해줘.
플레이스홀더는 원형 그대로 보존.
sandbox: workspace-write
model: gpt-4o
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: 변경사항 커밋
run: |
git config user.name "codex-bot"
git config user.email "codex-bot@users.noreply.github.com"
git add src/locales
git diff --cached --quiet || git commit -m "chore(i18n): fill missing translations"
git pushopenai/codex-action의 정확한 입력 스펙과 지원 옵션은 Codex GitHub Action 공식 문서에서 확인하고 사용하는 버전에 맞춰 조정하는 것을 권합니다. 위 YAML은 잡 분리 구조를 보여주기 위한 골격이라, 실제 환경에서는 서명된 커밋, 리뷰 요청, 라벨링 등을 얹어 팀 규칙에 맞춰야 합니다.
한 가지 더. 자동 커밋된 번역은 반드시 사람 리뷰 후 승인을 거치도록 브랜치 보호 규칙을 걸어두세요. LLM이 만든 문자열이 그대로 프로덕션에 들어가는 경로가 열려 있으면 번역 사고가 나기 좋은 조건입니다.
얻는 것과 포기하는 것
실제 트레이드오프
| 항목 | 얻는 것 | 포기하는 것 / 감수해야 할 비용 |
|---|---|---|
| 반복 작업 | 키 추출, 파일 동기화 자동화로 개발자 시간 확보 | AGENTS.md·프롬프트·스키마 유지보수 부담이 새로 생김 |
| 번역 품질 | LLM이 UI 문맥을 반영해 단순 사전 치환보다 자연스러움 | 도메인 특화 어휘·톤 일관성은 여전히 사람 검수 필요 |
| 감사 안전성 | read-only 샌드박스로 스캔 중 파일시스템 변경 없음 | workspace-write 단계와 CI 커밋 권한은 별도 위험 관리 필요 |
| CI 통합 | 기존 파이프라인에 잡 추가로 통합 가능 | 매 PR 실행 시 API 비용이 누적, 증분 실행 전략 필요 |
| MCP 재사용성 | 서버 하나를 여러 클라이언트에서 재사용 | 아직 대부분 커뮤니티 프로젝트라 유지보수 지속성이 변수 |
| 결정성 | 사람보다 빠르고 지치지 않음 | LLM 출력의 비결정성으로 스키마·플레이스홀더 방어 코드 필수 |
실무에서 자주 걸리는 함정
LLM 출력의 비결정성: 2단계 리포트, 3단계 초안 모두 같은 프롬프트에 다른 결과가 나올 수 있습니다. 후속 단계가 이 출력을 프로그램적으로 소비한다면 반드시 스키마 검증기를 붙이고, 검증 실패 시 재시도 상한과 사람 개입 경로를 두세요.
API 비용 누적: 대규모 번역 키를 매 PR마다 전부 처리하면 비용이 빠르게 쌓입니다. 변경된 키만 처리하는 증분 실행 전략이 필요합니다. git diff --name-only origin/main...HEAD로 변경된 소스·로케일 파일을 뽑아 Codex에 넘기는 방식이 효과적입니다.
네트워크 격리: workspace-write 샌드박스에서도 기본적으로 네트워크가 제한됩니다. 외부 TMS API를 호출해야 하는 MCP 서버는 별도 네트워크 허용 설정이 필요하고, 이걸 모르고 "왜 MCP 서버가 TMS에 못 붙나"로 시간을 날리기 쉽습니다. 사용하는 Codex CLI 버전의 샌드박스 옵션 문서를 먼저 읽어보세요.
플레이스홀더 붕괴: {name}, {count, plural, one {# item} other {# items}} 같은 ICU 구문이 번역 후 깨지는 경우가 간헐적으로 발생합니다. AGENTS.md에 명시해도 완벽하진 않으니, CI에 플레이스홀더 매칭 검증 단계를 별도로 두는 편이 안전합니다. 소스 값의 {...} 토큰 집합과 번역 값의 토큰 집합이 일치하는지 확인하는 짧은 스크립트만으로도 상당수를 잡아냅니다.
dead key 정리: 삭제된 UI에 대응하는 번역 키를 제거하는 작업은 별도 설정 없이는 자동 처리되지 않습니다. Ret2Hell/i18n-mcp 서버의 dead-key 탐지 기능을 활용하거나, 감사 프롬프트에 "더 이상 소스 코드에서 참조하지 않는 키 목록도 별도 섹션으로 포함해줘"를 추가해 리뷰 대상으로 올리는 방식이 현실적입니다. 자동 삭제는 위험하니, 리포트만 만들고 삭제 판단은 사람이 하는 흐름을 권합니다.
모델 선택: GPT-4o는 범용 지시 이행이 빠르고 저렴한 편이라 대량 리포트 생성에 유리하고, o1·o3 같은 추론 모델은 복잡한 판단(예: dead key 후보 선별, 컨텍스트 기반 번역 뉘앙스 판정)에 더 어울립니다. 감사와 초안을 같은 모델로 통일하기보다는 단계별로 나눠 붙이는 것이 비용·품질 균형에 낫습니다.
빌드 타임 탐지 + 런타임 폴백 이중 방어
Codex CLI 파이프라인만 믿는 것보다, 이중 방어 구조가 더 견고합니다.
i18next-cli의 status --ci 옵션으로 빌드 시 탐지하고, saveMissing + fallbackLng 설정으로 런타임 폴백하는 구조입니다. Codex CLI 파이프라인이 대부분을 커버해도, 런타임에 예상치 못한 경로에서 키가 빠지는 경우를 대비해 폴백 레이어를 유지하는 편이 실무적으로 안전합니다.
이 파이프라인이 커버하지 못하는 경계
여기까지 만들어 두면 "어느 키가 어디에 빠졌는지"를 사람이 뒤지는 시간은 확실히 사라집니다. 그러나 다음은 여전히 사람의 영역입니다.
- 톤·브랜딩 일관성: 자동 번역 초안이 문법적으로 맞아도 서비스의 말투와 어긋나는 경우가 있습니다. 톤 가이드는 별도 문서로, 리뷰어의 눈으로 지키는 것이 현실적입니다.
- 법적·의료·금융 도메인 표현: LLM 번역을 사람 검토 없이 프로덕션에 내보내기 어려운 영역입니다. 파이프라인은 "초안 자동 생성 + 전문가 검토" 워크플로우의 앞단으로만 쓰는 것을 권합니다.
- 문화적 로컬라이제이션: 날짜·통화·이미지·색상 같은 비문자열 요소는 이 파이프라인의 범위 밖입니다. 로케일별 리소스 관리 체계가 별도로 필요합니다.
- A/B 테스트 중인 문자열: 실험용 카피가 자동 번역·자동 커밋 파이프라인에 노출되면 원치 않는 변형이 로케일 파일에 굳어질 수 있습니다. 실험 문자열은 네임스페이스를 나눠 파이프라인에서 제외하세요.
- 결정성 요구가 높은 경계: 릴리스 노트·마케팅 문구처럼 워딩이 계약에 가까운 텍스트는 자동화 대상에서 빼는 편이 사고를 줄입니다.
파이프라인은 반복 노동을 줄여주는 도구지, 번역 책임을 위임받는 주체는 아닙니다. 이 경계를 팀 안에서 명확히 합의해 두면, 자동화 도입 이후에도 번역 품질에 대한 소유권이 흐려지지 않습니다.
참고 자료
- GitHub - openai/codex
- Codex GitHub Action 공식 문서
- Model Context Protocol 공식 사이트
- GitHub - Ret2Hell/i18n-mcp
- GitHub - gtrias/i18next-mcp-server
- GitHub - dalisys/i18n-mcp
- Missing Translations in i18next: Fallbacks, Detection & Fixes
- Automating i18next Translations with saveMissing and Locize AI
- i18next-cli 문서