「프론트엔드 테스트 제대로 하기」 시리즈의 14편입니다. 전체 목차 보기 · 용어집

“내 컴퓨터에선 되는데 CI에선 빨간불.” E2E를 운영하는 모든 개발자의 숙제이자, console.log를 도배하게 만드는 주범이죠. Playwright에는 이 미스터리를 증거 기반으로 푸는 도구가 셋 내장돼 있습니다 — 실패 순간을 통째로 되감는 트레이스, 시간여행하듯 보는 UI 모드, 그리고 신호로 쓰는 리트라이. 이 글에서 셋 다 챙깁니다.

시리즈 독자라면 지금까지 만든 E2E가 언젠가 깨질 때를 위한 보험이고, 검색으로 오셨다면 지금 깨져 있는 그 테스트를 위한 응급 키트예요. 예제는 자급자족합니다.

실습 코드: 이 편의 상태가 step-14 태그에 담겨 있습니다(트레이스·디버깅은 이미 있던 설정과 CLI 도구를 쓰는 편이라 새 코드는 없어요 — 픽스처까지 갖춘 13편과 같은 상태입니다). StackBlitz에서 바로 열기도 됩니다.

실패를 추적하는 세 가지 도구를 챙깁니다.

  • 트레이스 뷰어로 실패 순간 되감기
  • UI 모드·디버그 모드
  • 리트라이를 ‘해결’이 아니라 ‘신호’로 쓰기
Playwright 트레이스 뷰어에서 실패한 테스트의 단계별 스냅샷을 되감아 보는 화면
Playwright 트레이스 뷰어에서 실패한 테스트의 단계별 스냅샷을 되감아 보는 화면

왜 하필 CI에서만 깨질까

미스터리부터 걷어냅시다. CI 머신은 여러분 노트북과 다른 환경입니다 — 대개 더 느리고(로딩이 늦어 타이밍 이슈가 잘 드러남), 병렬로 돌고, 화면 없는 헤드리스 모드에, 타임존·로케일도 다를 수 있어요. 그러니까 “CI에서만 깨진다"는 초자연 현상이 아니라, 로컬에선 운 좋게 안 걸리던 문제가 더 가혹한 조건에서 걸린 것에 가깝습니다. 로케이터 편의 flaky 원인들이 CI에서 유독 잘 터지는 이유죠.

문제는 재현이에요. 내 컴퓨터에선 안 깨지니 들여다볼 수가 없죠. 그래서 필요한 게 — 실패 순간의 블랙박스입니다.


트레이스 뷰어

repo 설정엔 이미 trace: 'on-first-retry'가 들어 있어요 — 첫 재시도부터 트레이스를 기록합니다. 실패한 테스트의 모든 단계를 스냅샷·네트워크·콘솔과 함께 되감아 볼 수 있죠.

bash
# 항상 기록하고 싶을 때
npx playwright test --trace on

# 기록된 트레이스 열기
npx playwright show-trace test-results/**/trace.zip

실패한 테스트를 --trace on으로 돌리면 test-results/에 이런 실물이 남습니다.

test-results/
└── dashboard-대시보드에서-사용자를-검색한다-chromium/
    ├── trace.zip          ← 블랙박스 본체
    └── error-context.md   ← 에러·코드 위치를 정리한 마크다운 (AI에게 바로 던질 수 있는 형태)

error-context.md가 재미있어요 — 열어보면 “이 테스트가 왜 실패했는지 설명하고 수정 코드를 제안하라"는 지시문과 함께 실패 정보가 정리돼 있습니다. AI에게 붙여넣기 좋게 만들어둔 거예요(이 시리즈 후반의 AI 편들에서 다시 만납니다).

트레이스를 열면 뭐가 보이냐면 — 실행된 액션 목록(goto, fill, expect…)이 타임라인으로 늘어서 있고, 아무 액션이나 클릭하면 그 순간의 화면(전/후 DOM 스냅샷), 그때 오간 네트워크 요청, 콘솔 출력까지 통째로 재생됩니다. 실제 수사는 이렇게 흘러가요.

  1. CI가 빨간불 — 리포트를 아티팩트로 업로드해두면(다음 장에서 다룹니다) trace.zip을 내려받습니다
  2. show-trace로 열고, 빨갛게 표시된 실패 액션을 클릭
  3. 그 순간의 화면을 봅니다 — “아, 아직 로딩 스피너네” 혹은 “모달이 버튼을 가리고 있네”
  4. 네트워크 탭 확인 — 응답이 늦었나? 500이었나?
  5. 원인 확정. 추측이 아니라 목격으로요
