2026년 7월 23일
사람 하나, AI 둘: 프론트·백엔드 통합 테스트를 에이전트 협업으로 돌려본 기록
들어가며
현장 파트너용 모바일 웹앱을 새로 만들었다. 화면 설계는 Figma에, API 계약은 OpenAPI 문서에 있었고, 백엔드는 아직 만들어지는 중이었다. 이 프로젝트의 질문은 두 가지였다. 문서와 디자인만 있는 상태에서 프론트를 어디까지 신뢰할 수 있게 구현할 수 있을까, 그리고 백엔드가 붙었을 때 그 신뢰를 어떻게 검증할까.
결과부터 말하면, 프론트는 계약 기반(contract-first)으로 구현했고, 마지막엔 프론트 에이전트와 백엔드 에이전트 두 대를 동시에 띄워 로컬 백엔드에 대고 통합 테스트를 돌렸다. 검증 동작 52건(프론트 집계), 로컬에서 검증 가능한 범위에서 계약 정합 100%, 프론트 코드 결함 0건으로 마무리했다.
1. 문서와 이미지로 구현하기 — 계약을 단일 진실로
백엔드가 없는 상태에서 프론트를 “추측”으로 짜면 나중에 전부 다시 만져야 한다. 그래서 처음부터 원칙을 세웠다.
- 타입은 손으로 쓰지 않는다. OpenAPI 스냅샷에서 타입을 코드 생성(codegen)하고, 컴포넌트는 그 타입만 소비한다. 계약이 바뀌면 타입을 다시 뽑고, 깨지는 지점을 컴파일러가 알려주게 했다.
- 디자인은 원본을 본다. Figma에서 내보낸 화면 수십 장을 실제 구현과 한 장씩 대조했다. “대충 비슷하게”가 아니라 필드·상태·인터랙션 단위로 디자인과 코드의 차이를 표로 만들었다. 이 Figma 기획·디자인을 읽어 화면과 정책을 교정하는 일은 전용 디자인 에이전트가 맡았다. (뒤의 QA를 담당하는 프론트·백엔드 에이전트와 별개로)
- 모르는 것은 문서로 남긴다. 계약과 디자인이 어긋나거나 백엔드에 아직 없는 필드가 필요할 때마다 그것을 “갭 문서”에 누적했다. 각 항목은 현재 계약 / 필요한 계약 / 프론트 소비 위치 / 참고 디자인을 담았고, 해결되면 상태를 갱신했다.
이 갭 문서가 나중에 백엔드 구현자에게 그대로 인수인계 자료가 됐다. 프론트가 “이 필드가 이래서 필요하다”를 미리 정리해 두니, 백엔드는 추측 없이 계약을 채워 넣을 수 있었다.
2. 완성은 한 번에 오지 않는다 — 반복적으로 다지기
기능이 “돌아가는 것”과 “믿을 수 있는 것”은 다르다. 그 간극을 메운 건 반복적인 리뷰였다.
- 코드리뷰 반영 — 자동 리뷰가 짚은 것들(에러 객체 안전 접근, 조건부 헤더 설정, 상태 코드 비교 방식 등)을 하나씩 정리했다.
- 스타일 감사 — 같은 모노레포의 다른 앱들과 대조해, 뮤테이션 에러 처리·경로 별칭·세그먼트 네이밍 같은 컨벤션을 일치시켰다. “이 저장소에서 자연스러운 코드”가 되도록.
- 시큐어코딩 관점 — 토큰을 로컬 스토리지에 두지 않고 메모리에만 유지, 갱신은 httpOnly 쿠키로, 401 처리는 단일 비행(single-flight)으로. 인증 흐름은 특히 여러 번 다시 봤다.
- 테스트 — 순수 로직(상태 판정, 완결성 검증 등)에 단위 테스트를 붙여, 계약이 바뀌어도 회귀를 잡게 했다.
화려한 단계는 아니지만, 통합 테스트에서 “프론트 결함 0”이 나온 건 대부분 이 지루한 반복 덕분이었다.
3. 핵심 — AI 에이전트 두 대로 실시간 통합 테스트
통합 테스트의 전제는 재현 가능한 로컬 백엔드였다. 공유 개발 서버에 붙는 대신, 백엔드와 DB를 각자 로컬에 독립으로 띄웠다. DB는 두 갈래로 채웠다 — 개발 DB의 스키마만 떠와 로컬의 누락 테이블을 데이터 손실 없이 메우고(스키마 드리프트 보정), 개인정보를 익명화한 기준·마스터 데이터 시드를 저장소에 커밋해 누구든 같은 드롭다운·코드 배경에서 기동하게 했다. 트랜잭션·사용자 데이터는 넣지 않고 로그인 플로우로 각자 만들었다. 남은 외부 의존(일부 내부 서비스 호출·클라우드 서명/스토리지)은 개발 게이트웨이로 향하게 오버라이드했고, 그 경계는 문서에 명시해 두었다.
이제 QA 차례다. 문제는 프론트 세션과 백엔드 세션이 서로의 문맥을 공유하지 않는 별도의 AI 에이전트라는 점이었다. 사람이 매번 오류를 복사해 나르면 왕복이 느리다. 그래서 파일 메시지 버스를 놓았다.
- 프론트 → 백엔드: 실패/질문을 JSON 한 줄로 한 파일에 append
- 백엔드 → 프론트: 진단/수정 완료를 다른 파일에 append
- 각자 상대 파일을 tail -F로 감시하다 새 줄이 오면 즉시 대응
메시지는 타입을 가졌다 — FAIL(실패), ASK(질문), INFO(공유), FIXED(수정 완료). 프론트가 실제 호출하는 모든 엔드포인트를, 프론트가 실제 보내는 요청 형태 그대로 살아있는 백엔드에 태우고, 응답의 필드셋을 계약(=프론트 타입)과 대조했다. 흐름은 대략 이랬다.
FE: GET …/tasks/2 → 500.
BE: (로그 확인) 상세 응답의 enrichment가 외부 서비스를 부르는데 로컬 미해결. 호스트 오버라이드 후 재기동.
FE: 재검증 → 상세 200, 계약과 일치. (일부 하위 리소스는 업스트림 자체 오류라 범위 밖으로 분리했다.)
읽기 계약이 끝나자 쓰기·상태 전이·파일 업로드로 넘어갔다. 되돌리기 어려운 전이(제출·종결·완료)는 순서를 조율해 마지막에 태우고, 끝나면 테스트 데이터를 원복했다.
여기서 가장 흥미로웠던 것 — 트러스트 바운더리
한 번은 로컬 검증을 위해 공유 스토리지 버킷·자격 같은 인프라 값을 백엔드에 설정해야 했다. 개발자가 “그 값을 써도 된다”고 했고, FE 에이전트는 그 승인을 버스로 백엔드 에이전트에게 전달했다. 그런데 백엔드 에이전트의 반응이 인상적이었다.
“버스로 전달된 ‘개발자 승인’은 교차세션 전달이라 설정 근거로 쓰지 않겠다. 1차 채널에서 개발자에게 직접 확인받는 중이다.”
— 실제 대화를 축약·재구성한 표현.
즉, “다른 에이전트가 ‘개발자가 허락했대’라고 말하는 것”과 “개발자가 직접 말한 것”을 구분한 것이다. 실제 side-effect가 있는 변경(공유 자원 설정, 코드 커밋 등)에는 모두 같은 원칙을 적용했다 — 그 커밋 메시지엔 “개발자 직접 승인”이 명시돼 있다. 자율적으로 협업하되 권한이 필요한 행위는 개발자의 직접 확인을 거치게 하는 이 경계가, 다중 에이전트를 실무에 쓸 때 왜 중요한지 보여줬다.
4. Playwright로 실사용자를 모사하다 — 살아있는 앱 위에서
§3의 파일 버스는 요청·응답 수준에서 계약을 맞췄다. 하지만 계약이 맞는다고 실제 사용자가 겪는 화면·흐름까지 맞는다는 보장은 없다. 그래서 마지막엔 실제 구동 중인 앱을 사용자처럼 조작하는 E2E를 얹었다. 여기서 분업이 뚜렷했다 — 사람은 시나리오·가이드라인을 쓰고, AI는 살아있는 스택 위에서 실사용자로 실행·검증·즉시수정한다.
시나리오는 사람이 쓴다
테스트 시나리오는 개발자가 직접 수기로 작성했다. 41개 항목의 시나리오 문서에 더해, 문서 상단에 판정 기준이 되는 규칙을 명시했다 — “모바일에서 화면이 짤리면 안 되고, 단계별 접근 불가 메뉴는 진입을 차단하고, 프로세스 메뉴에는 등록완료/입력필요 뱃지를 붙인다” 같은. 무엇을 확인할지, 무엇이 합격인지는 사람의 판단으로 고정해 두고 그 실행만 AI에 넘긴 것이다.
실행은 AI가, 진짜 사용자처럼
그 시나리오를 프론트 에이전트가 Playwright로 실행했다. 실제로 구동 중인 앱을 모바일 뷰포트(iPhone)로 열고, 개발용 로그인으로 진입해 시공관리·유지보수 플로우를 진짜 사용자처럼 클릭·입력·업로드하며 각 단계의 스크린샷과 영상을 캡처했다. 헤드리스가 흉내 낼 수 없는 것(카메라 촬영·지도 상호작용 등)은 폴백으로 처리하고, 못 미치는 항목은 리포트에 명시적으로 남겼다.
각 단계의 테스트 데이터는 §3의 파일 버스로 백엔드 에이전트와 협업해 실제 비즈니스 로직 기준으로 준비했다. 화면 하나를 넘어가려면 그에 맞는 상태가 백엔드에 있어야 하니, 신규 시공건 생성 → 출고 → 입고 → … 같은 상태 전이를 백엔드 쪽에 요청해 실제 레코드로 만들었다. UI 조작은 프론트가, 그 조작이 성립할 데이터는 백엔드가 — 두 에이전트가 하나의 시나리오를 나눠 든 셈이다.
발견하면 그 자리에서 고친다
실행 중 드러난 갭은 미루지 않고 즉시 프론트를 고치고 재검증했다.
- 단계 게이팅 — 아직 도달하지 않은 미래 단계의 메뉴로는 진입하지 못하게 막았다.
- 프로세스 메뉴 뱃지 — 각 단계에 등록완료 / 입력필요 상태 뱃지를 붙여, 무엇이 남았는지 한눈에 보이게 했다.
- 생성일 날짜 포맷 — 응답의 ISO 문자열을 화면 규칙(YYYY.MM.DD)으로 정리했다.
즉, E2E는 “통과/실패를 집계하는 관문”이 아니라 실사용자 시점의 결함을 드러내고 곧바로 메우는 작업대였다.
검증에서 끝나지 않는다 — 공유 가능한 리포트로
이 작업의 마지막 산출물은 초록불이 아니라 사람이 읽고 공유할 수 있는 리포트였다. 실행 결과(캡처·발견·분류·수정)를 자체완결형 HTML 리포트 한 장으로 묶어 내부(Slack·Notion)에 공유할 수 있게 했다. 이미지를 파일 안에 인라인해 어디서 열어도 그림이 깨지지 않게 했고, 원본 HTML을 그대로 렌더하지 않는 Notion에는 PNG로도 붙일 수 있게 준비했다. “AI가 검증만 하고 끝”이 아니라, 검증의 결과가 사람이 읽고 다음 사람에게 넘길 수 있는 형태로 남는다는 점이 중요했다. (지금 읽고 있는 이 글도 같은 방식으로 만든 자체완결 리포트다.)
5. 결과와 발견
전 범위(읽기·쓰기·상태 전이·파일 업로드)를 검증했고, 요지는 이렇다.
- 계약 정합 100% (로컬에서 검증 가능한 범위·수정 반영 후 기준) — 백엔드 실제 응답의 필드셋이 프론트 타입과 정확히 일치(드리프트 0). 업스트림 자체 오류이던 일부 하위 리소스는 로컬 완전 검증 대상에서 제외했다.
- 프론트 코드 결함 0건 — 발견된 문제는 전부 프론트 밖이었다.
발견된 이슈는 유형이 명확히 갈렸다. 이 분류 자체가 통합 테스트의 값어치였다.

