4편까지 로컬에서 돌아가는 스크립트를 완성했습니다.
이제 남은 건 “사람이 기억해서 돌려야 한다”는 마지막 병목이었어요.
마지막 편에서는 이 스크립트를 GitHub Actions 주간 cron에 물리고, PR 생성부터 안전 검증까지 자동화한 과정을 다룹니다.
로컬 CLI의 다음 병목
4편까지 완성한 스크립트는 로컬에서 잘 돌아갔어요.
pnpm i18n:find-unused로 리포트를 뽑고, pnpm i18n:prune으로 삭제하고, PR을 올리면 됩니다.
그런데 실제로 해보니 한 가지가 걸렸어요.
사람이 주기적으로 돌려야 한다는 거였습니다.
스크립트가 아무리 정확해도, 누군가가 “이번 주에 돌려야지” 하고 기억해야 하면 결국 밀리게 되거든요.
한 달쯤 지나니까 정확히 그렇게 됐습니다.
스크립트는 있는데 아무도 안 돌리는 상태.
그래서 이 마지막 한 걸음을 자동화하기로 했어요.
파이프라인의 흐름
자동화의 목표는 단순했어요.
매주 한 번 스크립트가 돌고, 미사용 키가 있으면 PR이 올라오고, 사람은 diff 확인과 스모크 테스트만 하면 되는 구조.
전체 흐름을 정리하면 이렇습니다.
- 스케줄 트리거 - 매주 일요일 02:00 KST에 cron이 실행돼요
- 환경 준비 - checkout, pnpm setup, install
- 리포트 생성 -
--json으로 미사용 키 목록을 뽑는다 - 삭제 적용 -
--apply로 실제 JSON에서 키를 제거한다 - 포맷 정리 -
pnpm lint --fix로 코드 스타일을 맞춰요 - 변경 감지 - diff가 없으면 PR 없이 종료한다
- PR 본문 생성 -
build-i18n-pr-body.cjs로 현황 표·체크리스트·스냅샷을 조립한다 - PR 생성 -
peter-evans/create-pull-request로 자동 PR을 올려요
워크플로우 YAML
.github/workflows/i18n-prune.yml 전체 보기
# .github/workflows/i18n-prune.yml
name: i18n prune unused keys
on:
schedule:
- cron: '0 17 * * 6' # 매주 일요일 02:00 KST (UTC 17:00 토요일)
workflow_dispatch:
inputs:
dry_run:
description: '삭제 없이 리포트만 확인'
type: boolean
default: false
force:
description: '30% 안전 가드 무시'
type: boolean
default: false
permissions:
contents: write
pull-requests: write
jobs:
prune:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- run: pnpm install --frozen-lockfile
working-directory: fe
# 1) 리포트 생성
- name: Generate unused keys report
run: pnpm exec tsx scripts/i18n/find-unused-i18n.ts --json > /tmp/i18n-report.json
working-directory: fe
# 2) 실제 삭제 적용 (dry_run이면 스킵)
- name: Prune unused keys
if: ${{ !inputs.dry_run }}
run: pnpm exec tsx scripts/i18n/find-unused-i18n.ts --apply ${{ inputs.force && '--force' || '' }}
working-directory: fe
# 3) 포맷 정리
- name: Fix lint
if: ${{ !inputs.dry_run }}
run: pnpm lint --fix
working-directory: fe
# 4) 변경 없으면 early exit
- name: Check diff
id: diff
run: |
git diff --quiet && echo "no_changes=true" >> $GITHUB_OUTPUT || true
# 5) PR 본문 생성
- name: Build PR body
if: steps.diff.outputs.no_changes != 'true'
run: node .github/scripts/build-i18n-pr-body.cjs /tmp/i18n-report.json > /tmp/pr-body.md
working-directory: fe
# 6) PR 생성
- name: Create Pull Request
if: steps.diff.outputs.no_changes != 'true'
uses: peter-evans/create-pull-request@v6
with:
branch: chore/i18n-prune-${{ github.run_id }}
commit-message: 'chore(i18n): remove unused translation keys'
title: 'chore(i18n): remove unused translation keys'
body-path: /tmp/pr-body.md
labels: i18n, automated세팅하면서 신경 쓴 부분이 몇 가지 있었어요.
- cron 타이밍: 일요일 새벽에 돌려서, 월요일 오전에 PR을 확인하고 머지하면 한 주를 깔끔하게 시작할 수 있게 했어요
- 수동 실행:
workflow_dispatch로 화이트리스트 수정 직후 바로 결과를 확인할 수 있게 열어뒀어요.dry_run,force옵션도 넣었습니다 - 브랜치 격리:
run_id를 브랜치 이름에 붙여서 매주 별도 PR이 생기게 했어요. 각 PR의 diff를 독립적으로 보고 싶었거든요
PR 본문도 별도 스크립트(build-i18n-pr-body.cjs)로 자동 생성합니다.
- 미사용 키 제거 현황 표: 네임스페이스별 삭제 수 요약
- 리뷰 체크리스트: 동적 키를 쓰는 화면 목록
- 카탈로그 상태 스냅샷: prune 후 ko/en 키 수와 en 미번역 목록
JSON 리포트를 그대로 넣으면 리뷰어가 읽기 어려워서, 이 스크립트로 매번 같은 형식의 마크다운을 만들어요.
참고로 처음에 GitHub Actions의 PR 생성 권한 설정을 빠뜨려서, 워크플로우는 성공하는데 PR 스텝에서 403이 나온 적이 있었어요.
안전 레이어, 1편의 약속을 지키는 방법
1편에서 “안전하게 지운다”를 세 가지로 정의했습니다.
런타임 안전, 로케일 쌍 유지, 동적 키 예외.
자동화 파이프라인에서 이 세 기준이 각각 어떻게 보장되는지 살펴봅니다.
| 안전 기준 (1편) | 파이프라인의 대응 레이어 | 동작 |
|---|---|---|
| 런타임 안전 | 30% 가드 | 미사용 키가 전체의 30%를 넘으면 스크립트가 non-zero exit으로 중단, PR이 생성되지 않는다 |
| 로케일 쌍 유지 | --apply 로직 (4편) | ko.json과 en.json에서 같은 키를 동시에 삭제한다 |
| 동적 키 예외 | 화이트리스트 + dry-run diff | 화이트리스트로 보존하고, PR 본문 리포트에서 의심 키를 확인할 수 있다 |
여기에 추가로 두 겹의 검증이 더 있었어요.
실제로 운영하면서 이 레이어들이 빈틈을 메워주는 걸 여러 번 확인했습니다.
| 레이어 | 역할 |
|---|---|
| 타입체크/lint | 키 삭제 자체는 보통 TS 빌드를 깨뜨리지 않지만, 관련 상수나 테스트가 있으면 함께 잡힌다 |
| 수동 스모크 | 동적 키가 많은 화면을 PR 머지 전에 직접 확인한다 |
자동으로 삭제까지는 하지만, 머지는 사람이 합니다.
diff를 읽고, 의심 가는 키가 있으면 화이트리스트에 추가한 뒤 다음 주에 다시 돌리면 돼요.
수동 스모크 체크리스트
화이트리스트로 보존했더라도, 리뷰어가 PR 머지 전에 확인해야 할 화면이 있습니다.
동적 키가 집중된 화면들이에요.
- 권한 셀렉트 - 권한 레벨이 번역 키로 표시되는 컴포넌트.
t(`permission.${value}`)패턴을 쓰는 곳 - 히스토리/피드백 테이블 - 필드명을
FIELD_MAP으로 순회하며t()에 넘기는 컬럼 - 편집 모달 - 모달 제목을
mode(create/edit)로 분기하는 패턴 - 연동 관리 -
i18n.t('key', { ns })간접 호출을 사용하는 화면
이 목록을 PR 템플릿에 체크리스트로 넣어두면 리뷰어가 빠뜨리지 않습니다.
저희는 .github/PULL_REQUEST_TEMPLATE/i18n-prune.md에 별도 템플릿을 만들어서, 자동 PR에 연결해뒀어요.
파이프라인 전체 구성
- “사람이 기억해서 돌려야 한다”는 마지막 병목을 GitHub Actions 주간 cron으로 해결했다
- 1편의 “안전한 삭제” 세 기준은 30% 가드, 로케일 동시 삭제, 화이트리스트+dry-run으로 각각 대응돼요
- 동적 키가 집중된 화면은 수동 스모크 체크리스트로 PR 머지 전에 확인한다
최종적으로 이번 자동화에 관여하는 파일은 이렇습니다.
fe/scripts/i18n/
├── find-unused-i18n.ts # AST 스캐너 본체
└── i18n-preserve.ts # 동적 키 화이트리스트
.github/
├── workflows/i18n-prune.yml # 주간 cron + 수동 실행
└── scripts/build-i18n-pr-body.cjs # PR 본문 생성
다섯 편에 걸쳐 공유한 내용을 돌아보면, 출발점은 단순했습니다.
v0에서 v1으로 넘어가면서 JSON에 미사용 키가 쌓였고, 리뷰 부담이 늘었어요.
처음에는 공식 i18next-cli로 해결하려 했습니다.
그런데 저희 팀의 I18N_KEYS 패턴과 충돌하면서, 네임스페이스 전체를 삭제 대상으로 잡는 오탐을 실측으로 확인했어요.
결국 ts-morph로 직접 스크립트를 만들었고, 화이트리스트와 30% 가드로 안전 장치를 갖췄습니다.
마지막으로 GitHub Actions 주간 cron을 붙여서, 사람이 기억하지 않아도 PR이 올라오는 구조를 완성했어요.
완벽하다고는 할 수 없습니다.
화이트리스트는 수동으로 관리해야 하고, 동적 키가 새로 생길 때마다 누군가 목록을 업데이트해야 해요.
스모크 체크리스트도 화면이 바뀌면 함께 갱신해야 합니다.
그리고 미번역 비율 모니터링이나 역방향 lint 검증은 아직 남겨둔 과제예요.
그래도 “안 쓰는 키를 안전하게 지운다”라는 처음 목표는 달성했다고 생각합니다.
매주 월요일 아침에 PR이 올라와 있고, 리뷰어는 diff를 확인하고 스모크 테스트를 돌린 뒤 머지합니다.
그 정도면, 번역 카탈로그 관리에 들이는 비용을 충분히 줄인 셈이에요.