Codex CLI로 콜백 코드를 async/await로 옮길 때 AGENTS.md와 diff 루프로 안전망 만들기
레거시 Node.js 프로젝트를 넘겨받았을 때 가장 먼저 마주치는 건 콜백 지옥이다. 콜백 안에 콜백, 그 안에 또 콜백. 에러 핸들링이 각 단계마다 제각각이고, 스택 트레이스는 어디서 터진 건지 단서조차 주지 않는다. 2026년 기준으로도 async/await는 신규 Node.js 코드에서 가장 널리 쓰이는 비동기 작성 방식이지만, Promise 체인이나 스트림 기반 처리는 여전히 유효한 도구다. 다만 콜백 중심 코드는 유지보수 관점에서 부담이 커서 하나씩 손으로 고치는 건 고역이다. Codex CLI를 쓰면 이 작업을 자동화할 수 있는데, 핵심은 에이전트가 건드려도 되는 범위를 AGENTS.md로 명확히 고정하는 것이다.
먼저 필자의 시행착오를 하나 공유하자면, 단순히 "이 파일들 async/await로 바꿔줘"라고 던지면 Codex가 너무 많이 손댄다. package.json에 새 의존성을 추가하거나, 건드릴 생각 없던 테스트 파일까지 수정하거나, 병렬로 돌아야 할 비동기 로직을 직렬 await 체인으로 단순화해버린다. 처음 Full Auto 모드로 실행했다가 test/ 디렉토리 전체가 바뀌어 있는 걸 보고 식은땀을 흘렸던 기억이 있다. AGENTS.md로 제약 경계를 설계하고, diff와 review 단계를 반복해 각 변경을 검증하는 워크플로우가 이 문제를 해결한다.
이 글에서는 실제 레거시 콜백 코드를 Codex CLI로 변환하는 과정을 따라가면서, AGENTS.md 작성법과 diff 리뷰 루프를 구체적으로 살펴본다. util.promisify가 커버하는 범위와 커버하지 못하는 범위도 함께 다룬다. 이후 등장하는 필자 경험담은 모두 1인칭으로 표기한다.
왜 지금 이 변환이 중요한가
콜백 지옥의 실제 비용
중첩 단계가 세 개만 돼도 코드가 어떻게 달라지는지 바로 보인다. 아래 예시는 특정 DB 라이브러리를 전제하지 않은 개념적 코드다.
// 콜백 지옥의 전형적인 모습 (개념적 예시)
function getUserData(userId, callback) {
db.getUserById(userId, (err, user) => {
if (err) return callback(err);
fs.readFile(`./profiles/${user.id}.json`, 'utf8', (err, profileData) => {
if (err) return callback(err);
cache.set(`user:${userId}`, profileData, (err) => {
if (err) return callback(err);
callback(null, { user, profile: JSON.parse(profileData) });
});
});
});
}// async/await로 변환한 결과 (개념적 예시)
async function getUserData(userId) {
const user = await db.getUserById(userId);
const profileData = await fs.promises.readFile(`./profiles/${user.id}.json`, 'utf8');
await cache.set(`user:${userId}`, profileData);
return { user, profile: JSON.parse(profileData) };
}참고로 mysql2, pg 같은 실제 라이브러리에서 query()는 대체로 rows 배열을 반환하므로 const [rows] = await db.query(...) 형태로 구조 분해해서 받아야 한다. 위 예시는 흐름을 보여주기 위한 단순화된 코드다.
로직 흐름이 눈에 들어오고, try/catch 하나로 전체 에러를 잡을 수 있다. 스택 트레이스도 의미 있는 형태로 출력되어 원인 파악이 빨라진다.
util.promisify가 자동화의 출발점
Node.js 8부터 도입된 util.promisify()는 표준 error-first 콜백 패턴—마지막 인수가 (err, result) 형태인 함수—을 Promise 반환 함수로 감싸준다.
const util = require('util');
const fs = require('fs');
// util.promisify 적용 전
fs.readFile('./data.json', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// util.promisify 적용 후
const readFile = util.promisify(fs.readFile);
async function loadData() {
const data = await readFile('./data.json', 'utf8');
console.log(data);
}단, util.promisify()는 콜백이 마지막 인자이고 첫 번째 콜백 인자가 에러 객체인 표준 패턴만 지원한다. 비표준 시그니처를 가진 함수는 util.promisify.custom 심볼로 별도 처리해야 한다. 이 경계를 Codex에게 명확히 알려주는 게 AGENTS.md의 역할 중 하나다.
변환 전략 선택: Codex 단독인가, codemod와 조합인가
대규모 코드베이스라면 jscodeshift 같은 AST 기반 codemod 도구와 Codex를 함께 쓰는 하이브리드 전략이 효과적일 수 있다. 패턴이 명확한 기계적 변환은 codemod로 먼저 처리하고, 의미적 판단이 필요한 부분—병렬 처리 여부, 에러 전파 방식 변경—은 Codex CLI로 처리하는 방식이다. Mocha 테스트의 done 콜백을 async 함수로 옮기는 작업처럼 형태가 일정한 변환은 codemod가 유리하다는 사례 보고가 있다(참고 자료 항목 참조).
반대로 함수마다 콜백 시그니처가 제각각이거나 병렬/직렬 판단이 코드마다 다르다면, 처음부터 Codex로 다루는 편이 낫다. 프로젝트를 훑어보고 패턴이 균일한지 아닌지부터 판단하고 전략을 정하는 게 좋다.
AGENTS.md 제약 설계
에이전트에게 경계를 알려주는 방법
AGENTS.md는 저장소 루트에 두는 Markdown 파일로, Codex CLI가 이 파일을 읽고 프로젝트 구조, 명령어, 그리고 "항상 해야 할 것 / 먼저 물어볼 것 / 절대 하지 말 것"을 파악한다. 공식적으로 표준 스펙으로 확정된 규격이라기보다 Codex CLI가 채택한 관례에 가깝고, 다른 도구들의 지원 여부는 각 도구의 공식 문서에서 개별 확인하는 편이 안전하다.
콜백 변환 작업에 특화된 AGENTS.md는 이렇게 작성할 수 있다.
# 프로젝트 컨텍스트
Node.js 16 기반 레거시 이커머스 백엔드. 현재 콜백 기반 비동기 코드를 async/await로 마이그레이션 중.
## 변환 범위
### Always (항상 적용)
- src/legacy/** 하위 파일만 수정
- error-first 콜백은 util.promisify로 래핑하거나 직접 async 함수로 변환
- 변환 후 반드시 `npm test` 통과 확인
- 변경된 함수의 JSDoc 반환 타입을 Promise<T>로 업데이트
### Ask First (먼저 확인)
- util.promisify 적용이 불명확한 비표준 콜백 시그니처 발견 시
- 병렬 실행 의도가 의심되는 콜백 패턴 발견 시 (Promise.all 전환 여부 확인)
- 변환 대상 함수의 호출자가 src/legacy/ 외부에 있을 때
### Never (절대 금지)
- test/ 디렉토리 직접 수정
- package.json 의존성 추가 또는 수정
- src/legacy/ 외부 파일 수정
- 콜백 에러를 명시적으로 무시하던 패턴을 빈 catch 블록으로 변환
## 비표준 콜백 목록 (수동 처리 필요)
- src/legacy/queue/processor.js — onComplete(result, err) 순서 비표준
- src/legacy/vendor/oldLib.js — 콜백이 두 번째 인자
## 실행 명령
- 테스트: npm test
- 린트: npm run lint파일이 지나치게 커지면 컨텍스트에서 잘려나갈 위험이 있으므로, 실무적으로는 항목당 한두 줄로 요약하고 상세 규칙은 별도 문서로 분리하는 편이 안전하다.
승인 정책과 샌드박스 설정
Codex CLI가 제공하는 승인/샌드박스 옵션은 개괄적으로 다음과 같이 구분된다. 각 옵션의 정확한 키 이름과 지정 위치는 사용 중인 Codex CLI 버전의 공식 설정 문서를 확인하는 것이 안전하다.
| 설정 | 의미 | 언제 사용 |
|---|---|---|
| Suggest 모드 | 변경 제안만, 모든 적용에 승인 필요 | 마이그레이션 초기, 불확실성 높을 때 |
| Auto Edit 모드 | 파일 수정 자동, 명령 실행은 승인 필요 | 변환 패턴이 검증된 후 |
| Full Auto 모드 | 모든 작업 자동 실행 | AGENTS.md 제약이 충분히 검증된 후만 |
| 작업 공간 쓰기 샌드박스 | 작업 공간 쓰기 허용, 외부 접근 차단 | 일반 코드 변환 작업 |
| 전체 접근 샌드박스 | 모든 접근 허용 | 거의 사용하지 않음 |
권장 시작 조합은 요청 시마다 승인을 받는 정책 + 작업 공간 쓰기 샌드박스다. Suggest 모드로 시작해서 변환 패턴을 눈으로 확인하며 신뢰를 쌓고, 그 이후에 Auto Edit 모드로 전환하는 단계적 접근을 필자는 권장한다.
diff 리뷰 루프 실제 흐름
Codex CLI 인터랙티브 세션에서 사용하는 슬래시 명령(예: /diff, /review)은 세션 내에서 입력하는 명령이다. 아래 표기는 인터랙티브 프롬프트에서 그대로 입력한다는 뜻이며, 셸에서 실행하는 명령은 codex ... 형태로 별도 구분한다. 명령 이름과 정확한 사용법은 사용 중인 Codex CLI 버전에서 /help로 확인하는 것이 정확하다.
단계별 명령 흐름
1단계: 변환 명령 실행 (셸)
codex "src/legacy/auth/ 디렉토리의 모든 콜백 함수를 async/await로 변환해줘. \
util.promisify 적용 가능한 건 promisify로, 불가능한 건 직접 rewrite해줘."Codex가 영향받는 파일 목록과 변환 계획을 먼저 제시한다. Suggest 모드에서는 각 파일 변경을 개별적으로 승인할 수 있다.
2단계: diff로 범위 확인 (인터랙티브 세션 내)
/diffAGENTS.md에서 Never로 지정한 test/ 디렉토리나 package.json이 목록에 나타나면 즉시 중단해야 한다. 범위를 먼저 검증하는 게 핵심이다.
3단계: review로 품질 확인 (인터랙티브 세션 내)
/review/review는 코드를 수정하지 않고 지적 사항만 리포트로 출력한다. 각 회차의 결과가 독립적인 트랜스크립트 턴으로 저장되므로, 이전 회차와 현재 회차를 나란히 보면서 문제 해결 여부를 추적할 수 있다. 콜백 변환 맥락에서 자주 나오는 지적 사항들이다.
- 병렬로 실행되던 콜백이 직렬
await체인으로 바뀐 경우 - 콜백에서 에러를 무시하던 패턴이 catch 없는 Promise rejection으로 변한 경우
- 비표준 콜백 시그니처가
util.promisify에 그대로 넘겨진 경우
4단계: 수정 후 재검증
# 인터랙티브 세션에서 /diff 로 범위 재확인 후 셸로 전환
npm test이 루프를 커밋 전까지 반복한다. npm test가 통과해야 커밋으로 넘어간다.
병렬 처리 패턴—자동 변환에서 가장 흔한 함정
솔직히 이게 가장 신경 써야 하는 부분이다. 콜백 코드에서 병렬로 실행되던 로직이 단순 await 체인으로 변환되면 성능이 저하된다.
// 원본: 콜백 기반 병렬 실행 (의도적)
function fetchUserAndOrders(userId, callback) {
let user, orders, done = 0;
db.getUser(userId, (err, u) => {
if (err) return callback(err);
user = u;
if (++done === 2) callback(null, { user, orders });
});
db.getOrders(userId, (err, o) => {
if (err) return callback(err);
orders = o;
if (++done === 2) callback(null, { user, orders });
});
}// 잘못된 변환: 직렬화로 의도하지 않은 성능 저하
async function fetchUserAndOrders(userId) {
const user = await db.getUser(userId); // 순서대로 실행
const orders = await db.getOrders(userId); // user 끝나야 시작
return { user, orders };
}
// 올바른 변환: 병렬 실행 유지
async function fetchUserAndOrders(userId) {
const [user, orders] = await Promise.all([
db.getUser(userId),
db.getOrders(userId),
]);
return { user, orders };
}AGENTS.md의 Ask First 섹션에 "병렬 실행 의도가 의심되는 패턴 발견 시 확인"을 명시해두면, Codex가 이런 패턴을 만났을 때 자동으로 변환하지 않고 먼저 물어보게 된다.
비표준 콜백과 util.promisify.custom
표준 패턴이 아닌 함수는 util.promisify.custom 심볼로 수동 정의해줘야 한다. 이 목록을 AGENTS.md에 미리 명시해두는 것이 중요하다.
const util = require('util');
// 비표준: 콜백이 (result, err) 순서
function legacyOp(input, callback) {
// callback(result, err) — 비표준 순서
}
// util.promisify.custom으로 수동 정의
legacyOp[util.promisify.custom] = function(input) {
return new Promise((resolve, reject) => {
legacyOp(input, (result, err) => {
if (err) reject(err);
else resolve(result);
});
});
};
// 이제 promisify 적용 가능
const legacyOpAsync = util.promisify(legacyOp);이 함수들을 AGENTS.md의 비표준 목록에 명시해두지 않으면, 에이전트가 표준 util.promisify를 직접 적용하려다 잘못된 에러 처리 코드를 만들어낼 수 있다.
트레이드오프와 실무 주의사항
| 항목 | 내용 |
|---|---|
| 가독성 향상 | 중첩 콜백 구조가 직렬 코드로 평탄화되어 로직 흐름 파악이 쉬워짐 |
| 에러 처리 통일 | try/catch로 비동기 오류를 동기 코드와 같은 방식으로 처리 가능 |
| 디버깅 개선 | 스택 트레이스가 의미 있는 형태로 출력 |
| AGENTS.md 안전망 | 의도치 않은 파일 수정이나 의존성 추가 위험 차단 |
| 반복 검증 | diff 리뷰 루프가 커밋 전에 문제를 포착 |
| 위험 요소 | 대응 방법 |
|---|---|
| 비표준 콜백 자동 변환 실패 | AGENTS.md에 비표준 목록 명시, Ask First로 설정 |
| 병렬 처리 직렬화 → 성능 저하 | /review에서 Promise.all 누락 여부 확인 |
| 에러 무시 패턴 → uncaught rejection | Never 섹션에 빈 catch 블록 생성 금지 명시 |
| 범위 이탈 수정 | diff로 매 단계 확인, 작업 공간 쓰기 샌드박스 사용 |
| 테스트 커버리지 부족 | 변환 전 커버리지 확보가 전제 조건 |
변환 전 테스트 커버리지가 부족하면 동작이 바뀌었는지 검증할 방법이 없다. Codex와 AGENTS.md가 아무리 잘 작동해도, 테스트 없이는 리그레션을 잡아낼 수 없다.
마무리
flowchart TD
A[레거시 콜백 코드베이스] --> B[AGENTS.md 작성]
B --> C[Suggest 모드로 시작]
C --> D[Codex CLI 변환 명령]
D --> E[/diff 로 범위 확인]
E --> F[/review 로 품질 확인]
F --> G{지적 사항 있음?}
G -->|예| H[코드 수정]
H --> E
G -->|아니오| I{npm test 통과?}
I -->|아니오| H
I -->|예| J[git commit]
J --> K{전체 마이그레이션 완료?}
K -->|아니오| D
K -->|예| L[Auto Edit 모드 전환 검토]정리하자면 세 가지다. 첫째, AGENTS.md 없이 Codex를 레거시 코드베이스에 풀어놓는 건 지도 없이 리팩토링을 맡기는 것과 같다. 제약 경계를 먼저 설계해두면 에이전트가 판단을 멈추고 확인을 요청하는 순간이 명확해진다. 둘째, util.promisify가 커버하는 범위와 비표준 콜백 목록을 미리 정리해두면 잘못된 자동 변환을 사전에 차단할 수 있다. 셋째, diff와 review로 매 회차를 검증하고 npm test로 동작 동등성을 확인한 뒤에야 커밋으로 넘어가는 규율이, 자동화 속도와 코드 품질을 동시에 지키는 방법이다.
Suggest 모드로 조심스럽게 시작해서 변환 패턴에 대한 신뢰가 쌓이면 Auto Edit로 넘어가는 흐름을 필자는 추천한다.
참고 자료
- OpenAI Codex GitHub 공식 저장소 — AGENTS.md
- AGENTS.md for OpenAI Codex: Complete Setup and Configuration Guide (2026) — The Prompt Shelf
- Codex CLI Skills & AGENTS.md Setup Guide 2026 — Agensi
- Codex CLI Cheatsheet: config, commands, AGENTS.md, best practices — Shipyard
- How to Build Your AGENTS.md (2026) — Augment Code
- Best AGENTS.md Examples and Templates for AI Coding Agents in 2026 — Promptessor
- Configuration Smells in AGENTS.md Files — arXiv
- Codex CLI agent review loop: the 2026 workflow — Ralphable Blog
- Codex CLI Code Review Workflows: /review, review_model — Codex Knowledge Base
- Codex CLI approval_policy — SmartScope
- Refactoring Legacy JavaScript with OpenAI Codex — Sumit Saha
- From Callback Hell to Async Heaven: Modern Node.js Patterns in 2025 — Medium
- Automating Callbacks to Async/Await Migrations for Mocha Tests — Marco Labarile
- Node.js util.promisify — Mastering JS
- Node.js Best Practices 2026 — Medium