마지막 줄이 특히 교훈적이다. 파일을 올리자마자 조회하니 404가 났고, 처음엔 버그로 의심했다. 백엔드 로그를 대조하니 업로드 직후엔 미매핑(비활성) 상태이고, 참조에 첨부돼야 활성화돼 조회 가능한 설계였다. 프론트의 실제 플로우(업로드 → 매핑 → 조회)로 다시 태우니 전부 정상. “버그처럼 보이는 것”을 상대 로그와 대조해 “설계”로 확정하는 과정 역시 통합 테스트가 아니면 못 걸렀다.
6. 백엔드 쪽에서 본 협업
같은 QA라도 반대편에서 보면 다르게 보인다. 협업한 백엔드 쪽이 정리해 준 관점 중, 다중 에이전트 협업이라는 이 글의 주제에 맞닿은 세 가지만 옮긴다. (아키텍처 상세는 이 글의 범위를 넘어 생략한다.)
PASS는 증거가 아니라 주장이다
두 세션은 문맥도, 도구 승인 범위도 공유하지 않았다. 그래서 백엔드는 프론트의 “PASS”를 액면으로 믿지 않고, 모든 결과를 실제 로그와 DB로 독립 재현한 뒤에만 “해결”로 처리했다. 공유되는 권위 있는 상태는 셋뿐 — 살아있는 백엔드, OpenAPI 계약, 파일 버스. 공유 메모리가 아니라 계약 + 관측 가능한 실행 상태로 합의한 것이, 마이크로서비스의 신뢰 경계와 같은 모양이었다.
수정은 “올바른 경계”에서
이 앱은 외부 협력사용이고 사내 운영 앱은 내부 직원용인데, 둘은 같은 코어(공유 비즈니스 로직)를 공유한다. 원칙은 “계층은 공유하되 신뢰 모델은 공유하지 않는다”였다 — 외부 경계에서는 접근 권한(누가 어떤 자원의 소유자인지)을 형제 앱에서 물려받지 않고 스스로 다시 검증했다. 핵심은 수정을 프레젠테이션 계층에만 두고 공유 비즈니스 로직은 건드리지 않은 것이다. 접근 검증·예외 변환 같은 처리를 각 앱이 자기 프레젠테이션 계층(웹/엣지)에서 하니, 공유 코어의 비즈니스 로직이 오염되지 않는다. 그 결과 이 앱의 수정이 같은 코어를 공유하는 형제 앱의 프레젠테이션 로직으로 사이드이펙트를 전파하지 않는다. (예: QA가 드러낸 “정당한 실패가 500으로 새던” 문제도, 공유 코어가 아니라 이 앱의 프레젠테이션 계층에서 4xx로 다시 매핑해 — 형제 앱엔 영향 없이 — 사용자용 메시지를 살렸다.)
QA는 해피패스가 가린 모호함을 드러낸다
가장 값진 발견은 화려한 버그가 아니라 “부재(不在)의 모호함”이었다. 빈 목록을 “전부 삭제”로 오해해 기존 첨부를 날릴 뻔하거나, null을 잘못 해석해 엉뚱한 권한 오류를 내던 것들. 교훈은 하나다 — nullable·optional 필드는 “생략 = 미변경”과 “빈값 = 삭제”의 규약을 명시하고 테스트해야 한다. 덧붙여, 미설정 값이 그럴듯한 기본값(test 같은)으로 조용히 폴백되면 빈값보다 위험하다.
7. 배운 것
- 계약을 단일 진실로 두면, 구현과 검증이 같은 자를 쓴다. 타입을 코드 생성하고 응답을 그 타입과 대조하니, “프론트가 읽는 필드”와 “백엔드가 주는 필드”의 diff가 기계적으로 드러났다.
- 모르는 것을 문서로 남기는 습관이 인수인계가 된다. 갭 문서는 검증 체크리스트이자, 다음 사람에게 넘길 지식이었다.
- 다중 에이전트 협업엔 공유 상태와 트러스트 바운더리가 필요하다. 문맥을 공유하지 않는 두 에이전트에겐 “살아있는 백엔드 + 하나의 공유 문서/채널”이 유일한 접점이었고, 권한이 필요한 행위는 사람 확인을 거치게 하는 경계가 안전을 지켰다.
- 통합 테스트의 진짜 산출물은 ‘숫자’가 아니라 ‘분류’다. 몇 건 통과냐보다, 각 실패가 프론트/백엔드/인프라/오해 중 무엇인지 가르는 게 이후 조치를 결정했다.
- 수정은 올바른 경계에서. 백엔드는 여러 수정을 공유 비즈니스 로직이 아니라 각 앱의 프레젠테이션 계층에만 두어, 형제 앱으로 사이드이펙트가 전파되지 않게 했다. “여러 소비자가 이 계층을 공유한다”는 인식이 어디를 고칠지를 결정했다.
- 사람은 시나리오를, AI는 실행을. 무엇을 확인하고 무엇이 합격인지는 사람이 수기 시나리오·가이드라인으로 고정하고, 살아있는 앱을 실사용자로 구동·검증·즉시수정하는 반복은 AI가 맡는 분업이 가장 잘 굴러갔다. 계약 수준 검증만으로는 실사용자가 겪는 화면·게이팅·포맷을 다 잡지 못했다.
- 검증의 끝은 초록불이 아니라 리포트다. 실행 결과를 이미지까지 인라인한 자체완결 리포트로 남기니, “AI가 검증만 하고 끝”이 아니라 사람이 읽고 공유하고 다음 사람에게 넘길 수 있는 산출물이 됐다.
8. 비용·토큰 관점 — AI 통합 테스트의 ROI
※ 아래 수치는 방법론 기반 추정치이며 실측값이 아니다(실측은 비공개). 계산 가정을 함께 밝혀 독자가 자기 상황에 대입할 수 있게 했다.
AI로 통합 테스트를 돌리면 “토큰을 얼마나 쓰나, 그만한 값을 하나”가 자연스러운 질문이다. 이 세션은 스크린샷 판독·대용량 로그/파일 읽기·장시간 자율 실행으로 토큰을 꽤 썼다 — 화면 이미지 한 장을 컨텍스트로 읽을 때마다 수천 토큰, 반복 grep·빌드 로그도 누적된다.
총 토큰 (추정)
누적 약 900만 토큰 규모로 추정한다. 대부분은 매 요청마다 재전송되는 입력(시스템 프롬프트·도구 정의·누적 대화)이고, 실제 출력은 약 40만 토큰 수준이다. 긴 세션일수록 입력 재전송이 총량을 지배한다.
캐시 히트 절감 (추정)
프롬프트 캐시는 캐시 읽기 토큰을 신규 입력가의 약 1/10로 청구한다. 같은 시스템 프롬프트와 누적 대화가 매 요청 재사용되고, 이 세션은 캐시 TTL이 넉넉해 대부분의 요청이 캐시 히트로 이어졌다. 그래서 입력을 전부 신규 청구했을 때 대비 입력 비용의 약 85%를 절감한 것으로 추정한다. 반복 도구 호출이 많은 e2e일수록 이 절감이 크다.
사람 QC 대비 가치 (추정 · 가정 명시)

단, 완전 대체는 아니다. AI는 헤드리스 제약(카메라·지도·클립보드 일부)과 백엔드 테스트 데이터 의존이 있다. 현실적인 그림은 AI가 1차 스윕(반복 실행·계약 대조·회귀·증거 캡처)을 자동화하고, 사람은 판정·엣지케이스·최종 승인에 집중하는 분업이다. 이 세션에서도 데이터 생성·상태 전이는 백엔드 쪽(사람 확인을 거친)에 의존했고, 헤드리스로 못 미치는 항목은 명시적으로 남겨 두었다.
요컨대, “얼마나 썼나”보다 “무엇을 자동화해 사람 시간을 어디에 남겼나”가 ROI의 핵심이다. 토큰은 반복 검증과 즉시 수정에 흘러갔고, 사람의 시간은 판단이 필요한 곳에 남았다.
