최근 회사에서 AI를 이용하여 테스트코드 영역을 위임해보자라는 sub task를 받았다.
AI를 이용해서 테스트코드 작성해줘! 하면 그럴듯한 테스트코드를 작성해주기도 하지만, 말도 안되는 테스트 코드를 작성하기도 한다.
그러다보니 팀 내에서 우리가 AI를 이용해서 테스트코드를 일정한 규칙으로 위임시키려면 안전장치가 필요했고, 그 과정에서 왜 유닛테스트와 통합테스트가 아닌 e2e테스트를 선택했고, 도입하면서 고민했던 과정을 기록해보았다.
조금 거창하게 얘기해보면 “AI driven E2E test generation harness” 구축 과정이라고할까나..?
결국 이 글은 AI 에게 위임할 수 있는 영역과 사람이 반드시 결정해야 하는 영역 사이의 경계를 어떻게 찾아갔는지에 대한 기록이다.
왜 E2E 테스트인가
기존에 이미 AI를 이용해 다양한 방법으로 테스트 코드를 작성해왔다. 단위 테스트, 통합 테스트는 이미 상당 부분 AI 위임으로 굴러간다. 그런데도 QA 팀의 TC 검수에서 잡히는 버그는 여전히 상당히 많다. 아래 테스트 층위가 그린이어도 사용자가 마주하는 화면에서는 깨지는 일이 계속 생긴다.
이 공백을 어떻게 메울지 고민하다가 떠오른 생각은 단순했다. QA의 TC라는 "답지"가 이미 있으니, 그걸 그대로 e2e 코드로 옮기면 최소한 TC에 해당하는 버그 카드는 더 이상 안 올라오지 않을까? 라는 생각을 했다.
e2e가 그동안 비중이 작았던 이유는 명확하다. 작성 비용이 가장 비싼 테스트 영역이기 때문이다. 시나리오 설계, selector 관리, mock 전략, 플레이키 디버깅까지 코드 한 줄당 들어가는 사람 시간이 다른 어느 테스트보다 크다.
하지만 답지가 이미 있고, 사용자 시나리오라는 명확한 자연어 입력이 존재한다는 점에서, 우리팀 상황에서는 e2e가 AI에게 위임했을 때 비용/효과 비율이 가장 좋은 영역이라고 판단했다.
성공기준은 무엇일까
AI에게 Task를 위임시킬 때 가장 중요한 부분은 output에 대한 명확한 데이터라고 생각한다.
그래서 AI로 E2E테스트를 사용하여 안정적인 테스트가 돌아가고 있다! 라고 말할 수 있는 성공기준은 무엇일까에 대해서 제일 먼저 고민해보았다.
1. 커버리지 측정을 통한 수치화
처음생각했던 vitest로 커버리지 측정하는 것과 비슷하게 코드실행기준으로 커버리지를 측정하기였다.
커버리지 측정 자체는 여기어때 기술블로그의 방식을 거의 그대로 따라해보았다.
| 단계 | 도구 | 시점 |
|---|---|---|
| 인스트루멘트 | Vite + istanbul-lib-instrument 플러그인 |
dev 서버가 소스를 서빙할 때 |
| 수집 | Playwright afterEach fixture |
각 테스트 종료 시 window.__coverage__ → .nyc_output/*.json |
| 병합 | merge-e2e-coverage.mjs (istanbul-lib-coverage) |
전체 테스트 종료 후 1회 |
| 리포트 | istanbul-reports (html, text-summary) |
위와 동일 |
그럴듯하고, 수치화를 명확히 할 수 있다는 점에서 유용해보였다.
하지만 실제로 적용해보았을 때 실행한 코드에 대한 line 커버리지 수치가 비정상적으로 높았다.
단 하나의 페이지의 e2e코드를 작성했으나 마운트되고 있는 다양한 영역의 코드도 실행이 되었다는 점에서 커버리지 수치에 포함이 되어버렸다.
이 방법은 우리가 실제로 e2e테스트코드가 있으니 우리 앱은 안전하다라고 생각할 수 있다고 말하기 어려운 수치였다.
2. TC를 통한 커버리지 측정
그러면 어떤 성공기준을 갖고 갈 수 있을까? 고민하다가 처음으로 돌아와서 생각해보니, 애초에 우리는 TC라는 사람이 만든 고품질 데이터가 있었다. 명확한 성공기준은 이미 사람이 일하는 곳에 있었다.
TC 는 그 자체로 "이 동작이 이렇게 일어나야 한다" 라는 답지 역할을 한다. QA 가 직접 작성하고 갱신하기 때문에 신뢰도는 사람 시스템으로 보장되고, 내용 자체가 자연어라 LLM 에게 그대로 입력으로 줄 수 있다. 답지와 응시자, 채점 기준이 한 세트로 이미 갖춰져 있었던 셈이다.
그래서 채점 단위를 "코드 라인" 이 아니라 "TC 한 행" 으로 바꿨다. 라인 커버리지처럼 부풀려질 여지가 없다. TC 한 행이 통과했다는 건 그 시나리오 하나가 정확히 검증됐다는 의미이고, 사람이 만든 답지와 1:1 로 매칭된다.
다만 모든 TC 가 자동화될 수 있는 건 아니었다. 매장 POS 단말, 실제 결제, 배치 스케줄러처럼 FE 코드가 아무리 잘 짜여도 닿을 수 없는 시나리오가 존재한다. 이런 행들을 분모에서 빼버리기보다는, 자동화된 행 / 자동화 가능한데 아직 안 한 행 / Manual 행 / 스펙아웃 행 을 한 표에 같이 더서 기준 자체를 여러가지로 두었다.
아래는 측정하는 스크립트를 실행했을 때의 결과이다.

하네스가 필요한 이유
이제 명확한 성공기준도 존재하고, 자연어로 모든 시나리오가 명확히 적혀있는 TC, 완성된 코드들도 존재한다.
그러면 playwright를 사용해서 모든 시나리오에 대해서 작성하세요! 하면 짜잔하고 나오겠지 할 수 있지만, 그렇지 않다. 테스트 케이스에서 playwright e2e 테스트 코드로 나오기까지는 아직 사람이 결정해야되는 부분이 많이 남아있다.
1. FE에서 테스트할 수 있는 부분인가?
TC자체는 백엔드와 프론트가 분리되어있지 않다.
외부 의존성이 있거나, 스케쥴링에따라 업데이트되는 등 FE코드에서 테스트할 수 없는 TC들도 존재했다.
백엔드팀과 협업해서 관련된 테스트 API를 추가하는 방식으로 고도화할 수 있겠지만, 일단은 프론트 로직 자체 문제가 없는지 테스트하는게 1차 목표였다.
이러한 분류 자체는 LLM 판단으로 분류는 시킬 수 있겠지만, 확정은 반드시 사람이 해야한다.
2. TC가 최신 기획을 반영하고 있나?
TC는 기획단계에서 적혀있던거라 최신기획을 반영하지 않은 경우들이 종종 있었다.
문구가 변경되었거나 스펙아웃된 기능 등이 존재했다.
실제로 spec 을 돌리면 보통 아래 세가지 케이스에 대해서 실패를 하였고, 사람의 결정이 필요했다.
spec-out: 기능 자체가 사라져서 TC 가 가리키는 UI 가 없는 경우 → TC 스펙아웃처리tc-fe-mismatch: TC 의 기대값과 FE 의 실제 동작이 다른 경우 → TC 수정ui-render-diff: 텍스트 줄바꿈 위치가 다른 등 사소한 차이 → spec 의 selector 만 보완
3. 작성된 e2e코드가 제대로 테스트하고 있는가?
실제로 e2e코드 자체는 코드리뷰할 때 봐도 문제없어 보이지만 제대로 테스틀 못하고 있는 경우가 있다.
예를들면 페이지에 진입했을 때 특정 ui가 노출되지 않는다라는 테스트가 있는데, ui자체가 깨져서 모든 ui가 제대로 노출되지 않고 있었다면 유효한 테스트라고 볼 수 없을 것이다.
이런 경우를 위해 TC케이스마다 캡쳐해서 사람이 리뷰해야될 부분을 간편하게 리뷰할 수 있도록 해주었다.
각 spec 의 step 끝에 캡쳐하는 함수를 호출하면 해당 시점의 화면이 PNG 로 저장되고, 회귀가 끝나면 TC No 별 PNG 가 표로 임베드된 보고서가 자동 생성된다.
- 예시 이미지

사람은 PASS 한 TC 의 캡쳐만 훑어보면서 정말 의도한 화면이 노출됐는지 검증하면 된다.
플로우
답지(TC)도 있고 채점 기준도 명확해졌으니, 이제 자동화 라인만 짜면 된다. 단계마다 잘게 쪼개서 각 단계 사이에 사람이 끼어들 수 있게 만들었다.
전체 흐름은 네 단계로 잡았다.
- 수백 행의 원본 TC 를 피쳐별로 분할 — raw CSV 한 장을 N 개의 피쳐 CSV 로 자른다.
- 각 행이 FE 에서 검증 가능한 시나리오인지 분류 — 매장 POS, 실제 결제, 배치 스케줄러처럼 FE 가 닿을 수 없는 행을 분모에서 빼거나 따로 표시한다.
- 검증 가능한 행만 spec.ts 작성 + 실제 회귀 + PR 생성 — 피쳐별로 독립된 worktree 에서 N 개가 동시에 굴러간다.
- 실패한 행은 자동 분류해서 사람에게 결정 옵션 제시 — TC 가 옛날 기획인지, FE 가 잘못 짜인 건지, spec selector 만 보완하면 되는지를 결정한다.
각 단계 사이의 산출물 (분할 결과, feasibility 분류, PR, 실패 분류 보고서) 은 모두 사람이 한 번 검토하고 다음 단계로 넘기는 게이트 형태로 굴러간다.
이 중 1번의 피쳐 분할이 결과적으로 가장 큰 효과를 봤는데, 이유는 세 가지다.
- PR 한 개의 크기가 작아진다 — 피쳐 하나의 spec.ts + 그에 필요한 mock/fixture 변경만 들어가니까 리뷰어가 한 번에 다 읽고 머지를 결정한다. 거대한 PR 이 쌓여 리뷰가 정체되는 일이 없다.
- LLM 한 세션의 컨텍스트가 작게 유지된다 — 피쳐 하나만 읽고 spec 하나만 작성하니 작업 범위가 일정하게 작게 유지된다. 거대한 시트를 통째로 던지면 LLM 이 중간에 길을 잃기 쉽다.
- 피쳐 단위 병렬화가 가능해진다 — 피쳐 간 의존이 없으니 N 개의 spec.ts 작성을 N 개의 worker 로 동시에 굴린다. 회귀까지 끝나는 시간이 N 분의 1 로 줄어든다.
아래는 My Account 도메인의 TC파일 한 개를 위에 플로우로 돌렸을 때의 결과물이다.
| Spec | PASS | FAIL | SKIP |
|---|---|---|---|
| addresses-mobile | 22 | 0 | 0 |
| beauty-profile-mobile | 32 | 0 | 0 |
| main-page-mobile | 12 | 0 | 0 |
| menu-tree-mobile | 10 | 0 | 0 |
| reward-points-mobile | 13 | 0 | 1 |
| wishlist-mobile | 23 | 0 | 1 |
| membership-mobile | 41 | 0 | 0 |
| 합계 | 153 | 0 | 2 |
7 개 피쳐가 모두 병렬로 실행되고, 사람은 피쳐 단위로만 PR 리뷰를 하면 되는 구조다.
하네스 구조
위 4 단계를 책임 단위로 쪼개니 자연스럽게 세 개의 축이 나왔다.
- 단계별 스킬 7 개 — 라우팅 1 + 4 단계 각각 + 단일 spec writer + 학습 스킬.
- writer ↔ critic 에이전트 페어 — orchestrate 안에서 피쳐마다 페어로 굴러간다.
- 라운드 간 학습 —
/use:ref-learn으로 매 회기마다 룰이 누적된다.
스킬 7 개
| 스킬 | 책임 | 입력 | 출력 |
|---|---|---|---|
/qa-e2e |
라우팅. 입력 / 의도 보고 어느 단계로 갈지 결정 | 자연어 또는 CSV 경로 | 다음 스킬 호출 |
/qa-e2e-refine |
외부 CSV 를 피쳐별로 분할 | raw CSV | 정제 CSV n 개 + README |
/qa-e2e-feasibility |
각 행을 fe에서 작성 가능한 스킬인지 카테고리 분류 | 정제 CSV | Category 컬럼 갱신 + 보고서 md |
/qa-e2e-orchestrate |
N 개 worktree + spec.ts 생성 + PR | 정제 CSV | spec.ts + PR + 캡쳐 |
/qa-e2e-write |
단일 spec 작성 (orchestrate 가 내부 호출) | 피쳐 CSV 1 개 | spec.ts 1 개 |
/qa-e2e-resolve |
현재 context(cwd) 의 실패 분석 — 8 카테고리 + 옵션 매트릭스 (분석 only) | 가장 최근 playwright results.json | 마크다운 보고서 stdout (결정/fix 는 호출자) |
/use:ref-learn |
세션 대화에서 새 룰 추출 → ref 문서에 영구 반영. 다음 세션의 LLM 이 자동으로 따라간다 | 세션 대화 + 사용자 선택 | .claude/ref/ md 파일 갱신 |
핵심은 /qa-e2e 만 부르면 입력 상태 보고 알아서 다음 단계로 라우팅해주는 하네스 스킬이다. raw CSV 던지면 refine 부터, 정제 CSV 면 feasibility 부터, FAIL 이 남았으면 resolve 부터 자동 라우팅된다.
에이전트 페어 — writer ↔ critic
이 중 /qa-e2e-orchestrate 의 PR 자동 생성과 실패 분류는 단순한 스크립트가 아니라 에이전트 페어가 굴린 결과다. orchestrate 의 Phase O3 에서 2N 개 worker 를 단일 team 에 동시 spawn 한다. 피쳐 1 개당 worker 2 개 (writer + critic) 가 페어로 묶여서 작동한다:
qa-e2e-orchestrate-batch (단일 team)
├ A_qa-e2e-spec-writer ↔ A_qa-e2e-critic (피쳐 1 = worktree A)
├ B_qa-e2e-spec-writer ↔ B_qa-e2e-critic (피쳐 2 = worktree B)
├ C_qa-e2e-spec-writer ↔ C_qa-e2e-critic (피쳐 3 = worktree C)
└ …
qa-e2e-spec-writer(A) — 피쳐 CSV 1 개를 받아 spec.ts 1 개를 작성 (/qa-e2e-write스킬을 내부 호출, W1~W5). critic 의 verdict 을 받아 FAIL 이면 자기가 진단/fix —Skill(qa-e2e-resolve)로 분석 보고서 받아team-lead에 escalate, 결정 받아 옵션별 fix 수행. "작성자가 자기 작업을 끝까지 책임" 패턴.qa-e2e-critic(C) — writer 의 spec.ts 를 받아 검증만 수행: 5 카테고리 정적 리뷰 + 자기 worktree 에서 실제 playwright 실행 (PORT env 자동 주입 → dev 자동 기동). verdict 만 응답 (PASS/FAIL + playwright_result + results.json 경로). resolve 호출 / escalate / fix 지시 모두 없음 — writer 가 처리. "외부 시점의 reviewer" 역할에 충실.
이 페어가 최대 2 회 사이클 을 돈다 (C 가 issue 제기 → A 가 수정 → C 가 재리뷰). 2 회 안에 합의가 안 되면 메인이 결과 회수해서 사람에게 결정 요청하도록 했다.
추가적으로 한 번 짜놓고 끝나는 게 아니라, 매 회기마다 새로 얻은 룰을 ref 문서에 영구 적립하는 /use:ref-learn 이라는 별도 스킬이 같이 돌고 있다.
흐름은 단순하다. 코드 리뷰나 AI 와 한참 티키타카하면서 결정된 내용이 있다면, /use:ref-learn 이 세션 대화에서 그 룰만 추출해 사용자에게 multi-select 로 보여주고, 사용자가 선택한 것만 .claude/ref/ 의 컨벤션 문서에 영구 반영된다. 다음 세션의 LLM 은 이 문서를 자동으로 참고하므로, 같은 실수를 한 번 더 하지 않도록했다.
예를 들어 route.continue() 를 사용하지 않겠다고 해당 세션에서 결정되었다고 하면, /use:ref-learn 을 통해 e2e-from-tc.md 에 CRITICAL 룰로 박아두고, 그 뒤로 다른 피쳐 spec 에서는 같은 사고가 재발하지 않게 된다.
사람과 AI 의 역할선
자동화 라인을 굴리면서 가장 신경 쓴 건 "어디까지 AI 에게 맡길 수 있고, 어디부터 사람이 반드시 결정해야 하는지" 의 경계였다.
AI 가 위임받는 부분
- 자연어 시나리오 (TC) → spec.ts 작성 — writer 에이전트
- 작성된 spec 검증 + playwright 실제 실행 — critic 에이전트
- 피쳐별 분할 / feasibility 분류 / 회귀 / 실패 분류 / 캡쳐 보고서 자동 생성
- 세션 대화에서 새 룰 추출 제안 — ref-learn
사람이 반드시 결정해야 하는 부분
위에 적힌 자동화가 굴러가는 동안 사람이 직접 손을 대야 하는 지점은 단계별로 다음 다섯 곳이다.
/qa-e2e-refine분할 결과 검토 — raw CSV 를 피쳐별로 자른 결과. 다중 카테고리 행이 한 피쳐로만 들어갔는지, 누락된 행은 없는지 확인./qa-e2e-feasibility분류 확정 (E2 게이트) — 모호한 행을 LLM 판단으로 채운 경우 보고서의 "재검증 필요" 섹션 확인./qa-e2e-orchestrate의 PR 리뷰 — 피쳐별로 자동 생성된 PR. spec.ts 가 컨벤션을 따르는지, mock 이 실제 BE 응답과 같은 형태인지, captureStep 이 step 마다 박혀있는지./qa-e2e-resolve의 옵션 매트릭스 결정 — FAIL 케이스마다 spec 수정 / TC 수정 / FE 수정 / Archived / 보류 중 어느 옵션을 고를지. "FE 가 정답인지 TC 가 정답인지" 의 판단.- capture-report.md 의 PNG 검토 — PASS 라는 결과가 정말 의도한 화면을 검증한 건지. 특히
toBeHidden()류의 false positive 가 있는 경우.
다섯 지점 중 (3) PR 리뷰와 (4) 옵션 매트릭스 결정은 위 "에이전트 페어" 섹션의 writer ↔ critic 사이클이 합의에 실패했을 때 메인이 결과를 회수해 사람에게 넘기는 시점이다.
남은 과제와 다음 라운드
지금 구조에는 여전히 사람 손이 많이 가는 영역과 다음 라운드 미션으로 미뤄둔 숙제들이 남아 있다.
- mock API 제거
- 로그인 플로우에 클라우드 플레어 캡챠가 걸려 실제 API 검증이 사실상 불가능하다. BE 테스트 계정이나 전용 로그인 경로가 마련되면 mock 의존을 걷어내고 회귀 신뢰도를 올릴 수 있다.
- 새로운 피쳐 개발시 기획서를 읽어 TC 문서자체를 작성하는 에이전트 추가.
- 현재 플로우와 피쳐개발시에 TDD 플로우도 진행하기 위해서는 지금 TC문서에 새로운 TC도 직접 추가되어야한다.
- QA 티켓을 통한 학습하는 플로우 "어떤 TC 가 왜 이를 검증하지 못했는지" 를 명세화해 ref 룰로 적립. 한 번 놓친 버그가 두 번 놓치지 않도록 보강이 필요하다.
- 이벤트 누르는 순간 캡쳐하는 플로우 추가
- 지금은 step 끝 UI 만 PNG 로 저장하는데, 클릭 / 입력 순간의 화면까지 같이 캡쳐하면 리뷰 정확도가 올라간다.
- TC 포맷 자체 다듬기
- 지금까지는 "답지가 있는 그대로" 받아쓰는 데 집중했다면, 다음 라운드는 AI 가 잘 움직이는 형태로 답지 자체를 같이 다듬을 라운드가 될 것 같다.
정리하며
이번 플로우를 구축하면서 또 한번 깨달은 것은 AI가 모든 것을 대신해줄 수 없다.
AI가 위임받을 수 있는 영역은 한정적이고 여전히 사람이 개입해야되는 부분은 명확하다. 모델이 바뀐다고해서 사람이 일하는 세상에서 이 플로우가 변할 수 있을 것 같지는 않다.
명확한 정답을 생성하는 플로우만가 아닌이상 사람이 방향성을 항상 결정지어줘야 한다.
이 때 하네스는 AI가 모호함속에서 혼자 추론하지 않고 사람의 개입이 필요한 부분을 확실하게 잡아주는 제한선 역할을 해준다.
미래에도 다양한 방법론이 나오겠지만, 이런 플로우는 크게 달라지지는 않을 것 같다고 생각한다.
