Codex CLI와 ESLint 래칫 패턴으로 레거시 코드베이스에 린트를 밀어 넣기
레거시 코드베이스에 린트를 붙여본 분들 계시죠? 저도 한번 겪었는데, eslint . 한 방에 에러 3,000개를 마주하고 멘탈이 무너졌던 기억이 납니다. 그 자리에서 터미널을 닫고 "다음에 하자"를 외쳤고, 결국 6개월이 지나도 린트는 없었습니다.
이 글은 그 상황을 피하는 방법에 관한 겁니다. 핵심은 두 가지입니다. 하나는 위반을 한 번에 다 고치려 하지 않고 래칫(ratchet) 패턴으로 점진적으로 강화하는 것, 다른 하나는 기계가 해결할 수 없는 복잡한 위반을 Codex CLI가 맡도록 역할을 분리하는 것입니다. 2026년 기준 ESLint가 .eslintrc 레거시 포맷을 제거하면서 flat config가 설정 체계의 유일한 입구가 됐고, Codex CLI를 CI나 훅에서 쓰는 사례도 많아진 시점이라 이 조합이 꽤 쓸만합니다.
먼저 짚을 것: ESLint 설정 체계의 현재
린트 도입 자체보다 ESLint 설정 체계의 현 상태를 아는 쪽이 선순위입니다. 설정 포맷을 잘못 고르면 뒤의 모든 단계가 흔들립니다.
flat config는 언제부터 기본이었나
flat config는 ESLint v9부터 기본 설정 포맷이 됐습니다. 즉 "flat config가 처음 표준이 된 시점"은 v10이 아니라 v9입니다. 그 뒤 메이저 릴리스에서 .eslintrc, .eslintrc.json, .eslintrc.js 같은 레거시 설정 파일 지원이 제거되는 수순을 밟았습니다. 2026년 시점에서 새 프로젝트를 시작하거나 레거시 프로젝트에 린트를 처음 붙인다면, 선택지는 사실상 flat config 하나입니다.
ESLint 각 메이저 버전의 지원 종료 일정은 프로젝트가 공식적으로 공지하는 범위를 그때그때 확인하는 쪽을 권합니다. 과거에 봤던 특정 날짜를 그대로 옮겨 쓰는 글들이 많은데, 정확한 EOL은 ESLint 저장소의 Version Support 문서를 참조하는 편이 안전합니다.
기존에 .eslintrc가 있다면 공식 마이그레이션 도구가 있습니다.
npx @eslint/migrate-config .eslintrc.jsonflat config의 모양은 이렇습니다.
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
rules: {
"no-unused-vars": "warn",
"no-console": "off",
},
},
];눈여겨볼 지점은 files 패턴을 설정 객체 안에 직접 쓴다는 점입니다. 점진적 도입에서 이 패턴 지정이 핵심 지렛대가 됩니다. 뒤에서 다시 등장합니다.
Codex CLI는 어디에 쓰는가
Codex CLI는 OpenAI가 공개한 터미널 기반 코딩 에이전트로, 자연어 명령으로 파일을 읽고 수정하고 실행할 수 있습니다. 승인(approval) 모드가 여러 단계로 나뉘어 있어 완전 수동 승인부터 자동 편집, 자동 실행까지 자율성 수준을 바꿀 수 있습니다. 플래그 이름과 서브커맨드는 버전에 따라 바뀌어 왔으므로, 아래 예시의 플래그는 설치 버전에서 codex --help로 확인해 쓰길 권합니다.
린트 작업에서 Codex CLI의 역할은 선명합니다. 스타일, 포매팅, 단순 패턴 치환처럼 기계적으로 끝나는 수정은 eslint --fix와 prettier --write가 처리하고, Codex CLI는 그걸로 해결되지 않는 쪽을 담당합니다. 예를 들어 사용 여부 판단에 코드 문맥이 필요한 no-unused-vars 위반, 의존성 구조가 얽힌 함수 분리 같은 작업입니다.
이렇게 나눠야 하는 이유가 있습니다. LLM에 스타일 수정을 통째로 맡기면 토큰이 낭비될뿐더러, 자동화된 치환 중에 논리 회귀가 섞여 들어올 위험도 생깁니다. 에이전트는 diff를 검토하고, 사람이 판단해야 할 복잡한 케이스만 다루는 쪽이 맞습니다.
래칫 패턴: 에러 3,000개와 싸우지 않는 방법
개념
래칫(ratchet)은 한 방향으로만 회전하는 톱니바퀴 메커니즘입니다. 코드 품질에 옮겨 적으면 이렇게 됩니다. 현재 위반 건수를 기준선(baseline)으로 저장해두고, 이후 커밋에서 건수가 늘어나면 빌드를 실패시킨다. 줄어들면 기준선을 자동으로 낮춘다.
기존 위반은 그대로 두되, 새 위반이 생기는 것만 막는 겁니다. 팀이 "지금 당장 3,000개를 다 지워야 한다"는 압박 없이 기존 코드를 조금씩 개선할 수 있습니다.
Notion 엔지니어링팀이 공유한 접근도 궤가 비슷합니다. 규칙은 전부 error로 설정하되, 커밋된 기준선과 비교해 기존 위반만 warning으로 다운그레이드하는 커스텀 ESLint 프로세서를 자체 구축했다고 합니다. 개발자들이 warn을 그냥 흘려보내는 경향이 있어 이 방식을 택했다는 설명이 인상적입니다.
기준선 파일 관리 방식
eslint-formatter-ratchet 같은 포매터 기반 접근으로 기준선을 자동 관리할 수 있습니다. 다만 ESLint 포매터는 기본적으로 stdout으로 결과를 뱉는 컴포넌트이므로, "실행만 하면 저절로 파일이 생긴다"가 아니라 출력을 파일로 리다이렉션해서 커밋 가능한 기준선으로 삼는 방식입니다. 설치와 사용 흐름은 다음 개념적 예시처럼 됩니다. 패키지의 실제 포매터 등록 이름과 CLI 옵션은 README를 확인한 후 쓰는 것을 권합니다.
npm install --save-dev eslint-formatter-ratchet# 최초 1회: 기준선 생성 (출력 리다이렉션으로 파일 생성)
npx eslint . --format eslint-formatter-ratchet > .eslint-ratchet.json
git add .eslint-ratchet.json && git commit -m "chore: 린트 기준선 커밋"핵심은 리다이렉션으로 저장된 기준선 파일을 저장소에 커밋해 공유 상태로 만든다는 점입니다. 이후 CI에서 동일한 명령을 돌리면, 포매터가 커밋된 기준선과 현재 위반을 비교해 증가 시 비정상 종료 코드를 반환합니다. 위반이 줄었을 때 기준선을 낮추는 쪽은 CI에서 파일을 갱신해 커밋/PR을 만드는 보조 단계로 두는 것이 보통입니다. "실행하면 저절로 낮아진다"고 가정하지 말고, 어떤 조건에서 어떤 파일이 어떻게 갱신되는지 팀 내에서 명시해두는 쪽이 안전합니다.
단계적 규칙 승격
래칫 패턴과 짝지어 쓰기 좋은 접근은 off → warn → error 순서로 규칙을 밀어 올리는 것입니다. 다만 승격 조건을 "전사 위반 0 달성"으로 두면 사실상 한발짝도 못 나갑니다. 그러면 래칫을 쓰는 의미가 없죠. 현실적인 승격 기준은 다음과 비슷하게 둡니다.
off → warn: 규칙이--fix로 자동 해결 가능한지 확인하고, 몇 개의 샘플 디렉터리에서 위반 분포를 파악했을 때warn → error: 신규 코드 범위(files패턴으로 좁힌 영역)에서 위반 0이 일정 기간 유지되고, 기존 코드 위반이 기준선 아래로 안정화됐을 때- 전사
error적용: 래칫으로 측정한 위반 건수가 수용 가능한 임계치 이하로 내려왔을 때
// eslint.config.js - 신규 디렉터리만 엄격하게 적용하는 예
export default [
{
files: ["src/**/*.js", "src/**/*.ts"],
rules: {
"no-unused-vars": "warn",
"eqeqeq": "warn",
},
},
{
files: ["src/features/new/**/*.ts"],
rules: {
"no-unused-vars": "error",
"eqeqeq": "error",
"@typescript-eslint/no-explicit-any": "error",
},
},
];flat config의 files 패턴 덕분에 "기존 코드엔 warn, 신규 코드엔 error"가 한 파일 안에서 깔끔하게 공존합니다. 팀이 새 코드를 짤 때부터 좋은 습관을 들이게 하는 쪽이 전체 전환 비용을 가장 많이 떨어뜨리는 지점이기도 합니다.
Codex CLI 실전 설정
AGENTS.md의 역할과 적용 시점
AGENTS.md는 Codex CLI에게 이 프로젝트에서 어떻게 행동해야 하는지 알려주는 지침 파일입니다. 중요한 점은 AGENTS.md에 적힌 작업이 매 커밋마다 자동으로 발동되지는 않는다는 것입니다. Husky + lint-staged가 모든 커밋에서 돌리는 eslint --fix와, 사람이 명시적으로 Codex CLI를 호출해 돌리는 "복잡한 위반 정리 세션"은 다른 레이어입니다. AGENTS.md가 작동하는 시점은 바로 그 명시적 세션 쪽입니다.
# AGENTS.md
## 린트 강화 세션 지침 (사람이 명시적으로 Codex CLI를 호출했을 때만)
### 전제
- 커밋 훅에서 돌아가는 eslint --fix와 prettier는 이미 통과한 상태라고 가정한다.
- 이 세션의 입력은 '자동 수정으로 해결되지 않은 위반 목록'이다.
### 실행 순서
1. 자동 수정이 끝난 상태에서 ESLint를 한 번 더 돌려 남은 위반 목록을 받는다.
2. 위반을 파일별로 분류한다.
3. no-unused-vars 위반은 사용 여부를 코드 문맥으로 판단한 뒤 수정한다.
4. 수정 후 기존 테스트가 통과하는지 확인한다.
5. 한 번에 최대 20개 파일만 처리한다.
### 금지 사항
- 테스트 없이 비즈니스 로직을 수정하지 않는다.
- 타입 정의를 임의로 any로 변경하지 않는다.
- 기존 주석을 삭제하지 않는다.1번 지침에서 "자동 수정 후 남은 위반을 받는다"로 표현을 바꿔두면, Husky + lint-staged가 하는 일과 Codex CLI 세션이 하는 일이 겹치지 않습니다. 이 세션은 보통 스프린트 중 "린트 빚 갚는 날" 같은 블록을 잡아 사람이 수동으로 트리거하는 흐름이 자연스럽습니다.
Codex CLI 호출 예시
대화형으로 쓸 때는 자연어 프롬프트를 그대로 넘기면 됩니다. Codex CLI에 어떤 언어로 지시해야 결과 품질이 가장 좋은지는 모델 특성과 저장소 톤에 따라 다르므로, 영어 프롬프트를 기본으로 두고 한국어 프롬프트는 팀 저장소에서 실제로 돌려본 뒤 품질을 확인해 쓰는 쪽을 권합니다.
# 영어 프롬프트 예시
codex "Clean up remaining no-unused-vars violations in src/legacy per AGENTS.md. Tests must pass."# 한국어 프롬프트도 가능하나, 저장소에서 품질을 먼저 검증할 것
codex "src/legacy 아래 no-unused-vars 위반을 AGENTS.md 지침대로 정리해줘. 테스트는 통과해야 해."승인 모드 플래그와 비대화형 실행 방식은 Codex CLI 버전에 따라 변경이 잦습니다. 설치된 버전에서 codex --help로 승인 모드 플래그와, 비대화형 실행 지원 여부(예: 승인 자동화 플래그, 입력 파이프 사용 등)를 먼저 확인하고 CI 스크립트를 짜는 것을 권합니다. 지원되지 않는 서브커맨드를 CI YAML에 박아두면 조용히 깨집니다.
커밋 단계 자동화: lint-staged + Husky
사람 손으로 매번 돌리긴 번거로우니, 커밋 시 자동으로 돌아가게 묶어두는 쪽이 편합니다.
npm install --save-dev husky lint-staged
npx husky init// package.json
{
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}# .husky/pre-commit
npx lint-stagedlint-staged는 스테이징된 파일만 걸러서 eslint --fix와 prettier --write를 돌리고, 수정된 결과를 자동으로 다시 스테이징해 같은 커밋에 포함시킵니다. 사람이 git add를 다시 할 필요가 없습니다. 에러가 남으면 커밋 자체가 차단됩니다.
주의할 지점이 하나 있습니다. Husky 훅은 git commit --no-verify로 우회할 수 있습니다. 바쁠 때 이걸 쓰면 린트 체크가 통째로 건너뛰어집니다. 그래서 CI에서도 동일한 린트 검사를 반드시 다시 실행해야 합니다. 로컬 훅은 편의, CI는 방어선이라는 역할 분담입니다.
# .github/workflows/lint.yml (개념적 예시)
name: Lint Check
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npx eslint . --format eslint-formatter-ratchet > .eslint-ratchet.current.json
- run: node scripts/compare-ratchet.js .eslint-ratchet.json .eslint-ratchet.current.json위 YAML에서 비교 스크립트 부분은 팀 상황에 맞게 포매터가 제공하는 종료 코드/비교 CLI를 활용하거나 자체 스크립트로 치환합니다. 핵심은 커밋된 기준선 파일을 CI가 참조한다는 구조 자체입니다.
전체 흐름 한눈에 보기
트레이드오프: 솔직한 장단점
처음엔 "그냥 다 자동화하면 되지"라고 생각하기 쉬운데, 실제로 해보면 각 선택지마다 비용이 붙습니다.
| 항목 | 장점 | 주의사항 |
|---|---|---|
| 점진적 도입 | 팀 부담 없이 기존 코드를 유지하면서 진행 | 기준선 파일 관리 번거로움 |
| LLM 직접 수정 | 복잡한 리팩터링 가능 | 논리 회귀 위험, 토큰 비용 |
| 래칫 패턴 | 코드 품질 후퇴를 구조적으로 차단 | CI 설정 복잡도 증가 |
| lint-staged | 커밋 전 자동 수정으로 흐름 방해 최소화 | --no-verify 우회 가능, CI 중복 필요 |
| flat config | 2026년 기준 사실상 유일한 선택지 | 일부 플러그인의 호환성 확인 필요 |
흔히 하는 실수들
flat config에서 files 패턴 없이 새 규칙 추가하기
생각보다 자주 벌어집니다. flat config에서 files 없이 규칙 객체를 넣으면 전체 코드베이스에 즉시 적용됩니다. 배포 직전에 하면 에러가 폭발합니다. 신규 규칙은 반드시 files로 범위를 좁혀 추가하는 것이 좋습니다.
LLM에게 스타일 수정을 직접 맡기기
eslint --fix로 끝날 일을 Codex CLI에 던지면 토큰이 쌓입니다. 역할 분리가 비용 관점에서도 맞는 선택입니다.
최신 메이저 ESLint로 올리기 전 플러그인 호환성 미확인
eslint-plugin-react, eslint-config-next 같은 플러그인은 메이저 전환에 따라 호환성이 늦게 따라오는 경우가 있습니다. 메이저 업그레이드 전에 사용 중인 플러그인의 호환 범위를 각 패키지 릴리스 노트에서 먼저 확인하는 것이 좋습니다.
어디서부터 시작하면 좋을까
레거시 코드베이스에 린트를 도입할 때 가장 자주 실패하는 지점은 "전부 한 번에 해치우려는 욕심"입니다. 이 글에서 다룬 도구들을 한꺼번에 전부 세팅하려고 하면 PR이 거대해지고, 리뷰가 지체되고, 결국 머지되지 못한 채 묻힙니다. 임팩트와 리스크를 함께 보면 투자 순서는 꽤 분명합니다.
가장 먼저 할 일은 래칫 기준선을 커밋해두는 것입니다. 이것만 해두면 "지금 이 순간 이후부터 새로 생기는 위반은 막힌다"는 안전망이 즉시 생깁니다. 규칙을 바꾸지도, 기존 코드를 손대지도 않으므로 리스크가 거의 없고, PR도 작습니다. 팀 설득 비용이 가장 낮은 단계입니다.
그다음이 신규 코드 디렉터리에 flat config의 files 패턴으로 엄격 규칙 영역을 분리하는 작업입니다. 새로 짜는 코드부터 깨끗해지면, 자연히 "오염된 코드 비율"이 커밋 단위로 떨어지기 시작합니다. 여기까지가 1~2주 내에 끝낼 수 있는 1차 투자입니다.
그 뒤에 Husky + lint-staged로 커밋 자동 수정, 마지막으로 Codex CLI 세션으로 복잡한 위반을 블록 단위로 소화하는 흐름을 얹습니다. Codex CLI를 가장 마지막에 두는 이유는, 앞 단계가 깔려 있지 않으면 에이전트가 처리해야 할 범위가 지나치게 넓어져 세션 품질이 급락하기 때문입니다. 자동 수정으로 충분히 걸러진 뒤에 남은 "진짜 복잡한 것들"만 넘겨야 에이전트가 유효하게 일합니다.
레거시 코드 린트 도입은 "완벽한 상태로 도달하는 프로젝트"가 아니라 "품질이 뒤로 가지 않는 체계를 세우는 프로젝트"에 가깝습니다. 기준선을 그어두면, 나머지는 시간이 해줍니다.
참고 자료
- ESLint 공식 블로그 - Flat Config 롤아웃 계획
- ESLint 공식 블로그 - Configuration Migrator
- ESLint 공식 블로그 - 2025 연간 리뷰
- ESLint Version Support 정책 (저장소 문서)
- Notion 엔지니어링 블로그 - 커스텀 ESLint 규칙으로 코드 진화시키기
- eslint-formatter-ratchet (npm)
- Mainmatter 블로그 - lint-to-the-future로 점진적 린트 도입 — 래칫과 결이 비슷한 또 다른 점진적 린트 도입 전략을 소개합니다. 이 글에서는 포매터 기반 래칫을 택했지만, 규칙별 off-list를 코드로 관리하는 방식이 더 맞는 팀도 있습니다.
- OpenAI Codex CLI GitHub 저장소
- lint-staged 공식 문서
- Husky 공식 문서