`any` 300개 코드베이스를 Codex CLI와 AGENTS.md로 단계별 해체하기
TypeScript strict 마이그레이션을 미뤄둔 팀이라면 한 번쯤 겪었을 겁니다. tsc --strict --noEmit을 처음 돌린 날, 터미널이 빨간색으로 가득 차고 오류 개수가 스크롤 끝까지 이어지는 그 광경. 저도 재작년에 20만 줄 규모의 Node.js + React 모노레포 마이그레이션을 맡았을 때 첫날 터미널 창을 닫아버린 적이 있어요. 시작점을 못 찾겠던 거죠.
이 글은 그 상황에서 찾은 탈출구에 대한 이야기입니다. Codex CLI의 codex exec 커맨드와 AGENTS.md를 조합하면, 수백 개의 any를 의미론적으로 올바른 타입으로 교체하는 반복 작업을 AI 에이전트에게 위임할 수 있습니다. 핵심은 한 번에 다 고치려는 욕심을 버리고, 컴파일러 플래그 하나씩 활성화하는 단계별 커밋 전략을 에이전트가 그대로 따르게 만드는 것입니다.
전제를 두 가지만 짚어두겠습니다. Codex CLI는 2025년 4월 OpenAI가 오픈소스로 공개한 터미널 기반 코딩 에이전트입니다. AGENTS.md는 여러 AI 코딩 도구가 공통으로 참조할 수 있도록 제안된 프로젝트 컨텍스트 파일 규약입니다. 표준화 거버넌스 주체에 대해서는 공개된 자료에서 명확히 확인되지 않으므로, 이 글에서는 "여러 도구가 참조 가능한 규약"이라는 실용적 관점에서만 다룹니다.
왜 지금 이 조합이 필요한가
TypeScript 6/7이 예고하는 변화
2026년 9월 기준으로 TypeScript 6.0은 아직 정식 릴리스되지 않았습니다. Microsoft는 별도 프로젝트로 Go 언어로 재작성한 네이티브 컴파일러(코드명 Corsa)를 TypeScript 7.0 목표로 개발 중이라고 공개했고, 이 프로젝트는 빌드 성능 향상을 주된 목표로 삼고 있습니다. 구체적인 배수(예: N배 빠름)는 아직 최종 벤치마크로 확정된 수치가 없으므로, 이 글에서는 "네이티브 컴파일러 전환이 예정되어 있다"까지만 사실로 다룹니다.
또한 "strict가 6.0에서 기본값이 된다"는 것은 커뮤니티의 예측이지 공식 로드맵에 명시된 사항은 아닙니다. 다만 현실적으로 확실한 것은 하나 있습니다. 신규로 tsc --init을 실행하면 수년 전부터 이미 strict: true가 기본으로 활성화된 tsconfig.json이 생성됩니다. 신규 프로젝트와 레거시 프로젝트의 표준이 다른 상태가 계속 유지되고 있고, 이 간극을 좁혀두는 편이 채용·라이브러리 호환성·미래 업그레이드 관점에서 유리합니다.
즉, "언젠가 하면 되겠지"에서 "미뤄두면 신규 인력이 합류할 때마다 이질감으로 돌아오고, 서드파티 타입 업데이트를 따라가기 어려워진다"로 상황이 바뀌었다고 보는 편이 정확합니다.
대규모 코드베이스에서 수작업이 막히는 이유
Airbnb 엔지니어링 블로그에 따르면, 팀은 대규모 JavaScript 코드베이스를 TypeScript로 옮기기 위해 ts-migrate라는 자동화 도구를 별도로 만들어야 했습니다. 파일 하나씩 수동 전환하는 방식으로는 엔지니어 시간이 타입 개선이 아니라 반복 편집에 소진되기 때문입니다.
여기서 얻을 수 있는 실용적 결론은 하나입니다. 결정론적 변환은 codemod(예: jscodeshift)로, 문맥적 판단은 AI 에이전트로 나눠야 사람이 감당 가능한 리뷰 부하 안에 들어옵니다. 두 도구가 각각 어느 정도 비율을 담당하는지에 대한 검증된 통계는 없지만, 분업 자체는 다수의 마이그레이션 사례가 공유하는 실용적 패턴입니다.
한 가지 짚어둘 점은, 이 글의 codemod 언급은 "결정론적 변환 층이 필요하다"는 아키텍처 관점의 설명입니다. 어떤 codemod를 쓸지는 프로젝트마다 다릅니다. React props 리네임이라면 jscodeshift 기반 스크립트, import 정리라면 ts-morph 스크립트가 흔한 선택지입니다.
AGENTS.md — 에이전트에게 전달하는 단 하나의 진실
AGENTS.md의 핵심 가치는 단순합니다. 팀원과 AI 에이전트가 동일한 컨벤션을 동일한 파일에서 참조한다는 것입니다. Codex CLI는 이 파일을 공식적으로 읽어 컨텍스트로 사용합니다. 다른 도구들(Claude Code, Cursor 등)의 지원 여부는 도구·버전마다 다르므로, 도입 전 각 도구의 최신 문서에서 확인하는 편이 안전합니다. Claude Code의 CLAUDE.md, Cursor의 .cursorrules처럼 도구별 파일이 별도로 존재하며, AGENTS.md와의 상호 폴백 동작을 사실처럼 단정하지 않는 편이 좋습니다.
최소 예시 (권장)
먼저 실제로 유지 관리할 만한 짧은 버전입니다. 이 정도 분량이 리뷰와 수정 부담이 적습니다.
# AGENTS.md
## 컨텍스트
Node.js + React 모노레포, 약 20만 줄. TypeScript strict 마이그레이션 Phase 2.
## 타입 규칙
- `any` 금지. 불가피하면 `unknown` 후 narrowing.
- `as` 캐스팅 대신 type guard.
- 함수 시그니처의 반환 타입은 명시.
## 검증
- 수정 후 `tsc --noEmit` 종료 코드 0.
- `pnpm test --changed` 통과.
## 커밋
- 모듈 단위 PR. 메시지: `feat(types): remove any from <module>`.확장이 필요할 때
컨벤션이 늘어나면 파일이 커지는 게 자연스럽지만, 컨텍스트 창을 계속 차지한다는 사실은 잊지 않는 게 좋습니다. 임계값에 대한 검증된 수치는 없지만, 경험적으로는 한 화면(약 100줄) 안에서 스캔 가능한 분량을 유지하는 편이 에이전트가 규칙을 놓치지 않았습니다. 프로젝트별 예외 규정이나 도메인 용어집처럼 길어지는 부분은 별도 문서로 분리하고 AGENTS.md에서 링크로 참조하는 방식이 실용적입니다.
단계별 커밋 전략 — 한 번에 다 켜면 안 되는 이유
strict: true를 프로젝트 루트 tsconfig.json에 한 줄 추가하는 건 1초면 됩니다. 그러나 그 직후 수백~수천 개의 오류가 동시에 터집니다. 이 상태에서는 에이전트도, 사람도, 어디서부터 시작해야 할지 알 수 없습니다.
권장하는 순서는 다음과 같습니다.
각 단계마다 tsc --noEmit이 종료 코드 0을 반환하는 걸 확인한 뒤 커밋합니다. 리뷰어 입장에서는 "이 PR은 noImplicitAny 오류만 제거한 것"이라는 맥락이 명확해서 검토 시간이 대폭 줄어듭니다.
tsconfig.json 단계별 설정
1단계 — JS 파일과 공존:
이 단계의 목적은 타입 오류를 잡는 게 아닙니다. JS 파일을 삭제하지 않고 그대로 둔 채, .ts로 확장자를 옮긴 파일만 컴파일 대상에 편입하는 게 목표입니다. checkJs: false로 두면 JS 파일에는 여전히 아무 검사도 하지 않으므로, 팀은 기능 개발을 멈추지 않고 파일 단위 이관을 시작할 수 있습니다.
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"target": "ES2022",
"module": "NodeNext"
}
}2단계 — 암묵적 any 차단:
이 시점부터 .ts 파일에서 타입 주석 없는 파라미터가 오류를 냅니다. 명시적으로 : any를 쓴 코드는 아직 통과합니다.
{
"compilerOptions": {
"allowJs": true,
"noImplicitAny": true
}
}3단계 — null 안전성 추가:
undefined | null 관련 오류가 대량으로 노출되는 단계입니다. 여기서 옵셔널 체이닝(?.)과 nullish 병합(??)을 대규모로 적용하게 됩니다.
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}최종 단계 — 전체 strict:
{
"compilerOptions": {
"strict": true
}
}Codex CLI로 자동화 루프 구성하기
기본 실행 패턴
git checkout -b migrate/remove-implicit-any
codex exec "src/services/ 디렉터리 내 TypeScript 파일의 noImplicitAny 오류를 수정해줘.
제약:
- as 캐스팅 및 명시적 any 사용 금지
- 리프 모듈(의존성 없는 파일)부터 시작해 의존성 순서대로 진행
- 각 파일 수정 후 tsc --noEmit 실행, 종료 코드 0 확인
- 변경된 파일마다 무엇을 바꿨는지 한 줄로 요약"codex exec는 실제 파일을 수정하므로 반드시 클린 워킹 트리에서, 피처 브랜치 위에서 실행하는 편이 안전합니다. Codex CLI는 실행 시 승인 모드(approval mode)와 샌드박스 정책 옵션을 제공하므로, 처음 시도할 때는 파일 쓰기 전에 사람이 확인하도록 승인 모드를 활성화하는 방법을 권장합니다. 구체적인 플래그명·기본값은 버전에 따라 바뀌므로 codex exec --help로 현재 옵션을 확인하는 게 확실합니다.
에이전트가 돌리는 검증 루프
에이전트가 자율적으로 수행하는 피드백 루프는 다음과 같습니다.
여기서 중요한 건 컴파일 성공만으로 끝내지 않고 테스트 실행까지 루프에 포함시키는 것입니다. 이유는 뒤에서 실패 사례로 다룹니다.
서드파티 타입이 없는 라이브러리 처리
@types/* 패키지가 없거나 자체 .d.ts를 제공하지 않는 라이브러리를 만나면 에이전트도 막힙니다. 이런 경우를 위해 AGENTS.md에 정책을 명시하거나, 별도의 types/ 디렉터리에 앰비언트 선언을 두는 방식이 실용적입니다. 아래는 개념적 예시입니다(실제 패키지가 아님).
// types/legacy-analytics.d.ts (개념적 예시)
declare module 'legacy-analytics-sdk' {
export interface TrackOptions {
event: string;
properties?: Record<string, unknown>;
}
export function track(options: TrackOptions): void;
}ESLint로 잔존 any 추적
Codex가 작업한 뒤에도 빠져나간 any가 있을 수 있습니다. typescript-eslint 규칙으로 CI에서 걸러낼 수 있습니다. 2026년 기준으로는 ESLint v9 이상이 흔하므로 flat config 형식을 예시로 씁니다.
// eslint.config.js
import tseslint from 'typescript-eslint';
export default tseslint.config(
...tseslint.configs.recommendedTypeChecked,
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
},
},
);ESLint v8까지 쓰는 프로젝트라면 기존 .eslintrc.json에 동일 규칙을 넣으면 됩니다.
파일별 점진 전환 — @ts-nocheck와 커뮤니티 도구
팀 전체가 기능 개발을 병행하면서 마이그레이션해야 한다면, 파일별 점진 전환이 현실적인 선택입니다. TypeScript 컴파일러가 공식으로 인식하는 지시어는 // @ts-nocheck로, 해당 파일의 타입 검사를 통째로 스킵합니다.
Angular 커뮤니티에서 흔히 언급되는 // @ts-strict-ignore는 TypeScript 네이티브 지시어가 아니라 별도 도구(예: ts-strict-plugin)가 있어야 동작하는 마커입니다. 도입하려면 해당 플러그인을 설치하고 파일별 strict 규칙을 관리해야 합니다. 아무 설정 없이 이 주석만 붙이면 그냥 무시되는 일반 주석이 됩니다.
간단하게는 @ts-nocheck로 시작한 뒤, 남은 파일 수를 CI에서 카운트하는 게이팅을 걸 수 있습니다.
IGNORE_COUNT=$(grep -rl "@ts-nocheck" src/ | wc -l)
echo "미전환 파일: $IGNORE_COUNT개"
if [ "$IGNORE_COUNT" -gt 50 ]; then
echo "마이그레이션 속도가 목표 아래입니다"
exit 1
fi트레이드오프 — 솔직한 평가
| 항목 | 장점 | 고려사항 |
|---|---|---|
| 에이전트 자동화 | 반복적인 타입 추론·수정 작업을 위임 가능 | AI가 의미론적으로 잘못된 타입을 추론할 수 있음. 테스트 실행이 루프에 포함되어야 함 |
| 단계별 커밋 전략 | 모듈 단위 PR로 리뷰 비용 최소화, 기능 개발 병행 가능 | 마이그레이션 브랜치와 메인 브랜치 간 merge conflict 관리 필요 |
| AGENTS.md | 팀원과 에이전트가 동일 컨벤션 참조 | 파일이 커질수록 컨텍스트 비용 증가. 핵심만 유지 |
tsc --noEmit 루프 |
명확한 완료 기준, CI 통합 용이 | 대형 코드베이스에서 전체 검사 시간이 길어질 수 있음 |
@ts-nocheck 파일 마킹 |
팀 전체 참여 구조, 점진 전환 가능 | 마커가 방치될 경우 기술 부채로 전환 |
에이전트가 조용히 실패하는 순간 — 실제 사례
이 접근의 가장 큰 함정은 에이전트가 컴파일러를 통과시키기 위해 타입을 헐겁게 만들어버리는 경우입니다. 제가 마이그레이션 중에 마주친 실제 패턴 하나를 축약해서 옮기면 이렇습니다.
Before(수정 전, 암묵적 any):
export function mergeConfig(base, override) {
return { ...base, ...override };
}에이전트가 만든 After(컴파일은 통과, 하지만 타입은 무의미):
export function mergeConfig(base: unknown, override: unknown): unknown {
return { ...(base as object), ...(override as object) };
}이 코드는 noImplicitAny를 통과합니다. 하지만 호출부에서 반환값의 프로퍼티에 접근하는 순간 다시 오류가 터지거나, 새 as 캐스팅을 낳습니다. AGENTS.md의 "as 금지" 규칙과 정면으로 충돌하지만, 에이전트가 급하면 이런 지름길을 선택합니다. 사람이 원했던 것은 이런 형태입니다.
export function mergeConfig<T extends object, U extends object>(
base: T,
override: U,
): T & U {
return { ...base, ...override };
}이런 경우를 잡아내려면 tsc 통과만으로는 부족하고, 호출부까지 포함한 테스트 실행과 no-unsafe-* 계열 ESLint 규칙이 루프 안에 함께 있어야 합니다. 특히 복잡한 제네릭·콜백 체인·이벤트 핸들러 시그니처에서 에이전트가 unknown으로 도피하는 패턴은 리뷰 시 우선순위를 두고 봐야 합니다.
마무리 — 다음에 무엇을 할지
이 글의 워크플로를 실제로 도입해보려면, 저라면 아래 순서로 시작하겠습니다.
- 현재 코드베이스에서
grep -rn ": any" src/ | wc -l과tsc --strict --noEmit 2>&1 | wc -l을 돌려 시작점을 숫자로 기록해두기. 이 수치가 감소하는 게 유일한 진행 지표입니다. - AGENTS.md를 앞의 최소 예시 수준으로 짧게 작성하고, 팀 리뷰를 한 번 받기. 사람이 이해 못 하는 규칙은 에이전트도 못 지킵니다.
- 가장 의존성이 적은 유틸리티 디렉터리 하나만 골라
codex exec를 승인 모드로 돌려보기. 여기서 나온 diff를 팀 채널에 공유하면 다음 논의가 훨씬 쉬워집니다.
에이전트에게 마이그레이션을 통째로 맡기는 것도, 사람이 전부 수동으로 하는 것도 최적해가 아닙니다. 컴파일러·테스트·린터라는 결정론적 검증기를 사이에 두고, 에이전트가 반복 작업을 하고 사람이 모호한 타입 결정과 실패 사례 리뷰에 집중하는 분업이, 지금 시점에서 감당 가능한 접근이라고 느낍니다.