케이스 작성부터 결함 등록까지: AI 에이전트와 한 달 만에 만든 QA 파이프라인
첫 커밋부터 한 달, 개발자가 아닌 QA가 개발 조직의 도움 없이 테스트케이스 관리 시스템(QMS)을 만들어 팀에 배포하기까지의 기록입니다.
왜 직접 만들었나
출발점은 AI 시대의 속도였습니다. AI가 개발 사이클(SDLC)을 끌어올리면서 릴리스 주기는 계속 짧아지는데, 검증해야 할 범위는 오히려 늘어납니다. QA 인원이 같은 속도로 늘지 않는 이상, 테스트 업무와 커버리지의 증가를 사람의 손만으로는 따라잡을 수 없는 시점이 왔습니다. 그래서 필요한 건 단순한 케이스 저장소가 아니라, AI로 테스트케이스를 작성하고 E2E 자동화까지 이어지는 시스템이었습니다.
기존 방식(프로젝트 단위별 스프레드시트에 케이스 작성 + 별도 자동화 케이스의 관리)의 문제도 뚜렷했습니다.
- 자동화가 케이스와 따로 논다 — 자동화를 위한 테스트케이스를 별도로 다시 만들어야 했고, 정작 검증이 가장 급한 신규 Feature Test에는 자동화를 활용할 수 없었습니다.
- 이력이 없다 — 누가 언제 어떤 케이스를 왜 고쳤는지 시트는 말해주지 않고 또는 요구사항의 변경은 수정에 많은 리소스가 소요됩니다.
- 테스트케이스 관리가 어렵다 — Regression/Smoke/Feature 테스트케이스 세트 구분도, 시스템(Admin/OMS/DriverApp)별 분류도 시트 탭 이름, 또는 파일로 분류하는 것이 전부였습니다. 구조가 없으니 케이스가 쌓일수록 찾고, 고치고, 지우는 테스트케이스를 현행화 하는 것을 사실상 불가능했고 Feature test case만 생성하여 파편적으로 관리되었습니다.
상용 테스트 관리 도구를 도입하는 대신 직접 만들기로 한 건, 우리가 원한 게 "AI가 케이스를 쓰고, 버튼 하나로 자동화를 돌리고, 실패하면 Jira 결함까지 이어지는 루프 즉 QA 파이프라인을 구축하는 것"이 목표였습니다. 모든 것을 자동화 하지 않더라도 자동으로 실행되는 케이스는 테스트 리소스를 획기적으로 줄일수 있다는 판단이며, 그 루프는 우리 파이프라인(GitLab CI, Jira, Slack)에 딱 맞게 깎아야 했고, 그건 기성품이 해줄 수 없는 일이었습니다.
QMS는 이 케이스 하나만 만드는 게 아니라, 회사가 이미 쓰고 있는 3개 도구를 사이에 끼고 도는 구조로 설계했습니다.
케이스 작성부터 자동화 실행, 결함 등록, 사람의 확인까지 — 이 네 축이 QMS를 중심으로 하나의 루프로 돌아가는 게 핵심이었고, 뒤에서 다룰 스킬 계층과 Play 버튼, Jira 결함 루프는 모두 이 그림의 화살표 하나씩을 구현한 것입니다.
이 요구사항을 가장 잘 아는 것도, 원하는 형태로 정확히 구현하고 싶었던 것도 QA 엔지니어인 저였습니다. 그래서 개발 조직에 요청하는 대신, AI를 통해 직접 만들기로 결정했습니다.
에이전트와 일하는 방식
개발은 멀티 에이전트 데스크톱 환경 위에서 Claude 에이전트와 함께 진행했습니다. 한 달을 지나고 보니, 잘 굴러간 날과 아닌 날의 차이는 "에이전트에게 절차를 스킬로 박아뒀는가"였습니다.
QMS는 만들어지는 도구인 동시에, 에이전트가 쓰는 도구이기도 합니다. 그래서 스킬을 계층으로 쌓았습니다.
- testcase-writer — Jira 티켓/기획서를 주면 QA 관점의 테스트케이스를 작성해 QMS DB에 등록하는 스킬. QMS의 API 토큰 인증이 "스킬 스크립트용" 경로를 따로 가진 이유입니다.
- testcase-modify — 등록된 케이스끼리 대조·정리하는 스킬. Feature Test의 신규 케이스를 Regression의 기존 케이스와 대조해 수정 범위(신규/변경/동일/미영향)를 리포트하고, 한 범위 안의 중복 케이스를 병합·정리합니다. 케이스가 쌓일수록 커지는 관리 비용을 여기서 흡수합니다.
- e2e-snapshot / e2e-script — 등록된 TC를 기반으로 화면을 실측해 POM을 만들고(snapshot), Playwright 스펙을 작성·실행·트리아지(script)하는 스킬.
- run-qms — QMS 자체를 검증하는 스킬. 로그인 화면을 거치지 않고 세션 토큰을 직접 민팅해 localStorage에 주입한 뒤 화면을 훑으며 스크린샷을 남깁니다. 에이전트가 자기가 만든 앱을 스스로 검증하는 경로를 자기 손으로 만든 셈인데, 이게 개발 속도에 가장 크게 기여했습니다. UI를 고칠 때마다 "돌려보고 스크린샷으로 확인해줘" 한 마디면 됐으니까요.
중요한 건, 에이전트의 결과물이 그대로 확정되지 않는다는 점입니다. AI가 자동으로 해주는 영역과 사람이 검토할 내용은 따로 분류하여 테스트케이스의 완성도를 높이는 형태로 설계했습니다.
- testcase-writer가 케이스를 등록하면 'QA확인필요' 항목이 함께 기록되고, Slack의 QA 확인 채널로 담당 QA를 멘션해 검토를 요청합니다. QA가 케이스를 확인·보완한 결과는 QMS에 그대로 남아, 어떤 케이스가 사람의 검토를 거쳤는지 추적할 수 있습니다.
- e2e-script의 실행 결과도 마찬가지입니다. 자동화가 실패하면 Slack으로 알리고, 사람이 ①진짜 기획/구현 불일치 ②스테이징 데이터 부재 ③셀렉터 실수 중 무엇인지 트리아지한 결과가 QMS의 검증 결과 필드에 기록됩니다. raw 실행 결과와 사람의 판정을 따로 두는 이유는 뒤에서 다시 나옵니다.
역할 분담은 명확했습니다. 작성과 구현은 AI 에이전트가, 확인과 결정은 사람이. 스키마를 ENUM으로 갈지 테이블로 갈지, 실패한 자동화를 어떻게 분류할지는 제가 정했고, 마이그레이션 SQL과 909줄짜리 그리드 컴포넌트는 에이전트가 썼습니다. 대신 저는 결정의 이유를 코드 주석과 마이그레이션 파일에 남기게 했습니다. 다음 세션의 에이전트는 기억이 없기 때문에, 주석이 곧 인수인계 문서입니다. 이 습관은 뒤에 나올 사건들에서 몇 번이나 우리를 구했습니다.
QMS의 뼈대 — 서비스 > Test Set > 시스템
스키마의 진화 과정이 그대로 교훈입니다. 첫 마이그레이션에서 시스템 구분은 고정 ENUM이었습니다.
CREATE TYPE tms_system AS ENUM ('OMS', 'DriverApp', 'Admin', '외부연동');
일주일 만에 한계가 왔습니다. 새 시스템 하나 추가하는 데 배포가 필요했으니까요. ENUM을
services / systems 테이블로 갈아엎었고, 이후 지금의 3단 구조가 됐습니다.
여기서 모델링 선택이 하나 있었습니다. 시스템을 Test Set 아래에 물리적으로 중첩시키지 않고, 시나리오/케이스가 (system_id, test_set_id) 두 축을 갖는 "분류 축 모델"로 갔습니다. 시스템 목록은 서비스당 한 벌만 유지되고, 같은 Admin 시스템의 케이스가 Feature 세트에도 Regression 세트에도 존재할 수 있습니다.

