정의
Playwright flaky test는 첫 실행에서 실패했지만 자동 retry에서 성공한 테스트다. failed는 첫 실행과 모든 retry가 동일한 이유로 실패하는 재현 가능한(deterministic) 결함이다. CI job이 빨간색이어도 이 둘을 먼저 분리해야 수정 방향(테스트 안정화 vs 앱/빌드 버그)이 달라진다.
왜 필요한가
E2E(End-to-End) 테스트는 네트워크 지연, SSE(Server-Sent Events, 서버가 브라우저로 이벤트를 밀어주는 연결), 포털 UI, hydration(서버 HTML에 클라이언트 이벤트를 연결하는 과정) 타이밍에 민감하다. 팀이 "flaky를 고칠지" "앱 버그를 고칠지"를 혼동하면 같은 빨간 CI 로그를 두고 엇갈린 작업을 한다.
failOnFlakyTests: true 설정에서는 retry가 성공해도 CI exit code가 1이라, GitHub conclusion만 보면 flaky와 failed가 구분되지 않는다. Playwright 리포트·stdout의 per-attempt 결과를 봐야 한다.
동작 원리
- passed: 첫
test()실행 성공. - flaky: attempt 1 실패 → retry 성공. 원인은 대개 순서 경쟁(타이머, focus, 네트워크 predicate).
- failed: 모든 attempt가 같은 assertion/예외. 원인은 앱 로직, 환경, production 빌드 산출물(예: CSS가
<script>로 로드). - 진단 깊이:
pageerror는 메시지만 있는 경우가 많다. CDP(Chrome DevTools Protocol)Runtime.exceptionThrown은 예외 URL·lineNumber를 준다. - 재현: dev 서버가 아닌
production build+next start(또는 동등 환경)로 실패 테스트를 돌린다. 원인 설정 on/off A/B 빌드로 가설을 검증한다.
흔한 flaky 패턴:
| 패턴 | 증상 | 대안 |
|---|---|---|
고정 setTimeout + SSE refetch | 퍼센트·상태 assertion 순서 뒤바뀜 | Promise gate로 이벤트 순서 통제 |
| Radix Select 키보드 | portal option focus 전 키 입력 | option visible·focused 대기 |
networkidle | 클릭 무반응 | dialog open·button enabled 등 semantic 대기 |
느슨한 waitForRequest | 다른 API 요청을 먼저 잡음 | method·pathname·query 일치 predicate |
실무 적용
CI 조사 절차:
- 기간 내 전체 push 워크플로 로그 수집(샘플 몇 건만 보지 않음).
- 각 테스트를 passed / flaky / failed로 태깅.
- flaky 상위는 테스트 안정화, 100% failed는 production 재현 + CDP.
- custom webpack
splitChunks변경 후 HTML의 script 태그·build manifest에서 CSS가 JS entry로 섞였는지 확인.
트레이드오프
- retry를 늘리면 flaky는 통과하지만 실제 불안정성은 남는다.
failOnFlakyTests는 그걸 CI에서 드러낸다. - CDP 연결은 로컬 재현에 유리하지만 CI마다 붙이기 어렵다 — stdout 파싱·artifact 정책을 별도로 둔다.
networkidle·sleep은 작성은 쉽지만 flake를 가려 재발한다.
사용하면 안 되는 경우
- 단위 테스트 수준 로직은 Playwright triage가 과하다 — Vitest 등으로 격리한다.
- 인프라 장애(artifact 업로드 실패, 디스크 full)로 로그가 없을 때 flaky 비율을 추정하지 않는다.
- flaky 테스트에서 assertion을 삭제해 CI를 초록으로 만드는 것은 결함 은닉이다.
흔한 실수
- GitHub job conclusion = flaky로 가정한다.
group-delete처럼 123/123 동일 SyntaxError를 flaky로 부른다 — deterministic failed다.- dev 서버만 돌려 production 빌드 결함을 놓친다.
page.route()만으로 SSR 인증 요청을 mock하려다 서버·브라우저 mock 불일치가 난다.
관련 개념
- webpack-splitchunks-vendor-shared-chunk — CSS가 JS entry로 들어가
<script src="*.css">가 생기는 함정 - ssr-hydration-mismatch —
networkidle≠ hydration 완료 - react-render-count-isolation-testing — 렌더 횟수 격리 단위 테스트