AI와 함께 레거시 프로젝트에 i18n 안정성을 더하기 (전체 프롬프트 공유)
지난주에 코르카에 입사한 뒤 여러 사람들과 대화를 나누며 기웃거리고 있습니다. 원래는 저만의 AI 앱을 바로 만들기 시작하려고 했는데, 그보다는 기존 팀원들의 AI Transformation을 돕는 게 임팩트가 더욱 크겠다는 판단이 들었기 때문입니다.
어제도 기웃거리다가, 한 분이 코르카의 대표 서비스인 AI 논문 읽기 도우미 Moonlight 프로젝트에서 코딩하시는 게 눈에 띄었습니다. 그분 화면에 locale 관련된 파일이 보여서 현재 문라이트가 국제화(i18n)를 어떻게 처리하고 있는지 궁금해졌어요. 바이브 코딩 덕분에 i18n 작업이 엄청 수월해졌지만, 사실은 번역을 자동으로 해주는 거 빼면 AI가 정말 오류를 많이 내는 부분이 i18n이기도 하거든요.
그분과 대화를 나누며 어제 15분, 오늘 1시간 정도 페어작업을 하고, 이후 1시간 정도 더 들여서 문라이트 웹 버전의 i18n 안정성을 높이는 작업을 마무리해서 첫 PR을 올렸습니다. 코르카 엔지니어 분들께, 제가 레거시 프로젝트에 AI를 어떻게 활용하는지 좋은 예제를 드릴 수 있을 것 같아서 전체 과정과 프롬프트를 상세하게 기록했습니다. 기록한 게 아까워 글로 남기니, 이런 작업에 관심 있으시면 한번 읽어보세요 :) 앞으로 문라이트와 코르카의 발전도 기대해주시고요.
아래는 실제 PR Description을 일부만 수정한 것입니다.
주요 변경사항
- Codex를 위한
AGENTS.md
를 Codex에서/init
으로 생성 - i18n 키 일관성을 확보하기 위해
dictionaries.ts
타입 정의를 정비하고en.json
을 단일 소스로 관리 en.json
과 로케일별 사전을 비교하는 스크립트를 추가해 미사용 키와 누락 키를 정리하고, 동적으로 i18n key를 할당하는 FAQ는 예외 처리husky
pre-push 훅에서tsc
와lint:i18n:unused
를 실행해 타입 및 i18n 검증을 자동화
진행과정
제가 어떤 식으로 일하는지 보여드리기 위해 전체 과정을 기록했습니다. 저는 요즘은 이 문서를 커밋 안에 집어넣는 걸 선호하긴 합니다.
저는 보통 레거시(브라운필드) 프로젝트에서 + 나의 도메인/문제/코드 이해도가 부족한 상태일 때, 코딩 에이전트에게 질의하면서 현재 내 문제를 해결하기 위해 어떤 용어를 쓰고, 어떤 파일을 참조해야 하는지 물어봐서 프롬프트 하나를 만드는 걸 목적으로 첫번째 세션을 사용합니다.
그렇게 해서 문제가 잘 발견되면 (그리고 컨텍스트 윈도우가 어느정도 찼다면) 다음 세션에서 실제로 해결합니다. 보통 이전 맥락 전체가 없어도 문서화된 파일, 또는 스크립트의 실행 결과로 뭘 해야 하는지 명확하게 알 수 있다면 다음 세션으로 바로 넘어가는 편입니다.
참고사항
- 모든 구현은 Codex CLI를 이용해서
gpt-codex-medium
모델로 했습니다. - 세션 1까지는 **님과 함께 페어로 진행했습니다.
- 중간중간 직접 스크립트를 실행(
tsc
, linter 등)하고, diff가 정말 원하는대로 나오는지 등을 확인했습니다. - 위 "주요 변경사항"도 codex에게 아래 진행과정을 다 주고 적은 다음 살짝 수정했습니다.
세션 1 시작
Prompt 1
### 현재 상황
문라이트는 7개 언어에 대한 i18n을 제공하고 있음.
### 문제의식
i18n하면서 자주 생기는 문제로, i18n에서 [ko.json](packages/moonlight-web/src/app/[lang]/dictionaries/ko.json) 에만 key를 넣어놓고 en.json에는 안 넣어둔다거나, 아예 없는 key를 참조한다거나, 새 key를 추가하고 기존 key는 안 지운다거나 등이 있음. 그래서 이런 문제를 컴파일 타임에 방지하고 싶어서, 타입 정의를 사용하고 싶었음. 그래서 만든 게 [dictionaries.ts](packages/moonlight-web/src/app/[lang]/dictionaries.ts) 임.
### 기대 동작
이렇게 타입 정의를 해두면 i18n key의 사용부, 또는 i18n.json의 key 정의 이후에 dictionaries.ts에서 에러가 나길 바람
### 실제 동작
테스트를 위해 ko.json에서 `banner_title` key를 지워봤는데, 사용부( [page.tsx](packages/moonlight-web/src/app/[lang]/page.tsx) ) 든 dictionaries.ts에서든 에러가 나지 않음.
### 알고 싶은 것
1. 타입 정의만으로 내가 원하는 대로 사용부 또는 정의부에서 에러가 나게 하는 게 애초에 가능한가? 가능하면 사용부에서 나게 하고 싶다. 정의부에서 에러가 나면 어떤 key가 missing인지 등을 알기 어려우니까.
2. 해결책과 별개로, 타입스크립트를 더 잘 쓰고 싶은데, 기존에는 (사용부 에러든 정의부 에러든 간에) 에러가 나지 않았던 원인을 알고 싶다.
3. 타입 정의만으로 내가 원하는 바를 성취하기 어렵다면 pre-push hook 같은 걸로 스크립트를 돌려서 missing key를 찾아내는 것도 괜찮아 보이는데, 어떻게 생각하나? 이외에도 다른 방법을 제안해도 좋다.
AI 응답대로 해보니 바로 정의부/구현부에서 잘 됨
Prompt 2
네가 얘기한대로 [dictionaries.ts](packages/moonlight-web/src/app/[lang]/dictionaries.ts)를 변경했더니 원하는대로 사용부/정의부 모두 에러가 잘 나고 있어. 물론 en.json이 완벽한 source of truth라는 가정 하에 그렇긴 하지만, 훨씬 진전이 많이 됐어.
문제는 기존에
1. en.json에는 있는데 다른 곳에는 없거나
2. UI에서는 사용을 안하고 있는데 en.json에는 존재하는 key들이 있어.
이 문제들을 어떻게 해소하면 좋을지 (스크립트 등) 다양한 아이디어를 제안해줘.
내 우려사항은, tsc 돌린 결과물을 네가 보고 직접 고칠 수도 있겠지만 너무 시간도 오래 걸릴 것 같고 너무 많은 양의 텍스트가 인풋으로 들어갈 것 같아. 그렇다고 tsc 결과물을 파싱해서 보는 것도 쉽지 않을 것 같기도 하고.. 잘 모르겠다.
두 종류의 린터 스크립트를 제안받음. en.json에는 있는데 다른 곳에는 없는 거 찾기(keyset linter), 안쓰는 key 찾기(unused key detector).
Prompt 3
네 계획을 보니 그럴듯한데, 이런 순서로 하면 좋겠어.
1. moonlight-web에서 package.json 수준에서 tsc를 돌릴 수 있게 변경 필요. 지금 네가 제시한 pnpm tsc --pretty false --noEmit --project packages/moonlight-web/tsconfig.json를 호출하면 Command "tsc" not found.가 뜸.
2. keyset linter보다 unused key detector가 우선임. 안쓰는 key가 missing인 걸 개선할 필요 없으니까.
일단 1을 실행해주고, 2는 스크립트를 만들어줘.
스크립트 실행해보니 아주 여러개가 뜨고, 몇 개 직접 확인해보니 실제로 안쓰는 게 맞았음.
세션 2 시작
Prompt 4
lint:i18n:unused 를 실행해서 사용하지 않는 모든 key를 삭제해줘.
diff를 보니 en.json만 삭제되는 것이었음. 하나하나 모두 수동으로 검증하다 보니 동적으로 i18n key를 정의한 경우 false positive로 검출된다는 걸 확인.
Prompt 5
네가 제거해준 en.json의 diff를 모두 직접 검토해봤더니, 동적으로 key를 할당하는 경우에 사용하지 않는 걸로 판단하는 문제가 두 군데에서 있었어. 하나는 내가 SubscribeModal 코드 자체를 고쳤고, faq 쪽은 내가 다시 되살렸어.
아직 남아있는 faq 쪽은 스크립트에서 이걸 룰로 개선하긴 어려울 것 같아. 아주 마음엔 안 들지만 faq 쪽은 건너뛰도록 스크립트를 수정하면 좋겠는데 네 의견은 어때?
내 의견대로 ignoredPrefixes를 넣어줌
세션 3 시작
원래는 여기서 pre-push hook으로 바로 가려다가 좀 더 엄밀하게 하고 싶다고 생각해서 몇 번 롤백함
Prompt 6
find-unused-i18n-keys.js 를 수정해서, en.json에는 없는데 다른 json에 있는 key도 같이 표시되게 해줘.
그다음, 그 key들을 제거하는 스크립트를 구현해서 pnpm run 할 수 있게 추가해줘.
그리고 그 스크립트를 실행해서 그 key들을 제거해줘.
지켜보고 있다가 find-unused-i18n-keys.js가 수정된 시점에 직접 돌려봐서 뭐가 나오는지 확인.
이후 제거가 의도대로 잘 되어있음 확인
이후 직접 몇 개 key 추가해보고 삭제하는 것도 해봄. 잘 되는 것 확인
Prompt 7
en.json에 없는 key를 (동적으로 사용하는 게 아니라면) 썼다면 사용부/정의부(dictionaries.ts)에서 (= tsc 체크에서) 에러가 날 것이고, en.json에는 정의했는데 사용하지 않는 key가 있다면 lint:i18n:unused 에서 에러가 나올 거야. i18n key 관련 robustness를 챙기는 건 이걸로 충분할 것 같은데 네 생각은 어때? 새로 동적으로 key를 사용한다면 unused key detector가 잡아낼테니, ignoredPrefixes에 추가해야 한다는 걸 알 수 있을 거라고 생각해.
혹시 (낮은 확률이라도) 그 외에 생길 수 있는 다른 문제가 있다면 무엇일지 제안해줘.
생각해보니 ignoredPrefixes에 추가해야 할지도 모른다는 메시지를 unused 스크립트가 출력하는 것도 좋겠네.
AI 응답
> I agree: with the TypeScript checks wiring LangDict usage back to en.json, plus the unused-
key lint, you’ve covered most failure modes. The only gap I still see is when a key exists
in en.json but never gets localized in another dictionary; we currently fall back silently.
If that matters, a future follow-up could diff per-locale key sets and warn on missing
translations.
Script tweak – I added a hint when extra keys are detected so folks remember to extend the
ignore list if they’re legitimately generating keys dynamically (scripts/find-unused-i18n-
keys.js:364).
Other low-probability issues worth keeping in mind:
- Interpolator drift: mismatched placeholders (like {{name}} vs {{user}}) won’t surface in
these checks; a formatter-style validator would catch it.
- Runtime composition: if someone spreads multiple objects into a dictionary before export,
TypeScript still sees the structure but our flattener might miss freshly added prefixes—
worth auditing any meta-dictionary builders.
- Removal safety: the new cleaner assumes single-source en.json. If another canonical
locale is introduced later, we’ll want to generalize before pruning.
Prompt 8
좋아. 다른 문제는 일단 괜찮을 것 같은데, 오로지 en.json에만 추가되어 있고 다른 곳에는 없는 key가 있다면 이건 타입스크립트 외에도 알 수 있는 게 편하겠네. unused 스크립트가 이걸 잡아낼 수 있게 수정해줘. 스크립트 실행은 내가 직접 해볼거야.
실행해보고, key 일부러 지워보고 하면서 정상 동작 확인
Prompt 9
좋아. 이제 husky를 이용해 pre-push hook에서 tsc를 체크하고, lint:i18n:unused를 실행해서 둘 다 문제가 없을 때만 푸시가 가능하게 해줘.
이것도 직접 확인
마무리
이런 식으로, 앞으로도 종종 제가 어떻게 레거시 프로젝트를 개선하는지 기록을 남겨볼 예정입니다.
여담이지만 전체 과정을 gpt-5-codex 로 진행했는데 아주 마음에 들었습니다. 이 모델이 코딩을 정말 잘하고, 딱 요청한 작업만 똑똑하게 잘 한다는 인상을 받았습니다. 프롬프트를 정교하게 쓰는 사람들에게 특히 좋겠더군요. Cursor $20(Tab 자동완성을 위해) + Codex $20 이 가장 가성비 옵션이겠다는 생각이 들었습니다.
Member discussion