케이스 번호는 TMS_FEAT_ADMIN_TC_001 형식으로, (test_set × system) 조합마다 전용 카운터
테이블을 두고 UPSERT로 원자 증가시킵니다. "현재 행 개수 + 1" 방식은 동시 생성 시 같은 번호가
나올 수 있어서(레이스 컨디션) 처음부터 배제했습니다.
INSERT INTO test_set_system_seq (test_set_id, system_id, next_tc_seq)
VALUES ($1, $2, 1)
ON CONFLICT (test_set_id, system_id)
DO UPDATE SET next_tc_seq = test_set_system_seq.next_tc_seq + 1
RETURNING next_tc_seq AS seq;
가장 아픈 대목은 기존 번호의 리넘버링이었습니다. 레거시 번호가 TS_005, TC-001,
TMS_ADMIN_TS_001… 형식이 제각각이라 옛 번호에서 순번을 파싱하는 건 포기하고, 시스템별
row_number()로 새 순번을 통째로 다시 부여했습니다. 부모(user_scenarios.ts_no)와
자식(test_cases.ts_no)을 같이 바꿔야 해서 FK를 임시로 끊고 TEMP TABLE로 old→new 매핑을
만들어 양쪽을 갱신했는데, 한 달 뒤 시스템 코드 rename 기능을 만들 때 이 FK가
NOT DEFERRABLE이라 SET CONSTRAINTS ALL DEFERRED가 조용히 아무 효과도 없다는 걸 실측으로
배웠습니다. 부모를 먼저 바꿔도, 자식을 먼저 바꿔도 매번 위반. 결국 아래 한 줄로 고쳐서 평소엔
즉시 검사, 필요할 때만 트랜잭션 끝으로 미루게 했습니다.
ALTER TABLE test_cases
ALTER CONSTRAINT test_cases_ts_no_fkey DEFERRABLE INITIALLY IMMEDIATE;
이런 설계를 통해 서비스별로 하나의 기능에 대한 Testcase는 시스템에 하나만 존재하며 Test Set 별로 현행화 유지가 가능해 졌습니다.

