AI가 쓴 테스트를 어떻게 신뢰할 수 있을까
들어가며
안녕하세요, 모비두 개발본부 프론트엔드팀 김하늬입니다. 이번에 프론트엔드 프로젝트에 단위·컴포넌트 테스트를 도입하면서, 테스트가 만들어지는 과정 자체를 AI 에이전트 워크플로우로 설계했습니다. 그 과정에서 했던 고민과 선택을 공유해 보려고 합니다.
AI로 개발하는 것이 자연스러워진 흐름 속에서, 테스트 코드도 자동으로 작성해 붙이고 싶었습니다. 다만 그냥 단순히 AI에게 “테스트 코드 짜 줘”라고 말할 수는 없었습니다. 기능 개발을 할 때도 원하는 코드를 얻으려면 맥락과 조건을 꽤 정확하게 제어해 줘야 한다는 것을 경험으로 알고 있었으니까요.
테스트 파일을 만드는 일 자체는 AI에게 맡기면 몇 분이면 끝납니다. 하지만 그렇게 나온 테스트가 전부 통과했을 때, 그것을 ‘배포해도 된다’는 근거로 쓸 수 있을까요? 이 질문에 답하려면 꽤 정교한 절차가 필요하다는 결론에 이르렀습니다. 단계마다 무엇을 해야 하고 어떤 조건을 만족해야 넘어갈 수 있는지를 하나씩 정리해 보니, 그것이 그대로 하나의 워크플로우 설계안이 됐습니다. 이 글은 그 절차의 원칙과 설계, 각 단계의 선택, 실행을 강제한 장치, 전파 과정 순서로 이어집니다.
스택: Vitest + React Testing Library + user-event + MSW (jsdom, 커버리지는 coverage-v8), 에이전트는 Claude Code입니다.
1. 배경
1.1. 목표 설정: 회귀를 코드로 막을 장치
프론트엔드 저장소에 테스트가 아예 없지는 않았습니다. 프로덕트를 담당했던 개발자들이 각자의 스타일대로 필요한 곳에 조금씩 작성해 둔 파일이 있었습니다. 다만 수가 많지 않았고 기준이 서로 달랐던 데다 코드가 바뀔 때 함께 갱신되지는 않아서, 배포 여부를 판단하는 근거로 삼기는 어려운 상태였습니다.
그래서 회귀를 발견하는 지점이 늘 뒤쪽이었습니다. 공용 컴포넌트를 고치면 그 컴포넌트를 쓰는 다른 화면이 깨졌는지 확인할 방법이 사람의 기억과 QA밖에 없었고, 화면상으로 멀쩡해 보이는 변화(폼이 서버로 보내는 값, 인증 가드가 반환하는 상태 코드)는 QA에서도 잘 잡히지 않았습니다.
테스트를 도입해야 한다는 결론은 쉬웠지만, 실무에서 막히는 지점은 따로 있었습니다. 테스트 작성 경험이 팀 전체적으로 많지 않았고, 기능 개발과 병행해 테스트를 쌓을 시간도 넉넉하지 않았습니다. AI 에이전트를 쓰기로 한 이유가 여기에 있습니다. 작성 속도만을 위해서가 아니라, 테스트를 잘 쓰는 사람이 옆에 붙어 있지 않아도 일정한 수준의 테스트가 나오게 하기 위해서였습니다.
1.2. 설계 전에 정한 다섯 가지 원칙
그렇다고 테스트 작성을 AI에게 그대로 맡길 수는 없다고 봤습니다. 아무 기준 없이 시키면 렌더만 되면 통과하는 테스트, 현재 동작이 맞는지 따지지 않고 그대로 기대값에 박아 넣은 테스트, 커버리지 숫자를 올리려고 만든 의미 없는 케이스가 나오기 쉽습니다. 그런 테스트는 있으나 마나 한 정도가 아니라, ‘테스트가 있으니 안전하다’는 잘못된 신호를 주기 때문에 오히려 위험하다고 생각했습니다.
그래서 작업을 시작하기 전에 원칙 다섯 개를 먼저 정하고, 워크플로우의 각 장치를 그 원칙에서 도출했습니다.
원칙 | 무엇을 막으려는 것인가 | 워크플로우에 반영된 장치 |
|---|---|---|
① 잘못된 테스트를 만들지 않는다 | 틀린 기대값을 정답으로 고정하는 것 | 생성 전 스펙 승인 · 기대값 불일치 시 자가수정 금지 · 맥락을 차단한 독립 검수 |
② 테스트를 위한 테스트를 만들지 않는다 | 숫자를 채우려고 만드는 무의미한 케이스 | 대상 필터(동작이 있는 소스만) · 위험 중심 케이스 도출 · 검증 불가 항목은 갭으로 기록 |
③ 누가 해도 일관된 결과가 나오게 한다 | 작성자마다 다른 품질·구조 | 컨벤션 문서 단일화 · 카테고리별 견본 · 테스트 전용 lint 규칙 |
④ 프로덕트에 맞는 형태로 생성되게 한다 | 그 프로덕트의 Provider·디자인 시스템·데이터 흐름과 맞지 않는 일반 예제를 그대로 따라 써서, 실행조차 되지 않거나 겉도는 테스트가 나오는 것 | 그 프로덕트의 실제 파일을 견본으로 등록 · 대표 케이스로 환경 도출 |
⑤ 그럼에도 공통 컨벤션은 유지한다 | 프로덕트마다 규칙이 갈라지는 것 | 컨벤션·스크립트·훅은 자산으로 동봉해 복사·머지(재생성 금지) |
원칙 ④와 ⑤를 세운 이유는 다음과 같습니다. 프로덕트마다 Provider 스택도, 디자인 시스템 컴포넌트의 쿼리 전략도, 필요한 폴리필도 다릅니다. 그렇다고 프로덕트마다 규칙을 새로 쓰면 팀 전체의 기준이 사라집니다. 그래서 ‘규칙은 하나, 적응은 각자’로 경계를 그었습니다. 무엇을 어떻게 검증할지는 공통 문서 하나가 정하고, 그것을 돌리기 위한 환경 설정만 프로덕트별로 만들도록 했습니다.
1.3. 고민의 결과물: 네 개의 레이어(Layer)
이 원칙들을 3주에 걸쳐 네 개의 레이어로 구현했습니다.
구분 | 구성 | 역할 |
|---|---|---|
규칙 | 테스트 컨벤션 문서 - 10개 영역 · 38개 항목 | AI와 사람이 같은 기준으로 쓰고 검수하기 위한 단일 진실원 |
워크플로우 | 스킬 3개 + 독립 검수 서브에이전트 1개 | 분류 → 스펙 승인 → 생성 → 완전성 → 검수 파이프라인 |
강제 장치 | pre-push 검증 2개 · 파일 해시 기록 · 테스트 전용 lint · CI 테스트 단계 | 실행 여부를 사람의 기억에 맡기지 않기 위한 장치 |
전파 장치 | 워크플로우 도입 스킬과 필요한 재료 | 다른 프로덕트에 1회 실행으로 자동 이식 |
이어지는 내용은 이 네 레이어를 순서대로 풀어 놓은 것입니다. 2장에서는 테스트가 만들어지는 일곱 단계를 따라가며 각 단계에서 어떤 선택지를 두고 무엇을 골랐는지, 3장에서는 그 워크플로우를 실제로 실행하게 만드는 장치를 소개하고, 4장은 결과물을 다른 프로덕트로 전파하는 방법을 다룹니다.
2. 워크플로우 상세
2.1. 메인 플로우: 한 파일이 거치는 일곱 단계
테스트 생성 스킬은 대상 파일 하나를 일곱 단계로 처리합니다. 여러 파일이 대상이면 한 파일씩 일곱 단계를 끝까지 밟은 뒤 다음 파일로 넘어갑니다.
단계 | 이름 | 하는 일 |
|---|---|---|
1 | 분류 | 종류(유틸/훅/컴포넌트) · 기대값의 출처(스키마/코드/사람) · 고위험 여부 판별 + 상류·하류 영향 파일 식별 |
2 | 스펙 추출 | 필드 × 규칙 전수 매트릭스 작성 → 사람 승인 |
3 | 생성 | 컨벤션 준수 + 카테고리별 견본 파일을 읽고 모방 |
4 | 완전성 루프 | 실행 + 커버리지 → 매트릭스 전 행이 단언됐는지 확인 → 보강 → 재측정 |
5 | 품질 검수 | 검수 서브에이전트를 맥락을 차단한 상태로 실행 |
6 | 종합·판정 | 검수 결과의 유효성을 메인 스레드가 먼저 거른 뒤 사람이 최종 판단 |
7 | 기록 | 최종 상태의 파일 해시를 기록 — 3장의 push 검증에 쓰입니다 |
사람이 반드시 개입하는 지점은 두 곳(2단계, 6단계)입니다. 나머지는 AI가 진행합니다. 사실 이 작업에서 가장 오래 붙잡고 있었던 것은 개별 기능을 구현하는 일이 아니라 이 설계 자체였습니다. 어디까지 AI에게 맡기고 어디서 사람이 확인해야 하는지, 확인을 어느 형태로 받아야 형식적인 통과가 되지 않는지를 정하는 데 시간이 가장 많이 들었습니다. 아래에서 단계 순서대로 그 판단을 풀겠습니다.
2.2. [1단계] 테스트 대상 선정
테스트를 위한 테스트가 생기는 가장 흔한 경로는 대상 선정을 하지 않는 것입니다. 그래서 파일을 워크플로우 파이프라인에 태울지부터 걸렀습니다. 적용 대상은 동작이 있는 .ts, .tsx만이고, 타입·상수·테스트 인프라 등 단순 재노출이나 검증할 로직이 없는 파일은 아예 워크플로우를 태우지 않습니다.
1단계에서 하나 더 하는 일은 대상 파일 밖에 사는 규칙을 찾는 것입니다. 대상 파일만 읽으면 다른 파일에 있는 조건을 통째로 빠뜨립니다.
상류(부모의
defaultValues, 형제 섹션의setValue, "식별자 없음"을0으로 표기하는 센티널) — 여기서 만들어지는 값의 가능한 상태가 곧 이 파일의 입력 도메인입니다.하류(페이로드 조립부, 변환 유틸, API 타입) — "폼에서는 허용되지만 서버로 나가면 안 되는 값" 같은 교차 규칙은 대상 파일 안에 적혀 있지 않습니다.
서브섹션 컴포넌트와 부모 페이지가 대표 사례입니다. 서브섹션 단독 테스트는 부모의 페이로드 변환 규칙을 보지 못하고, 부모 페이지 테스트는 서브섹션 상호작용을 보지 못합니다. 그래서 어느 쪽을 테스트하든 반대쪽 규칙을 표에 올리고 담당(이번/다음)을 명시하게 했습니다. 범위 밖으로 정한 조건은 사유와 함께 갭으로 남깁니다. 무리해서 한 파일에서 다 검증하려는 것도, 조용히 빠뜨리는 것도 막기 위해서입니다.
2.3. [2단계] 선 스펙 승인, 후 코드 생성
테스트의 기대값이 틀리면 그 테스트는 틀린 동작을 지켜 줍니다. 그래서 기대값이 맞는지 여부를 사람이 먼저 확인하도록 하였습니다.
선택지 | 장점 | 단점 |
|---|---|---|
생성 후 코드 리뷰에서 확인 | 별도 단계가 없다 | 테스트 코드에서 기대값 오류를 찾는 일은 리뷰어에게 부담이 크고, 실제로는 통과 여부만 보고 넘어가기 쉽다 |
생성 전 스펙 표를 승인 (채택) | 표 한 장이면 검토가 끝난다. 잘못된 전제로 코드를 만드는 낭비가 없다 | 단계가 하나 늘어난다 |
승인 없이 전량 자동 생성 | 가장 빠르다 | 원칙 ①(잘못된 테스트를 만들지 않는다)을 포기하는 선택 |
구체적으로는 다음과 같이 동작합니다. AI가 대상의 필드 × 규칙 매트릭스를 만들어 스펙을 표로 추출합니다. 스펙은 [필수/선택] · [형식·길이 제약을 각각] · [경계값 ±1] · [조건부·교차 규칙] · [기본값] · [상호작용] 기준으로 생성합니다.
그리고 각 행에 근거 라벨을 붙입니다.
[스키마근거]— 스키마에서 도출한 정확한 값[현재동작]— 코드의 현재 동작을 기록한 것[확인필요]— 사람의 판단이 필요한 것
순수 유틸은 기본적으로 [현재동작]으로 채우고, 음수·NaN·빈값·반올림·null 처리 등 모호한 동작만 [확인필요]로 올리게 했습니다. 모든 케이스를 사람에게 묻지 않는 것도 규칙으로 정했습니다. 물어보는 비용이 크면 승인 절차는 결국 형식적이게 되니까요. 이 결과물이 개발자의 승인을 거치게 됩니다.
2.4. [3단계] AI의 기준: 테스트 컨벤션과 견본
일관된 테스트 코드가 나오려면 기준이 될 컨벤션이 필요했습니다. 이전까지 팀에 테스트 컨벤션이 없었기 때문에 기준부터 만들어야 했습니다. 기존 컨벤션이 없었던 만큼 프론트엔드 테스트의 베스트 프랙티스를 조사해 출발점으로 삼고, 우리 프로덕트 구조에 맞는 것만 취사선택한 뒤 예시를 전부 우리 코드로 다시 썼습니다. 이렇게 만든 초안을 팀에 공유해 리뷰를 받았고, 팀에서 합의한 테스트 컨벤션이 생겼습니다. 그리고 워크플로우가 이 문서를 참조하게 했습니다.
문서에 담긴 규칙 중에는 이런 것들이 있습니다.
테스트 파일은 소스 옆에 두고(colocate) 이름은 대상 파일과 동일하게 생성
테스트 설명문은 한국어로 “~하면 ~한다” 형태로
describe/it만 읽어도 계약이 그려지도록 작성쿼리는 접근성 우선순위대로, 구현 디테일(클래스명·내부 state) 검증 금지
네트워크만 MSW로 가리고, 내부 모듈 모킹은 최소화
단언은 관측 가능한 계약을 통째로 하여 일부가 조용히 바뀌어도 통과하지 않도록 방어
다만 LLM 특성상 “컨벤션을 읽고 알아서 잘 써 줘”보다, 그 프로덕트에서 이미 통과한 실제 테스트 파일을 읽고 모방하게 하는 쪽이 결과가 훨씬 안정적이기 때문에 유틸 · 훅 · 단순 컴포넌트 · 폼 · 데이터 패칭 다섯 카테고리마다 견본 파일 경로를 매핑해 두고, 생성 단계에서 해당 견본을 실제로 열어 보게 했습니다.
기계가 잡을 수 있는 것은 lint로 넘겼습니다. 단언 없는 테스트(vitest/expect-expect), skip/only(no-focused-tests), 쿼리 우선순위와 fireEvent 사용(eslint-plugin-testing-library), 비동기 findBy 권장 등을 규칙으로 걸었습니다.
예전에 각자 스타일로 작성돼 남아 있던 테스트를 어떻게 할지도 여기서 함께 결정했습니다.
선택지 | 판단 |
|---|---|
전부 컨벤션에 맞게 재작성 | 도입이 그만큼 늦어진다. 당장 가치를 주지 않는 작업에 시간을 먼저 쓰게 된다 |
전부 삭제 | 완성도는 낮아도 지금 통과하고 있는 검증까지 잃는다 |
lint 대상에서만 격리하고 점진 편입 (채택) | 새로 쓰는 테스트에는 컨벤션이 100% 적용되고, 기존 테스트는 CI에서 계속 실행돼 회귀를 잡는다 |
채택한 방식은 기존 테스트 파일들을 .eslintignore에 등재해 lint에서만 제외하고, 컨벤션 규칙은 새로 쓰는 테스트에만 적용되도록 한 것입니다. 기존 테스트를 격리하는 작업은 후술할 워크플로우 전파 절차에 포함하여 다른 프로덕트에 도입할 때에도 기준이 일관되도록 하였습니다. 격리 목록은 해당 파일을 손볼 일이 생길 때마다 컨벤션으로 편입하면서 줄여 나가고 있습니다.
2.5. [4단계] 매트릭스와 커버리지로 판정하는 완전성
이제 AI가 테스트코드를 기준에 따라 생성합니다. 이 시점에서 고려해야 할 문제는, 테스트가 ‘온전히’ 작성되는가, 그리고 테스트가 ‘잘’ 작성되는가였습니다.
테스트를 실행했는데 실패했을 때 AI에게 “통과할 때까지 고쳐라”라고 맡기면, 가장 쉬운 해결책은 기대값을 실제 출력에 맞추는 것입니다. 그러면 버그가 정답으로 굳습니다. 그래서 실패를 두 종류로 나눴습니다.
구분 | 대응 |
|---|---|
기계적 실패 (import·셋업·문법·쿼리 오타·렌더 깨짐) | AI가 두세 번까지 자가수정 |
기대값 불일치 | 자가수정 금지. ‘코드의 버그인지, 스펙·테스트가 틀린 것인지’를 분류해 사람에게 올립니다 |
버그 후보로 판정된 항목은 단언하지 않습니다. 그렇다고 케이스를 그냥 빼지도 않습니다. 빼 버리면 그 라인은 다른 입력으로 실행되므로 커버리지는 100%인데 그 입력만 검증되지 않은 상태가 되어버리기 때문입니다. 그래서 파일에 흔적을 남기게 했습니다.
// 🚩 [BUG-CANDIDATE] formatPrice('12.50') → '1,250' (소수점 제거·자릿수 병합). 수정 후 todo를 실제 테스트로.
it.todo('[BUG-CANDIDATE] 소수점이 있는 가격을 올바르게 처리한다 (현재 12.50→1,250 오동작)')러너가 todo로 표시하고, [BUG-CANDIDATE] 태그를 검색해 push·머지 전에 ‘미해결 버그 후보 N건’을 집계할 수 있습니다.
완전성은 매트릭스와 커버리지 두 가지로 판정합니다. 둘은 보는 대상이 다르고, 둘 다 만족해야 판정이 성립합니다.
기준 | 무엇을 확인하나 | 혼자서는 못 잡는 것 |
|---|---|---|
승인된 매트릭스 | 스펙의 모든 행이 실제로 단언됐는가 | 매트릭스에 적히지 않은 분기(방어 코드, 예외 경로) |
커버리지(func·line) | 실행되지 않은 함수·라인이 남아 있는가 | 애초에 케이스에서 빠진 필드·규칙 |
커버리지 혼자로는 누락을 못 잡습니다. 커버리지는 이미 쓴 케이스가 어느 라인을 실행했는지만 측정하기 때문입니다. 스키마의 특정 필드에 대한 테스트를 하나도 쓰지 않아도, 다른 필드를 채워 제출하는 테스트가 같은 라인을 지나가게 되면 라인 커버리지는 100%로 보입니다.
반대로 매트릭스 혼자로도 부족합니다. 매트릭스의 행을 다 단언하더라도 파일 안의 다른 export나 방어 코드가 한 번도 실행되지 않은 채 남을 수 있기 때문입니다. 예를 들어 export 5개 중 2개만 다룬 상태에서 테스트가 전부 통과하면 겉으로는 완료처럼 보이지만 커버리지를 켜면 40%가 됩니다.
그래서 4단계는 두 기준이 동시에 만족될 때까지 루프를 돌도록 했습니다. 매트릭스 행을 다 단언한 뒤 커버리지를 측정하고, 미커버 라인이 남으면 그 라인에 도달하는 입력을 추가해 다시 측정합니다. 목표는 func·line 100%이며, 도달할 수 없는 방어적 폴백처럼 정당한 사유가 있는 경우에만 사유를 남기고 갭으로 인정합니다. branch 비율만 보지 않도록 한 이유도 있습니다. 실행되지 않은 함수의 분기는 branch 통계에 잡히지 않아 수치가 실제보다 높게 보이기 때문입니다.
정리하면 이 단계의 완료 조건은 세 가지입니다.
매트릭스 모든 행 단언
func·line 커버리지 100%(또는 사유가 기록된 갭)
미해결 버그 후보 없음.
통과했다는 사실만으로는 완료로 보지 않도록 했습니다.
2.6. [5단계] 테스트 코드 자체의 품질 검수
완전성을 확인해도 남는 질문이 있습니다. “이 테스트가 좋은 테스트인가?” 커버리지도 lint도 그것은 보지 못합니다. 구현이 망가져도 통과하는 단언, 클래스명·내부 state를 검증해서 안전한 리팩터에 깨지는 테스트, 비동기 대기를 빠뜨려 간헐적으로 실패하는 테스트가 생성되는 걸 막고 싶었습니다.
그래서 테스트 코드 자체의 품질을 검수하기 위해 검수 단계를 별도 서브에이전트로 분리했습니다. 여기서도 선택지가 있었습니다.
선택지 | 문제 |
|---|---|
같은 세션에서 체크리스트만 적용 | 생성 맥락이 그대로 남아 자기 결과물을 스스로 채점하게 된다 |
별도 에이전트 + 스펙 매트릭스 전달 | 테스트가 그 매트릭스에서 나왔으므로, 매트릭스로 채점하면 순환 검증이 된다 |
별도 에이전트 + 맥락 차단 (채택) | 소스를 직접 읽어 스스로 기준을 세우기 때문에 사람이 승인한 결정도 2차 소견으로 의심할 수 있다 |
테스트의 품질 확보를 최우선으로 한다는 목적에 ‘별도 에이전트 + 맥락 차단’ 방식이 가장 부합한다고 생각했기 때문에, 검수 에이전트는 테스트 파일 경로만 받고, 소스를 직접 읽어 판단하도록 구현되었습니다.
그러면 검수 에이전트는 어떤 기준으로 테스트 품질 검수를 해야 하는지의 기준이 필요하다고 생각했습니다. 그래서 검수 기준도 컨벤션과 같은 방식으로 세웠습니다. ‘좋은 테스트란 무엇인가’를 조사해 아홉 개 항목의 루브릭으로 정리하고, 문서로 만들어 에이전트 정의에 고정했습니다. 루브릭은 세 축으로 묶었습니다.
회귀 보호력 — 단언 강도·거짓양성, 기대값의 정확성(현재 버그를 무비판적으로 고정했는가), 에러 경로의 질(‘터졌는가’만 보는지 vs 올바른 에러·복구까지 하는지), 위험 집중(돈·권한·데이터)
리팩터 내성 — 구현 결합, 모킹 적정성(과대·과소), 결정성·flaky, 격리·정리
스위트 차원 —
describe/it만 읽어 계약이 그려지는가, 실패 시 원인을 빨리 찾을 수 있는가, 유지보수 대비 값어치
각 지적은 적대적 근거를 요구하도록, “구현을 X로 바꿔도 이 테스트는 통과한다” 형태로 쓰게 했습니다.
여기에 하나 더 명시한 규칙이 있습니다. ‘문제가 없으면 한두 문장으로 끝내고, 없는 문제를 만들어내지 말 것.’ LLM은 요청을 받으면 어떻게든 답을 채우려는 경향이 있어서, 검수할 것이 없을 때도 사소한 트집을 지적처럼 포장해 내놓기 쉽습니다. 그런 지적을 그대로 채택하면 테스트가 불필요하게 복잡해지고, 정작 중요한 지적이 묻힙니다. 그래서 ‘발견된 것만 쓰고, 깨끗하면 깨끗하다고 답하라’를 기준에 함께 넣었습니다.
이 검수 서브에이전트는 조언만 합니다. 통과·차단도, 자동 수정도 하지 않습니다.
추가로, 부수 효과로 확인된 내용이 있었습니다. 검수자가 찾아내는 문제 중 몇 가지가 테스트가 아니라 소스 쪽 문제였습니다. 그래서 출력을 테스트 품질과 소스 개선 제언으로 나누고, 후자에는 [테스트 용이성](label·aria·testid 부재)과 [버그 후보] 라벨을 붙이게 했습니다.
2.7. [6단계] AI의 종합 보고와 개발자의 최종 판단
검수 결과가 그대로 사람에게 전달되지는 않습니다. 실제 흐름은 3단입니다.
테스트 생성 (메인 스레드)
↓
적대적 검수 (서브에이전트 · 맥락 차단)
↓
검수 결과 유효성 판정 (메인 스레드)
— 이 지적이 이 코드에 실제로 유효한가?
— 컨벤션에서 이미 예외로 허용한 사항을 지적한 것은 아닌가?
— 테스트 문제인가, 소스 문제인가?
↓
종합 보고 → 사람의 최종 판단맥락을 차단했기 때문에 검수는 독립적이지만, 그만큼 문맥을 모르고 지적하는 경우도 생깁니다. 예를 들어 라벨이 연결돼 있지 않아 어쩔 수 없이 순서로 요소를 찾은 부분은 컨벤션이 이미 예외로 허용하면서 주석을 요구하는 사항인데, 검수자는 그 사정을 모르니 지적합니다. 이 검수 결과에 대한 필터를 사람이 아니라 메인 스레드가 먼저 맡아 걸러야 할 지적인지 수용해야 할 지적인지를 판단합니다.
그렇게 정리된 종합 보고를 사람이 받습니다. 추가된 케이스 수, 커버리지 수치, 남은 갭과 그 사유, 미해결 버그 후보, 유효하다고 판단한 검수 지적과 채택 제안이 한 번에 올라옵니다. 사람은 여기서 기대값이 의도와 맞는지, 어떤 지적을 채택할지를 결정합니다.
2.8. [7단계] 최종 상태에서 남기는 해시 기록
승인이 끝나고, 그 과정에서 소스를 고쳤다면 그 수정까지 모두 반영된 최종 상태에서 처리한 소스 파일의 내용 해시를 기록합니다. 이 기록이 다음 장에서 설명할 push 시점 검증의 근거가 됩니다. 테스트를 만드는 도중 소스가 함께 바뀌는 일이 종종 생기기 때문에 이 작업을 워크플로우 마지막 단계에 둬서 최종 상태의 코드를 기준으로 해시를 기록하도록 했습니다.
3. 실행 강제 장치
3.1. push 전 테스트 생성 여부 검증
워크플로우가 아무리 좋아도 실행하지 않으면 아무 일도 일어나지 않습니다. 특히 급할 때 가장 먼저 생략되는 것이 테스트입니다. 그래서 ‘변경한 소스에 대해 테스트 워크플로우를 돌렸는가’를 push 시점에 기계적으로 확인하게 만들었습니다.
판정 방법은 이렇습니다.
워크플로우의 마지막 단계에서, 처리한 소스 파일의 내용 해시(
git hash-object)를 브랜치별로 기록해 둡니다.push 직전(
pre-push훅)에 git으로 변경 파일 목록(브랜치 분기점~HEAD + 아직 커밋하지 않은 변경)을 뽑습니다.그중 테스트 대상인 파일의 현재 해시와 기록된 해시를 대조합니다. 다르면 push를 막습니다.
// 변경됐고 아직 처리되지 않은(현재 해시 ≠ 기록된 해시) 파일
export const pending = (file, filter) => {
const rec = loadFor(file)[branch()] || {}
return changedFiles()
.filter(filter)
.filter((f) => existsSync(f))
.filter((f) => hashOf(f) !== rec[f])
}‘실행했다’는 플래그 또는 실행 시각이 아니라 내용 해시를 쓴 이유가 여기 있습니다. 테스트를 쓰다 보면 소스도 함께 고쳐집니다. 플래그 방식이면 테스트를 만든 뒤 소스를 더 고쳐도 통과하지만, 해시 방식이면 파일이 바뀐 순간 다시 대상이 됩니다.
대상 판정에는 두 가지 예외를 뒀습니다. 타입·상수·barrel·스토리·테스트 인프라는 애초에 워크플로우 대상이 아니므로 검증에서도 빼고, 퍼블리싱 전용 브랜치나 충돌 해결용 브랜치(-dev·-merge 등 접미사)는 브랜치명 패턴으로 검증을 생략하도록 했습니다. 이런 브랜치의 변경은 대부분 다른 브랜치에서 이미 검증된 커밋을 옮기는 작업이라, 같은 기준을 적용할 필요가 없기 때문입니다.
3.2. push 전 영향 범위 테스트 실행
테스트를 만들었는지만 확인하면 ‘새 테스트는 만들었지만 기존 테스트를 깨뜨린’ 경우를 놓칩니다. 그래서 같은 구조로 검증을 하나 더 뒀습니다. 개발자가 원격에 push해 MR을 올리기 전에, 자기 작업이 영향을 주는 테스트를 로컬에서 먼저 실행해 확인하도록 했습니다.
영향 범위는 vitest related --run으로 산출합니다. 변경한 파일을 직접·간접적으로 import하는 테스트를 모두 찾아 실행하므로, 내가 건드린 코드가 닿는 곳은 전부 돌아갑니다. 전부 통과하면 그 시점의 해시를 기록하고 push가 열립니다.
물론 husky 훅으로 걸어 둔 장치라 --no-verify를 붙이면 우회할 수 있습니다. 우회를 원천 차단하려면 서버 쪽 훅이나 별도의 검증 장치를 붙여야 하는데, 얻는 것에 비해 드는 비용이 크고 정말로 우회가 필요한 상황도 생길 수 있어 그렇게까지 하지는 않았습니다. 이 장치는 규칙을 어기려는 사람을 막으려고 만든 것이 아니라 깜빡하고 지나치는 경우를 잡으려고 만든 것이기도 합니다. 일부러 우회해 테스트를 건너뛰려는 사람은 없다는 전제 위에서, 특별한 사유 없이는 우회하지 않기로 팀에서 합의하는 선으로 정리했습니다.
3.3. CI 테스트 단계와 배포 차단
로컬 검증은 개발자가 push하기 전에 자기 변경의 영향 범위를 확인하기 위한 것이고, 최종 확인은 CI가 맡습니다. 배포 브랜치의 파이프라인 첫 단계에 테스트를 추가해, 전체 테스트 코드를 실행하도록 했습니다. 하나라도 실패하면 build·deploy 단계가 아예 돌지 않으므로, 깨진 테스트를 안은 채로는 배포가 나갈 수 없게 되었습니다.
stages:
- test
- build
- deploy
# 전체 테스트코드를 실행하여 실패 발생 시 배포 차단
test:
stage: test
script:
- pnpm run test:run
4. 다른 프로덕트로 전파
4.1. 워크플로우 전파용 스킬
같은 워크플로우를 다른 프로덕트에도 적용하기 위해서 전파 작업 자체를 별도 스킬로 만들어 두기로 했습니다. 전파 스킬을 실행하면 도입에 필요한 작업이 순서대로 진행되도록 구성했습니다. 워크플로우 스킬 및 검수 에이전트 설치, 의존성 설치, 컨벤션 문서 배치, 러너·lint·타입 설정 머지, 공통 렌더 헬퍼와 MSW 골격 생성, 기존 테스트 격리, 강제 장치(훅·스크립트·CI) 설정, 마지막으로 대표 케이스를 통과시키는 검증까지입니다. 단계마다 진행 상태를 파일에 기록하게 해서, 중간에 끊기더라도 다음 실행에서 이어서 진행할 수 있도록 했습니다.
여기서 정한 경계가 원칙 ④(프로덕트에 맞는 형태로 생성되게 한다)와 원칙 ⑤(그럼에도 공통 컨벤션은 유지한다)의 접점입니다.
구분 | 대상 | 방식 |
|---|---|---|
프로젝트와 무관한 고정된 자료 | 컨벤션 문서, 스크립트, git 훅, 설정 스니펫, 워크플로우 스킬·에이전트 | 복사·머지. AI가 다시 만들지 않도록 했습니다 |
프로덕트마다 다른 것 | 러너 설정, 공통 렌더 헬퍼의 Provider 스택, MSW 골격, 테스트 데이터 factory, 폴리필 | 컨벤션 기준으로 생성·적응하게 했습니다 |
고정 자료를 매번 새로 만들게 하면 프로덕트마다 조금씩 다른 버전이 생기고, 그러면 원칙 ⑤(그럼에도 공통 컨벤션은 유지한다)가 깨집니다. 그래서 스킬 폴더에 자산을 동봉해 두고 도입 시 그대로 복사하도록 했습니다. 팀원이 스킬을 한 번 실행하는 것으로 도입이 끝나게 하는 것이 목표였습니다.
4.2. 프로덕트별 맞춤 설정
프로덕트마다 무엇이 필요한지는 미리 알 수 없습니다. 어떤 폴리필이 필요한지, 쿼리 캐시를 미리 채워야 하는지, 디자인 시스템 컴포넌트를 무슨 방법으로 찾아야 하는지는 실제로 테스트를 돌려 봐야 드러납니다. 그래서 도입의 마지막 단계를 ‘난이도를 한 단계씩 올리며 대표 케이스를 통과시키는 것’으로 잡았습니다.
아래 세 단계를 고른 기준은 의존성의 층위입니다. 각 단계가 서로 다른 환경 요구를 드러내도록 배치했습니다.
순서 | 대표 대상 | 이 대상을 고른 이유 | 결정사항 예시 |
|---|---|---|---|
1 | 순수 유틸 1개 | Provider도 DOM도 필요 없어, 러너·전역 설정·커버리지가 도는지만 깨끗하게 확인할 수 있습니다 | (대부분 여기서는 추가 설정이 필요 없습니다) |
2 | 단순 폼 1개 | DOM 렌더와 사용자 상호작용이 처음 들어오는 지점이라, 공통 렌더 헬퍼와 브라우저 API 부족이 드러납니다 |
|
3 | 복잡 폼 1개 | 서버 데이터·비동기 제출·커스텀 입력 요소가 한꺼번에 걸리는 가장 어려운 조합입니다 | 쿼리 캐시 시드 옵션, MSW 핸들러, 텍스트 기반 쿼리 전략 |
초기 세팅 단계에서는 컨벤션 문서에 실려 있는 기본 예시를 견본으로 걸어 두고 시작합니다. 그리고 위 세 개를 통과시키고 나면, 그 과정에서 만든 테스트 파일을 그 프로덕트의 견본으로 교체 등록합니다. 이후 워크플로우를 돌리면서 다른 카테고리(훅, 데이터 패칭 등)의 테스트가 실제로 만들어질 때마다 견본 목록이 채워지도록 했습니다. 원칙 ④(프로덕트에 맞는 형태로 생성되게 한다)를 만족시키기 위해 그 프로덕트의 실제 코드로 견본을 생성하도록 하였습니다.
마치며
이 워크플로우는 지금 업무에서 사용하고 있습니다. 도입 이후 진행한 티켓마다 이 절차로 테스트를 만들었고, 테스트 파일 18개 · 케이스 265개가 쌓였습니다.
숫자보다 체감되는 변화는 따로 있습니다. 이제 테스트가 전부 통과했다고 하면 그게 무슨 뜻인지 설명할 수 있게 됐다는 점입니다. 통과했다는 말은 아래 네 가지를 모두 지났다는 뜻입니다.
스펙 — 승인한 매트릭스의 모든 항목이 실제로 단언됐다
커버리지 — 실행되지 않은 함수·라인이 없다. 남아 있다면 사유가 적혀 있다
미검증 입력 — 아직 검증하지 못한 입력은 todo로 드러나 있다
기대값 — 사람이 직접 보고 승인했다
생성한 테스트코드를 신뢰할 수 있고, 테스트를 많이 써 보지 않은 사람이 같은 절차를 밟아도 비슷한 수준의 결과가 나옵니다. 처음에 원했던 것이 정확히 이것이었습니다.
AI가 쓴 테스트를 어떻게 신뢰할 수 있을까요. 조금은 말장난처럼 들리지만, 신뢰할 수 있는 테스트를 쓰도록 AI에게 시켰습니다. 무엇을 신뢰할 수 있다고 볼 것인지 먼저 정하고, 그 조건을 매번 빠짐없이 밟게 만드는 방식으로요.
AI와 일하는 방식을 저마다 찾아가고 있는 시기에, 제가 고민하고 경험한 이 과정이 긴 글을 읽어주신 분께 조금이나마 도움이 되었으면 좋겠습니다. 감사합니다.