Codex CLI로 Playwright E2E 스펙을 자동 생성하고, 실패 로그를 되먹여 스스로 고치게 만들기
E2E 테스트는 손이 많이 간다. 셀레늄 시절부터 Playwright까지, 뭘 써도 결국 선택자 하나 바뀌면 테스트가 줄줄이 터지고 그걸 고치는 데 오후가 통째로 사라진다. 나도 처음엔 "AI가 테스트를 써준다고? 어차피 내가 다시 고쳐야 할 텐데"라며 심드렁했는데, Codex CLI에 Playwright MCP를 물려 며칠 굴려보고 나서 판단이 조금 달라졌다.
핵심은 단순히 코드를 생성해 주는 게 아니라는 것이다. Codex CLI는 샌드박스 안에서 코드를 쓰고, 직접 npx playwright test를 실행하고, 실패 로그를 다음 판단의 입력으로 되먹여, 수정하고 다시 돌리는 과정을 반복한다. AGENTS.md 파일 하나로 이 루프의 조건과 규칙을 정의하면 통과할 때까지 에이전트가 알아서 굴러간다. 그 사이 사람은 다른 일을 할 수 있다.
이 글에서는 Playwright MCP 서버 설정부터 AGENTS.md 기반 루프 구성, 그리고 실제 굴려봤을 때 걸리는 지점까지 정리한다.
Codex CLI가 E2E 테스트를 다루는 방식
에이전트 루프의 구조
Codex CLI는 터미널에서 동작하는 코딩 에이전트다. 어떤 모델을 사용하는지는 OpenAI Codex 문서에 따라 시점별로 다르므로, 여기서는 특정 모델명 대신 "격리된 샌드박스에서 파일을 읽고 쓰고 셸 명령을 실행할 수 있는 에이전트"라는 성격에 초점을 두자. 중요한 건 단순한 코드 완성 도구가 아니라는 점이다. 파일을 읽고, 코드를 쓰고, 터미널 명령을 실행하고, 그 결과를 다시 자신의 입력으로 삼는다.
E2E 시나리오에서 루프는 대략 이렇게 흐른다.
이 루프에서 Playwright MCP 서버가 다리 역할을 한다. @playwright/mcp 패키지가 MCP(Model Context Protocol) 서버로 동작하며, 에이전트가 접근성 트리(accessibility tree) 스냅샷을 통해 헤드리스 브라우저를 조작할 수 있게 한다. 여기서 한 가지 짚고 갈 것은, 접근성 트리는 DOM의 의미론적 구조를 넘겨줄 뿐 렌더링된 시각적 픽셀 상태를 주지 않는다는 점이다. 그래서 "에이전트가 눈으로 UI를 본다"는 표현은 과장이다. 색상·정렬·오버플로우 같은 시각적 회귀는 이 루프만으로는 잡히지 않고, 필요하면 스크린샷 비교나 별도 시각 회귀 도구를 얹어야 한다.
AGENTS.md — 루프를 통제하는 파일
AGENTS.md는 원래 OpenAI Codex 진영에서 제안된 프로젝트 레벨 안내 파일 포맷이다. 이후 agents.md 형태로 공개적으로 문서화되면서 여러 에이전트 도구가 같은 파일명을 관례적으로 참조하는 흐름이 생겼다. 어떤 중립 재단이 이 포맷을 관리한다는 식의 이야기는 확인되지 않으므로, 여기서는 "사실상의 관례로 자리 잡은 공용 포맷"이라는 정도로만 두자.
Codex CLI는 작업을 시작할 때 이 파일을 읽고, 재시도 조건·테스트 명령·체크 게이트 같은 규칙을 컨텍스트에 넣는다. 즉 루프의 형태를 결정하는 지점이 여기다.
세팅: MCP 서버 연결부터 첫 스펙 생성까지
1단계: Codex CLI 설정 파일에 MCP 서버 등록
Codex CLI의 설정 파일은 홈 디렉터리의 ~/.codex/config.toml(글로벌) 또는 프로젝트 루트의 .codex/config.toml을 참조한다. 정확한 경로와 스키마는 설치한 버전의 codex --help 및 공식 문서에서 확인하는 게 안전하다. 아래는 개념적 예시다.
# ~/.codex/config.toml (개념적 예시)
[mcp_servers.playwright]
command = "npx"
args = ["@playwright/mcp@0.0.x"] # 실제 사용할 특정 버전으로 고정@latest로 두면 서버 릴리즈가 바뀔 때마다 툴 시그니처가 바뀌어 프롬프트가 뜬금없이 실패한다. 팀 레포에서는 특정 버전을 못박고, 업그레이드는 의도적으로 가져가는 편이 낫다.
2단계: AGENTS.md 작성
이게 핵심이다. 나도 처음엔 "항상 모든 테스트를 실행해"라고 적었다가 무관한 실패가 계속 쌓여 낭패를 봤다. Daniel Vaughan의 Why 'Always Run Tests' in AGENTS.md Makes Things Worse 글도 같은 지점을 지적한다.
# AGENTS.md
## Testing
- 변경 후 반드시 `npx playwright test --reporter=line` 실행
- 공유 모듈 변경 시에만 전체 스위트 실행, 그 외엔 변경된 스펙만 실행
- 실패 시: 실패 로그를 분석하고 selector 또는 assertion을 수정 후 재실행
- 테스트 러너가 0이 아닌 종료 코드를 반환하면 실패로 간주하고 다음 턴 진행
- 동일 스펙에서 3회 연속 실패 시 자동 수정 중단하고 사람에게 보고
## Code Style
- 테스트 파일은 `tests/e2e/` 디렉터리에 위치
- Page Object Model 패턴 사용, 기존 `pages/` 디렉터리 파일 참조
- `getByRole`, `getByLabel` 등 접근성 기반 selector 우선
## Assertion 품질 규칙
- expect(true).toBe(true) 같은 자명 assertion 금지
- 각 테스트는 UI에 실제로 나타나는 텍스트, URL, 상태 중 최소 1개를 검증
## Checks (모두 통과해야 완료)
- npx playwright test
- npx tsc --noEmit
- npx eslint src/여기서 exit code 규칙 부분에 주의하자. Playwright 테스트 러너는 관습적으로 테스트 실패 시 0이 아닌 코드로 종료하며, 실무에서는 통과=0, 실패=0 아님으로 취급하는 편이 가장 안전하다. "exit code 2일 때만 재시도" 같은 식으로 특정 숫자에 조건을 걸면 인수 파싱 오류나 인터럽트 상황과 뒤섞여 정상 실패를 놓칠 수 있다.
3단계: Codex CLI 실행
codex "앱의 로그인에서 대시보드 이동, 로그아웃까지의 플로우에 대한
Playwright E2E 스펙을 tests/e2e/auth.spec.ts에 작성하고
AGENTS.md의 Checks가 전부 통과할 때까지 수정해줘."이 시점부터 에이전트가 자율적으로 동작한다. Playwright MCP를 통해 접근성 트리로 UI 구조를 조회하고, 스펙을 쓰고, 테스트를 돌리고, 실패하면 로그를 읽어 selector나 assertion을 고친다.
재시도 루프에서 놓치기 쉬운 지점
종료 코드 기반 자동 반복과 탈출 조건
에이전트의 재시도 로직 자체는 단순하다. 테스트 명령이 0이 아닌 종료 코드로 끝나면, 그 실패 출력을 다음 턴의 컨텍스트에 넣고 다시 진입한다. 통과 여부만 보고 종료 코드로 판정하기 때문에, --reporter=line처럼 stderr에도 실패 요약이 뚜렷하게 남는 리포터를 쓰는 편이 디버깅에 유리하다.
문제는 탈출 조건이다. 아무 안전장치 없이 두면 다음과 같은 실패 양상이 나온다.
- 동일 selector에서 계속 실패하며 같은 수정을 반복
- 잘못된 방향으로 assertion을 완화해서 통과시켜 버림
- 세션 토큰/컨텍스트를 다 태우고 나서야 사람 개입 요청
그래서 AGENTS.md에 "N회 연속 실패 시 중단", "assertion을 삭제하거나 무조건 truthy로 바꾸는 수정 금지" 같은 명시적 가드를 넣어두는 게 실전에서는 훨씬 낫다. 아래는 사람 개입 지점을 명시한 흐름이다.
"통과했다"와 "의미 있다"는 다른 문제
여기서 곧바로 짚고 넘어가야 할 리스크가 있다. 에이전트는 종료 코드가 0이 되게 하는 데 최적화되어 있지, 테스트가 의미 있는지 판정하도록 훈련된 게 아니다. 실패를 반복하면 assertion을 슬쩍 완화하거나, expect(page).toBeTruthy() 같은 사실상 무의미한 검증으로 도망갈 수 있다. 마무리 섹션이 아니라 지금 이 자리에서 강조해 두자. 루프가 통과했다고 해서 그 테스트가 회귀를 잡아준다는 보장은 없다.
대응은 두 갈래다. 하나는 위에서 넣은 것처럼 AGENTS.md에 assertion 품질 규칙을 명시하는 것. 다른 하나는 리뷰 단계에서 사람 눈이 각 스펙의 expect 라인을 훑는 것. 둘 다 필요하다.
훅으로 게이트 강화하기
Codex CLI가 지원하는 훅(도구 호출 전/후, 종료 시점)을 활용하면 npx tsc --noEmit나 lint 통과 여부를 강제 조건으로 걸 수 있다. 훅 이름과 스키마는 버전에 따라 달라졌으므로 사용 중인 CLI의 문서에서 확인하고 붙이는 편이 안전하다. 요지는 "테스트 통과"라는 단일 지표를 넘어, 타입·린트·정적 분석까지 게이트에 포함시켜야 에이전트가 편법으로 통과를 만들어내기 어려워진다는 점이다.
모노레포에서의 계층화
단일 레포에서는 루트의 AGENTS.md 하나로 충분하지만, 모노레포에서는 서비스마다 테스트 명령이 다르다. Codex CLI를 비롯한 여러 에이전트 도구는 관례적으로 하위 디렉터리에도 AGENTS.md를 두면 해당 경로 작업 시 함께 참조한다. AGENTS.override.md 같은 특수한 파일명을 병합해준다는 이야기는 공식적으로 확인된 바 없으므로, 여기서는 검증된 패턴만 쓰자.
/
├── AGENTS.md # 공통 규칙
├── packages/
│ ├── payments/
│ │ └── AGENTS.md # payments 전용 테스트 명령
│ └── notifications/
│ └── AGENTS.md # notifications 전용 규칙# packages/payments/AGENTS.md
## Testing
- 이 디렉터리 변경 시: `make test-payments` 실행
- 전체 스위트 불필요, payments 서비스 스펙만 실행
- 상위 AGENTS.md의 규칙은 그대로 적용되며, 이 파일은 테스트 명령만 오버라이드병합/우선순위 규칙은 CLI 버전에 따라 다르니, 실제 어떤 지시가 컨텍스트에 들어갔는지 확인하고 싶다면 codex --help에서 지원되는 디버그 옵션(로그 레벨 상향, 세션 로그 파일 등)을 우선 살펴보는 편이 확실하다.
굴려보면 드러나는 것들
장단점을 같은 결로 정리
| 항목 | 실제 감상 |
|---|---|
| 비동기 실행 | 지시 후 자리를 떠도 테스트가 통과할 때까지 굴러가고 결과 보고를 받을 수 있다 |
| 컨텍스트 인식 | 레포의 기존 Page Object·설정·스펙을 읽고 스타일을 맞추려 시도한다 |
| 격리 샌드박스 | 의존성 설치·테스트 실행이 샌드박스 내부에서 이뤄져 로컬 오염이 적다 |
| 피드백 루프 자동화 | 종료 코드 기반 재시도로 selector 수정 같은 반복 작업을 사람이 안 해도 된다 |
| 시각적 회귀에 취약 | 접근성 트리 기반 조작이라 색·정렬·픽셀 회귀는 잡지 못한다. 필요하면 스크린샷 비교를 별도로 붙여야 한다 |
| 컨텍스트 윈도우 한계 | 대형 모노레포에서는 관련 파일만 골라 넣는 스코핑 규칙을 AGENTS.md에 명시해야 한다 |
| assertion 품질 검증 필수 | 통과했다고 안심할 수 없다. 리뷰 단계에서 expect 라인만이라도 사람이 훑어야 한다 |
| 실행 환경 신뢰 경계 | 클라우드에서 돌리는 모드를 쓸 경우 레포 접근 권한, 시크릿 노출 범위, MCP 서버가 접근하는 URL 화이트리스트, 네트워크 정책을 조직 보안 기준에 맞춰 명시적으로 설정해야 한다 |
가장 흔한 실수: "항상 전부 실행"
AGENTS.md에 "항상 모든 테스트를 실행"이라고 쓰면 파일 한 줄 고쳐도 전체 E2E가 돌고, 관련 없는 실패까지 에이전트가 붙잡고 씨름한다. 변경된 스펙만 선택 실행하도록 명시하는 게 훨씬 낫다.
MCP 서버 버전 고정
@playwright/mcp@latest는 편해 보이지만 툴 시그니처가 바뀌면 프롬프트 전체가 갑자기 어긋난다. 팀 레포일수록 버전을 못박고, 릴리즈 노트를 확인한 뒤에만 올리자.
Playwright 진영의 에이전트 기능과의 관계
Playwright도 최근 릴리즈에서 에이전트 친화적인 도구를 확장해 왔다. 정확한 하위 컴포넌트 이름과 기능 분리는 버전마다 다르므로 Playwright 공식 문서에서 사용 중인 버전의 것을 확인하는 게 정확하다. 여기서는 큰 그림만 정리하자면, "테스트 자동 생성/치유 성격의 기능을 Playwright가 제공한다면 Codex CLI 루프의 앞뒤에 붙일 수 있다" 정도로 이해하면 된다.
즉 툴 자체가 처리할 수 있는 얕은 회귀는 툴에 맡기고, 그 밖의 케이스만 Codex CLI 루프로 넘기는 이중 안전망 구성이 자연스럽다.
CI로 옮길 때 유의점
PR 트리거로 Codex CLI를 돌리려면 GitHub Actions 러너에 CLI가 없으니 설치 스텝이 반드시 필요하다. 아래는 개념적 예시이며, 정확한 패키지명·설치 방식·플래그는 사용 중인 CLI 버전의 공식 문서를 따라야 한다.
# .github/workflows/codex-e2e.yml (개념적 예시)
name: Codex E2E Generation
on:
pull_request:
types: [opened, synchronize]
jobs:
generate-e2e:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Codex CLI
run: npm install -g @openai/codex # 실제 패키지명은 공식 문서 확인
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Codex E2E generation
run: |
codex "변경된 파일에 대응하는 E2E 스펙을 작성하고 AGENTS.md의 Checks를 통과시켜"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}codex cloud exec 같은 특정 서브커맨드로 병렬 실행이 가능하다는 이야기를 들었다면, 실제로 그런 서브커맨드가 존재하는지, 어떤 요금·격리 정책을 가지는지는 사용 중인 CLI의 codex --help와 공식 문서로 확인하고 붙이자. 없는 명령을 문서에만 적어두면 팀원이 재현에 실패한다.
마지막으로 남는 이야기
정리하자면, 이 루프의 가치는 "테스트를 대신 써준다"가 아니라 "실패 로그를 사람이 다시 안 읽어도 되도록 되먹임 회로를 만들어준다"에 가깝다. 그런데 이 회로를 실제로 굴려보면, 결국 사람이 계속 손을 대야 하는 지점이 세 군데 남는다.
- AGENTS.md의 탈출 조건과 assertion 품질 규칙. 이걸 안 걸면 에이전트가 통과를 만들어내려고 assertion을 무너뜨린다.
- selector 전략.
getByRole·getByLabel같은 안정적 선택자를 앱 쪽 마크업에서 이미 잘 노출하고 있어야 루프의 수렴이 빨라진다. 접근성 속성이 부실한 화면은 에이전트가 억지 XPath로 도망간다. - 리뷰. 통과된 스펙의
expect라인을 사람이 한 번은 훑어야 한다. 이 단계가 없으면 CI는 초록불인데 회귀는 계속 나가는 상태에 조용히 도달한다.
그러니 이 도구는 "E2E를 대신 짜주는 기계"라기보다, 선택자 갈아엎기와 실패 로그 재해석이라는 지루한 부분을 걷어내 주는 도구로 보고 도입하는 편이 기대치 관리에 좋다. 그 지루한 부분이 사실 오후를 통째로 잡아먹는 부분이었다는 점에서, 걷어낼 만한 가치는 충분하다.
참고 자료
- Codex CLI 공식 문서 (OpenAI Developers)
- Playwright 공식 문서
- Playwright MCP 저장소 (@playwright/mcp)
- agents.md 포맷 소개
- Model Context Protocol 공식 사이트
- Why 'Always Run Tests' in AGENTS.md Makes Things Worse — Daniel Vaughan
- End-to-End Testing with Codex CLI and Playwright — Daniel Vaughan
- Test-Driven Development with Codex CLI — Daniel Vaughan
- Loop Engineering with Codex CLI — Daniel Vaughan