Play 버튼 — 케이스에서 자동화 실행까지
이 프로젝트의 존재 이유이자 기술적으로 가장 재미있었던 부분입니다. 테스트케이스 그리드의 행마다 ▶ 버튼이 있고, 누르면 그 TC의 Playwright 자동화가 GitLab CI에서 돌고, 결과 스크린샷과 녹화 영상이 그리드로 돌아옵니다.
이는 개발이 완료되고 QA 착수 시점에 바로 자동화 구현을 시작으로 자동화된 영역은 결과 확인 후 매뉴얼 테스트 영역에서 제외하는 방식으로 QA 확인 범위를 좁히는 형태로 테스트 일정을 단축시키고 있습니다. 현재 초기 자동화 성공률은 30~40% 수준입니다. 실패 원인의 대부분은 실제 버그가 아니라 스테이징 환경에 그 케이스가 필요로 하는 사전 데이터(예: 특정 상태의 계약, 배차 중인 오더)가 없어서 발생합니다 — 사람이 미리 데이터를 세팅해두지 않으면 자동화 자체가 통과할 수 없는 구조였던 셈입니다. 이를 해결하기 위해 테스트 실행 직전에 필요한 상태를 API 호출로 직접 만들어주는 Fixture 기능을 추가 개발 중이며, 이를 통해 성공률을 60~70% 수준까지 끌어올리는 것이 목표입니다.

전체 흐름은 이렇습니다.
설계 포인트 몇 가지:
- 스펙 파일 경로가 아니라
--grep태그로 케이스를 고릅니다. 모든test()제목에[tc_no]를 포함하는 컨벤션(e2e-script 스킬이 강제) 덕분에, 스펙 파일이 재배치되거나 개명돼도 Play 버튼은 안 깨집니다. - 상태는 폴링이 아니라 CI 콜백으로 받습니다. 프론트는 TanStack Query의 조건부
refetchInterval로, 실행 중인 행이 하나라도 있을 때만 4초 간격으로 새로고침합니다. - 실행 프로젝트는 서비스별 라우팅. 처음엔 환경변수 하나로 고정(회사의 전체 서비스별로 확장이 필요)이라 새로운 서비스를 붙일 때마다 백엔드 재배포가 필요했습니다. 지금은 서비스 관리 화면에서 GitLab 프로젝트 경로만 넣으면 됩니다.
그리고 대망의 삽질: 결과 영상이 재생되지 않는 문제. 원인은 세 겹이었습니다.
- 자동화 저장소가 private라서, 브라우저가 GitLab web raw URL을 직접 열면 로그인 페이지로
302 리다이렉트됩니다.
<video src>는 그걸 영상이라고 믿고 조용히 실패합니다. - 그럼 서버가 대신 받아오면 되겠지 했는데, web raw 라우트는 세션 쿠키 기반이라 토큰을 줘도
인증이 안 됩니다. 토큰이 통하는 건 REST API의 아티팩트 엔드포인트뿐이라, 저장된 URL을
정규식으로
/api/v4/projects/:id/jobs/:id/artifacts/...형태로 변환하는 함수를 만들어 백엔드가 프록시하게 했습니다. (DB에 저장된 URL만 중계하므로 오픈 프록시는 아닙니다.) - 마지막 반전 — 프록시 자체도 QMS 인증이 필요한데
<img>/<video>태그는 Authorization 헤더를 못 붙입니다. 결국 프론트가 인증된 fetch로 Blob을 받아URL.createObjectURL로 꽂아 넣습니다.
한 가지 원칙도 이때 세웠습니다. CI가 보내는 raw 실행 결과(성공/실패)는 사람이 트리아지한 검증 결과를 절대 덮어쓰지 않습니다. 자동화 실패에는 ①진짜 기획/구현 불일치 ②스테이징 데이터 부재 ③셀렉터 실수, 세 종류가 있고 그 판정은 사람의 것입니다. "데이터가 잠깐 없어서 실패"가 "버그"로 둔갑하는 순간 이 시스템 전체의 신뢰가 무너지니까요.
그리고 현재는 테스트 환경 데이터의 부재 문제를 해결하기 위해 API Call을 통한 데이터 생성 부분을 추가 개발하고 있습니다.
Jira로 이어지는 결함 루프
자동화가 실패를 잡아냈다면 다음은 결함 등록입니다. 그리드의 [Defect 등록] 버튼이 Jira 버그 프로젝트에 이슈를 만들어 줍니다.

