AI 하네스로서의 E2E 테스트: 1시간의 수동 QA에서 10분으로
요약
AI 도입으로 가속화된 개발 속도에 맞춰 E2E 테스트 인프라를 재구축하여 QA 병목 현상을 해결한 사례를 다룹니다. Playwright, Docker, 결정론적 시드를 활용해 수동 QA 시간을 1시간에서 10분으로 단축하고 개발 신뢰도를 높였습니다.
핵심 포인트
- AI 코드 생성 속도에 맞춘 자동화된 QA 하네스 구축의 중요성
- Playwright와 Docker를 활용한 휘발성 DB 기반 E2E 환경 조성
- 결정론적 시드와 역할별 계정을 통한 테스트 안정성 확보
- PR 프리뷰 단계에서의 자동화된 QA를 통한 회귀 오류 방지
TL;DR: 우리는 E2E 테스트 인프라(Playwright + Docker + 결정론적 시드(deterministic seeds) + 역할별 계정)를 처음부터 다시 구축했으며, 이는 두려움 없이 개발에 AI를 사용할 수 있게 해주는 _하네스(harness)_가 되었습니다. 1시간의 수동 UAT(사용자 수락 테스트)가 5~10분으로 단축되었고, QA는 각 PR(Pull Request)의 프리뷰에서 자동으로 실행되며, 보이지 않던 권한 버그들이 드러났습니다. 그 방법과 이유를 단계별로 설명합니다.
목차
- 문제점: AI는 모든 것을 가속화했지만, QA는 제외되었습니다
- 가설: 테스트는 AI의 하네스입니다
- E2E 인프라: 휘발성 DB, 결정론적 시드, 그리고 역할별 계정
- 처음부터 구축한 CI 파이프라인
- 루프 닫기: PR 프리뷰에서의 자동화된 QA
- 결과: 이것이 제품에 미치는 영향
- 핵심 요약 (Takeaways)
- 감사 인사 — 그리고 제가 이 글을 쓰기로 결심한 이유
문제점: AI는 모든 것을 가속화했지만, QA는 제외되었습니다
개발 흐름에 AI가 도입되면서 우리는 훨씬 더 많은 코드를 생산하게 되었고, 이는 고전적인 병목 현상을 드러냈습니다: 리뷰(review)와 QA가 그 속도를 따라가지 못했다는 점입니다.
우리의 경우(멀티 테넌트(multi-tenant) SaaS 플랫폼, 소규모 팀) 이는 다음과 같은 의미였습니다:
- 모든 흐름을 하나씩 확인하고, PR에 증거를 남기기 위해 모든 것을 스크린샷으로 찍는 수동 UAT
- 상태가
핵심적인 논리는 이렇습니다: 유닛 테스트 (Unit Test)가 로직의 케이스들을 커버하고, E2E 테스트가 모든 사용자 흐름 (User Flows)을 커버하며, 생성된 코드가 프로젝트 표준을 따라 리뷰를 통과했다면 — 당신이 수동으로 "뒤져봐야" 할 것이 무엇이 남겠습니까?
항상 무언가는 남습니다. 예외 상황이나 UX의 세부 사항 같은 것들 말이죠. 하지만 신뢰의 대부분은 하네스 (Harness)에서 옵니다. 그리고 이것은 AI와의 관계를 변화시킵니다. 모든 줄을 의심하며 검토하는 대신, 하네스가 힘든 일을 처리하도록 맡기고 당신은 가장 가치 있는 곳에 주의를 집중하게 됩니다.
이러한 하네스가 없다면 상황은 반전됩니다. AI는 인간이 리뷰할 수 있는 속도보다 더 빠르게 코드를 생성하며, 속도는 수동적인 상태가 됩니다 — 버그가 기능(Feature)과 같은 속도로 유입되고, 조용한 회귀 (Regression) 오류들이 고객이 불평할 때까지 발견되지 않으며, AI의 모든 변경 사항마다 다시 수동 UAT를 수행해야 하는 원점으로 돌아가게 됩니다.
여기서부터는 기술적인 문제를 넘어 제품의 문제가 됩니다. 견고한 하네스는 팀이 두려움 없이 빠르게 움직일 수 있게 해주는 핵심 요소입니다. 핵심 흐름이 커버되어 있다는 확신이 들 때, 당신은 더 큰 변화를 수용하고, 공포 없이 리팩터링 (Refactoring)을 수행하며, 더 많이 배포할 수 있습니다. 확신이 없을 때, 모든 배포는 도박이 됩니다. 그리고 그 비용은 칸반 보드에 나타나는 것이 아니라, 이탈률 (Churn), 고객 지원, 그리고 소리 없이 무너지는 사용자 신뢰로 나타납니다. 품질은 속도의 반대 개념이 아닙니다. 품질은 속도를 지속 가능하게 만드는 것입니다.
E2E 인프라: 휘발성 DB, 결정론적 시드 (Seeds), 그리고 역할별 계정
지난해 말, 저는 인프라와 E2E 테스트를 처음부터 다시 구축하는 책임을 맡았습니다. 저의 접근 방식은 점진적이었습니다. 먼저 기본적인 기능을 작동시키고, 그다음 패턴을 연구한 뒤, 가능한 한 빨리 베스트 프랙티스 (Best Practices)를 적용하여 리팩터링하는 방식이었습니다.
Docker를 이용한 휘발성 DB: 테스트가 데이터베이스를 소유한다
가장 큰 문제는 데이터베이스의 상태 (State)였습니다. 해결책은 방정식에서 그 상태를 제거하는 것이었습니다. pnpm test:e2e:ui를 실행하면, Playwright의 글로벌 셋업 (Global Setup)이 격리된 MySQL을 띄우고, 마이그레이션 (Migrations)과 시드 (Seeds)를 실행한 뒤, 테스트 파일 간에 복구 가능한 스냅샷을 찍습니다:
// tests/e2e/global-setup.ts (단순화 버전)
export default async function globalSetup() {
await dbManager.startContainer(); // Docker를 통한 전용 MySQL 8 (독립된 포트, 격리된 DB)
...
결과는 로컬 환경과 CI 환경에서 항상 동일합니다. Compose의 MySQL조차 테스트를 위한 튜닝(innodb-flush-log-at-trx-commit=0, 조정된 버퍼 풀(buffer pool))이 적용되어 있습니다. 일회성 데이터베이스에서는 내구성(durability)보다 속도가 중요하기 때문입니다. 또한 테스트 파일 간에 데이터베이스가 복구되므로, 하나의 스펙(spec)이 다른 스펙을 오염시키지 않습니다.
역할(Role)별 계정: 가장 큰 효과를 본 디테일
단 하나의 관리자(admin) 계정으로 모든 것을 테스트하는 것은 가장 중요한 권한(permission) 버그들을 정확히 가리고 있었습니다. 관리자는 어떤 화면에서도 통과하기 때문입니다. 해결책은 타입이 지정된 단일 진실 공급원(single source of truth)에 정의된 **시스템의 모든 역할(roles)**을 커버하는 계정 프로비저닝(provisioning)을 생성하는 것이었습니다:
// tests/e2e/setup/test-credentials.ts (일부)
export type E2EAccountKey =
| 'owner'
...
각 테스트가 올바른 역할(role)을 사용하기 시작하자마자, 아무도 알아채지 못했던 access denied 버그들이 나타나기 시작했습니다. 최종 사용자의 실제 권한으로 어떤 흐름도 실행되지 않았기 때문입니다. 만약 RBAC(역할 기반 액세스 제어)를 사용하면서 모든 것을 관리자로 테스트하고 있다면, 당신의 권한 테스트는 존재하지 않는 것이나 다름없습니다.
이것은 단연코 전체 인프라 투자 중 가장 높은 수익률(ROI)을 기록한 부분이었습니다. 그리고 우연이 아니게도 제품과 가장 밀접하게 연관되어 있었습니다. 권한 버그는 단순한 기술적 버그가 아닙니다. 사용자가 봐서는 안 될 데이터를 보거나, 접근해야 할 기능에서 차단되는 문제입니다. 이는 신뢰를 갉아먹으며, 프로덕션(production) 환경에서 발견될 경우 비용이 매우 많이 드는 바로 그런 종류의 문제입니다.
POM + BDD: AI가 이해하는 형식
테스트는 도메인별로 조직된 Page Object Model (POM) (예: pages/child, pages/relatives, pages/session...)을 따르며, fixtures 및 test-data builders를 사용합니다. 또한 선택적으로 흐름을 설명하는 .feature 파일 (Gherkin/BDD)을 작성하는데, 이는 실행 가능한 문서 (executable documentation)가 되며 바로 이 지점에서 AI가 빛을 발합니다. 문서화된 흐름, 일관된 POM, 그리고 Playwright MCP가 준비되어 있기 때문에, AI는 취약한 셀렉터 (selectors)를 처음부터 임의로 만들어내는 대신 프로젝트의 패턴을 따라 새로운 E2E 스펙 (specs)을 생성합니다. 이러한 구조가 존재하기 전에는 AI로 E2E를 생성하는 것이 제대로 작동하지 않았습니다.
한 테스트 내에서 두 개의 역할 (roles)이 포함되는 핵심 흐름의 실제 사례를 들어보겠습니다. 관리자 (supervisor)가 책임자 (responsible)를 초대하면, 책임자가 자신의 기기에서 초대를 열고 아이의 문진표 (anamnese, 입원 초기 진료 기록)를 작성하는 과정입니다:
Scenario: Invited relative completes every editable anamnese field
Given a main supervisor is logged in
And the supervisor invited and assigned a relative to the seeded child
...
그리고 스펙 (spec)은 시나리오를 단계별로 따릅니다. 각 test.step은 Gherkin의 한 줄을 반영하므로, Playwright 리포트 (report)는 비즈니스 흐름처럼 읽힙니다:
// testIsolated: 각 테스트(재시도 포함) 전에 DB 스냅샷을 복구합니다
test.use({ restoreDb: undefined });
test.use({ storageState: 'playwright/.auth/main-supervisor.json' }); // 올바른 역할, 이미 로그인됨
...
하네스 (harness)가 처리하는 세부 사항을 주목하십시오. 역할 (role)은 storageState를 통해 준비된 상태로 진입하며, 두 번째 로그인은 격리된 브라우저 컨텍스트 (browser context)에서 실행됩니다 (로그아웃 같은 편법 없이 실제 멀티 유저 환경 구현). 또한 testIsolated는 각 테스트 전에 데이터베이스 스냅샷을 복구하므로, 초대 + 등록 + 양식 작성으로 이어지는 이 전체 흐름은 항상 동일한 상태에서 필요한 만큼 반복해서 실행될 수 있습니다.
처음부터 구축하는 CI 파이프라인 (pipeline)
규칙은 간단합니다. 저렴하고 빠른 것이 먼저 실행되고, 비싼 것은 나머지가 모두 통과했을 때만 실행됩니다. PR (Pull Request)이 draft 상태에서 벗어나면, 워크플로우 (workflow)는 연결된 두 개의 잡 (jobs)으로 실행됩니다:
Job 1 — 품질 검사 (Quality Checks) (빠른 게이트, 약 15분 타임아웃):
두 개의 잡 (jobs)으로 실행됩니다:
Job 1 — 품질 검사 (Quality Checks) (빠른 게이트, 약 15분 타임아웃):
- 품질 검사 (lint, typecheck 등)
- 단위 테스트 (Testes unit)
- 통합 테스트 (Testes de integração)
Job 2 — E2E (Job 1이 성공해야 실행됨):
- Docker를 통해 MySQL 8을 격리된 DB (
proaba_e2e_test)로 올림 - 테스트용
.env파일을 생성함 (가짜 비밀값,NODE_ENV=test) - Playwright의 Chromium을 설치함
- 전역 설정(healthcheck → migrations → seeds → snapshot → 검증)을 실행함
- 완벽한 E2E 스위트를 실행함
- Playwright 보고서를 아티팩트(artifact)로 업로드함 (실패 시에도)
데이터베이스가 잡 내부에서 생성되고 사라지기 때문에, 공유 환경에 대한 의존성이 전혀 없음 — 전형적인
직접적인 수치로 보면 다음과 같습니다:
- 1시간의 수동 UAT (사용자 수용 테스트) → 5~10분: AI에게 지시하면 AI가 스스로 플로우를 실행하고 증거(evidence)를 캡처합니다.
- PR(Pull Request) 오픈 → 리뷰 → 머지(merge) 사이클이 훨씬 빨라짐: 매주 수많은 시간이 절약됩니다.
- 권한 관련 버그 발견: 적절한 역할(role)이 테스트에 도입되자마자 이전에는 보이지 않던 권한 버그들이 나타났습니다.
- 복리 효과: 테스트가 많아질수록 사용자가 발견하기 전에 다음 버그를 잡아낼 확률이 높아집니다.
하지만 수치가 중요한 이유는 그 수치가 해방해 주는 것들 때문입니다. 되찾은 시간은 단순히 생산성 스프레드시트 속의 숫자로 사라지는 것이 아니라, 무언가를 구축하는 역량으로 전환됩니다. 즉, 더 많은 기능을 인도(delivery)하고, 사용자의 목소리에 귀를 기울이며 UX를 고민할 시간을 확보하며, 회귀(regression) 테스트의 불을 끄느라 시간을 허비하는 인원을 줄이는 데 쓰입니다. 하네스(harness)가 보장하는 품질은 팀이 인도하는 결과물을 계속 신뢰하면서도 빠르게 인도할 수 있게 해줍니다. 규모가 작은 팀에게 이것은 항상 뒤처져서 쫓아가는 것과 실제로 제품을 앞으로 밀고 나가는 것 사이의 차이입니다. 결국, 제대로 된 테스트는 엔지니어링 규율로 위장한 제품의 레버리지(leverage)입니다.
핵심 요약 (Takeaways)
- 테스트는 AI의 하네스(harness)입니다. 테스트가 없다면 AI의 속도는 부채가 되지만, 테스트가 있다면 진정한 속도가 됩니다.
- 테스트가 데이터베이스를 소유해야 합니다. 휘발성 DB(ephemeral DB) + 결정론적 시드(deterministic seeds) + 스냅샷(snapshot)은 플래키니스(flakiness)의 가장 큰 원인을 제거합니다.
- 실제 역할(role)로 테스트하세요. 모든 것에 관리자(admin) 계정을 사용하는 것은 가장 심각한 버그들을 정확히 숨기는 행위입니다.
- 구조가 먼저, AI는 그다음입니다. POM(Page Object Model), 픽스처(fixtures), 문서화된 플로우가 AI의 테스트 생성을 가능하게 만드는 요소이지, 그 반대가 아닙니다.
- 단언(assertion)보다 증거(evidence)가 중요합니다. 스크린샷과 뮤테이션 테스트(mutation testing)를 포함한 자동화된 QA, 그리고 증거 없이는 성공을 주장할 수 없는 PR 설명(PR description)이 필요합니다.
감사의 인사 — 그리고 제가 이 글을 쓰기로 결심한 이유
솔직히 고백하자면, 자신의 작업에 대해 글을 쓰는 것은 저에게 자연스러운 일은 아닙니다. 저를 움직이게 한 계기는 Austin Kleon의 작은 책인 _Show Your Work!_였습니다. 저의 마음을 사로잡은 아이디어는 다듬어진 결과물뿐만 아니라 **과정 (process)**을 공유하는 것, 그리고 당신이 배운 것을 가르치는 것이 당신의 작업 가치를 깎아먹는 것이 아니라 오히려 더해준다는 것이었습니다. 저는 바로 이러한 정신을 바탕으로, 단순히 "우리는 E2E 테스트를 가지고 있다"가 아니라, 각 결정의 _어떻게 (how)_와 _왜 (why)_를 이곳에 기록했습니다.
따라서 만약 이곳의 내용이 제가 시행착오를 거치며 발견했던 것들 — 임시 데이터베이스 (ephemeral database), 역할 기반 계정 (accounts by role), 또는 테스트를 AI 하네스 (AI harness)로 사용하는 법 등에 관한 것들 — 중 몇 시간이라도 아껴줄 수 있다면 그것으로 충분합니다. 저를 밀어준 Kleon에게 감사를 전합니다. 만약 여러분이 더 나은 하네스를 가지고 있거나 저의 선택 중 동의하지 않는 부분이 있다면 댓글로 알려주세요. 저는 공개적으로 계속 배워나가는 중입니다. 🙂
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기