CI 실패에서 원인 확정까지의 트레이스 수사 흐름 다이어그램 - CI 빨간불에서 trace.zip 아티팩트를 내려받아 트레이스 뷰어로 열고, 실패한 액션을 클릭해 그 순간의 DOM 스냅샷과 네트워크, 콘솔을 확인한 뒤 원인을 확정하는 다섯 단계입니다. 로컬에서 재현하지 못해도 증거로 원인을 목격한다는 설명이 붙어 있습니다
CI 실패에서 원인 확정까지의 트레이스 수사 흐름 다이어그램 - CI 빨간불에서 trace.zip 아티팩트를 내려받아 트레이스 뷰어로 열고, 실패한 액션을 클릭해 그 순간의 DOM 스냅샷과 네트워크, 콘솔을 확인한 뒤 원인을 확정하는 다섯 단계입니다. 로컬에서 재현하지 못해도 증거로 원인을 목격한다는 설명이 붙어 있습니다

로컬 재현이 안 되는 실패를 재현 없이 푸는 것 — 이게 트레이스의 존재 이유입니다.


UI 모드와 디버그 모드

bash
npx playwright test --ui       # 타임라인 보며 실행·재실행
npx playwright test --debug    # 한 스텝씩 멈추며 진행

UI 모드는 각 액션 시점의 DOM 스냅샷을 시간여행하듯 보여줘요. 어디서 멈췄는지 눈으로 확인하면 디버깅 속도가 확 달라집니다.


리트라이는 신호다

ts
// playwright.config.ts (repo 설정에 포함)
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,

리트라이로 통과하면 어떻게 보이는지, 일부러 첫 시도에만 실패하는 테스트를 만들어 돌려봤습니다.

bash
Running 1 test using 1 worker
1 [chromium] › e2e/tmp-flaky.spec.ts:2:1 › 가끔 실패하는 테스트 (337ms)
2 [chromium] › e2e/tmp-flaky.spec.ts:2:1 › 가끔 실패하는 테스트 (retry #1) (174ms)

  1 flaky

주목할 건 마지막 판정입니다 — 재시도로 결국 통과했는데도 1 passed가 아니라 1 flaky. Playwright는 “한 번 실패했었다"는 사실을 덮어주지 않아요. 그리고 그 딱지가 바로 신호입니다: 리트라이로 통과한 테스트는 이미 아픈 테스트예요. 방치하지 말고 로케이터 편의 원칙으로 돌아가 근본 원인을 잡는 게 맞습니다.

덤으로 forbidOnly — 디버깅하다 남긴 test.only가 CI로 흘러들어 “테스트 1개만 돌고 전부 초록불"이 되는 사고를 막아줍니다.


한 장 요약

  • 트레이스 = 실패한 테스트의 블랙박스 — 단계별 DOM 스냅샷·네트워크·콘솔을 되감아 봄 (--trace on, 열기는 show-trace)
  • repo 설정은 trace: 'on-first-retry' — 재시도가 시작되는 순간부터 자동 기록
  • 로컬 디버깅은 --ui(타임라인 시간여행)와 --debug(한 스텝씩) — console.log 도배와 작별
  • 리트라이는 해결이 아니라 신호 — 리트라이로 통과한 테스트는 이미 아픈 테스트, 로케이터 원칙으로 근본 치료
  • forbidOnlytest.only가 CI에 흘러드는 사고 방지 — 리포트는 CI 아티팩트로 남겨 원격 부검

미스터리는 풀라고 있는 겁니다

이제 실패를 두려워하지 않게 됐어요. 다음 편은 이 챕터의 접근성 하이라이트 — axe로 페이지를 통째로 스캔합니다.

트레이스 뷰어를 처음 열면 “이걸 왜 이제 알았지” 싶어요. console.log 도배로 디버깅하던 날들이여, 안녕.

레벨업: CI에서만 깨지는 테스트도 트레이스로 원인을 추적할 수 있습니다.

다음 편: 접근성 E2E — @axe-core/playwright로 스캔

낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.