- 요약과 설명은 프론트가 [테스트 환경]/[사전 조건]/[테스트 절차]/[예상 결과]/[실제 결과] 템플릿으로 초안을 만들고, QA가 모달에서 다듬어 전송합니다. Jira API v3가 평문을 안 받아서 줄 단위로 ADF 문단을 만드는 변환기를 거칩니다.
- 담당자/보고자는 클릭한 사용자의 이메일로 Jira 계정을 조회하되, 못 찾아도 등록은 계속합니다. 부가 정보의 조회 실패가 본 기능을 막아선 안 된다는 원칙.
- 자동화 실행의 스크린샷과 영상은 이슈에 자동 첨부됩니다. 여기서도 private repo가 한 번 더 발목을 잡았는데 — 인증 없이 fetch한 로그인 페이지 HTML(200 OK)을 스크린샷이라며 Jira에 업로드하려다 실패한 적이 있습니다. 게다가 첨부만 조용히 빠지고 이슈는 정상 생성돼서, 서버 로그를 보기 전까지는 실패한 줄도 몰랐습니다.
- 결함 이력(
tc_defects)의 FK는tc_no가 아니라 불변 uuid입니다. 케이스 이동이 tc_no를 리넘버해도 이력이 끊기지 않아야 하니까요.
회고 — 한 달의 개발...
에이전트는 구현 속도가 아니라 "루프 완성"을 가능하게 했다. 케이스 작성 → QA 확인 → 자동화 실행 → 결함 등록 → 이력 관리라는 루프는 어느 한 조각만 있으면 의미가 없습니다. 개발자도 아닌 제가 AI를 통한 개발이 아니었다면 그리드 하나 만들다 분기가 끝났을 겁니다. 밀도 있는 한 달로 루프 전체를 닫을 수 있었던 건 순전히 구현을 담당할 병력이 있었기 때문입니다.
AI 시대의 커버리지 증가에 대한 답은 "AI가 다 한다"가 아니라 "AI가 만들고 사람이 확정한다"였다. 케이스 작성도, 자동화 실패의 판정도 최종 확인은 사람에게 남겼습니다. testcase-writer의 Slack 확인 요청, e2e-script의 실패 트리아지가 그 장치입니다. 이 확인 루프가 있었기에 에이전트에게 작성량을 마음 놓고 맡길 수 있었고, 그 결과가 QMS에 기록으로 쌓이면서 시스템에 대한 팀의 신뢰도 함께 쌓였습니다.
결정의 이유를 코드에 남기는 습관이 협업의 절반이었다. 에이전트는 세션이 바뀌면 아무것도 기억하지 못합니다. "왜 ENUM을 버렸는지", "왜 raw 결과가 검증 결과를 덮어쓰면 안 되는지"를 마이그레이션 주석에 남겨두자, 다음 세션의 에이전트가 그 원칙을 스스로 지키기 시작했습니다. 사람 신입에게 하는 온보딩과 정확히 같은 원리입니다.
그리고 도구는 쓰는 사람을 닮아갑니다. QMS의 API 토큰 인증, --grep 컨벤션, 트리아지
원칙은 전부 "에이전트가 쓰기 좋은 시스템"을 고민한 결과물이고, 그 결과 QMS는 사람과 에이전트가
같은 문으로 드나드는 도구가 됐습니다.
그리고 개인적인 생각이지만 기획/개발/QA가 모두 AI를 사용하는 시대에 QA는 하나의 프로젝트, 스프린트 단위의 테스트가 아닌 점점 서비스 전체의 요구사항이나 개발 코드에 의한 회귀를 막기 위한 활동이 점점 중요한 과제가 된다고 생각합니다. 해결을 위한 방법은 부분 변경이 이루어지더라도 최대한 엄격한 자동화된 Regression Test Set을 실행하는 것으로 회귀를 막을 수 있다고 판단하고 있고 이를 목표로 열심히 QA 프로세스를 다듬고 있습니다.
혹시 이와 같은 여정을 함께 하실 QA 엔지니어 분들의 연락도 기다립니다!! 👉 채용 공고 바로가기
스택: QMS는 Express + TypeScript + PostgreSQL 백엔드, React + Vite + TanStack Table/Query 프론트엔드, Docker Compose로 셀프호스팅 중입니다(현재는 사내 서버에서 운영하고 있으며, 추후 클라우드 환경으로 이관할 예정입니다). 글에 등장한 코드와 사건은 모두 실제 커밋 기록에서 가져왔고, 스크린샷은 데모 데이터로 재현한 화면입니다.