Playwright는 Auto-waiting 기능 덕분에 Selenium보다 Flaky Test(불안정한 테스트)가 적게 발생한다고 알려져 있지만, 완전히 없어지지는 않습니다. 「로컬에서는 통과하는데 CI에서만 실패한다」 「가끔 실패하지만 재실행하면 통과된다」——이런 증상으로 고민하는 QA 엔지니어는 적지 않습니다. 이 글에서는 Playwright 특유의 Flaky Test 원인을 진단하고, Auto-waiting·Locator 설계·Trace Viewer를 활용한 해결책을 실무 관점에서 정리합니다.
Flaky Test를 「어쩌다 그런 것」으로 넘기고 리트라이에만 의존하면, 근본 원인이 쌓여 테스트 스위트 전체의 신뢰성이 서서히 떨어집니다. 원인을 진단해서 고치는 습관이 장기적으로는 리트라이보다 효과적입니다.
📌 이런 분께 추천합니다
- Playwright 테스트가 「가끔 실패하는」 현상으로 고민 중인 QA 엔지니어·개발자
- CI에서는 실패하는데 로컬에서는 재현되지 않는 경험이 있는 분
- Auto-waiting의 동작 원리를 정확히 이해하지 못한 분
- Trace Viewer를 활용한 원인 조사 흐름을 알고 싶은 분
✅ 이 글을 읽으면 얻을 수 있는 것
- Playwright 특유의 Flaky Test 원인을 4가지 패턴으로 분류해 이해할 수 있다
- Auto-waiting·Locator·타임아웃 설정의 점검 포인트를 파악할 수 있다
- Trace Viewer를 활용한 원인 조사의 구체적인 절차를 구현할 수 있다
👤 이 글을 쓴 사람
QA 엔지니어·테스트 자동화 엔지니어로 15년 이상의 실무 경험을 가진 Yoshi가 작성했습니다. Selenium에서 Playwright로의 마이그레이션 프로젝트도 경험했으며, Flaky Test의 진단과 대응을 실무에서 지속적으로 수행하고 있습니다.
📌 결론
- Playwright의 Flaky Test는 「Auto-waiting에 대한 오해」 「불안정한 Locator」 「CI 환경 특유의 차이」가 주요 원인이 되기 쉽다
- 리트라이(pytest-rerunfailures)는 응급처치이며, 근본 원인의 특정·수정이 우선
- Trace Viewer를 활용하면 CI에서만 재현되는 실패도 시각적으로 원인을 조사할 수 있다
Playwright는 Selenium에 비해 Flaky Test가 적게 발생하도록 설계되어 있지만, 다음과 같은 증상은 드물지 않습니다.
- 로컬에서는 매번 통과하는데 CI 환경에서만 가끔 실패한다
- 같은 테스트를 재실행하면 통과되어 원인을 특정할 수 없다
- 애니메이션이 있는 화면에서 클릭이 실패한다
- 병렬 실행을 활성화하면 실패율이 올라간다
이 글에서는 이러한 증상을 일으키는 원인을 하나씩 진단하고, 각각의 해결책을 순서대로 살펴봅니다.
Flaky Test란? 왜 Playwright에서도 발생하는가
Flaky Test란 코드나 테스트 대상의 사양에 변경이 없음에도 실행할 때마다 성공하거나 실패하는 테스트를 말합니다.
Playwright는 Selenium과 비교해 Auto-waiting(요소가 조작 가능한 상태가 될 때까지 자동으로 대기하는 기능)이 내장되어 있어, 명시적인 대기 처리를 빠뜨려서 발생하는 Flaky Test가 크게 줄어듭니다. 다만 이것이 「대기 처리를 생각하지 않아도 된다」는 의미는 아닙니다. Auto-waiting이 커버하는 범위와 커버하지 않는 범위를 이해하지 못하면 여전히 Flaky Test는 발생합니다.
WebDriverWait)를 작성해야 했습니다. Playwright는 이런 종류의 대기를 자동화하고 있지만, 네트워크 응답 대기나 애니메이션 완료 대기처럼 Auto-waiting만으로는 해결되지 않는 요인은 여전히 존재합니다.Playwright 특유의 Flaky Test 원인 4가지 패턴
실무에서 자주 마주치는 원인을 4가지로 분류했습니다. 먼저 어느 패턴에 해당하는지 진단하는 것이 해결의 첫걸음입니다.
① Auto-waiting에 대한 오해
증상:요소는 존재하지만 클릭할 수 없거나 값이 반영되지 않는다
대책:Actionability checks를 이해한다
② 불안정한 Locator
증상:CSS 클래스 변경이나 DOM 구조 변화로 요소를 찾을 수 없게 된다
대책:role·test-id 기반 Locator로 변경
③ 네트워크·API 대기 부족
증상:API 응답이 느릴 때만 실패한다(특히 CI 환경)
대책:expect()의 자동 재시도 어서션 활용
④ CI 환경 특유의 요인
증상:로컬에서는 재현되지 않고 CI에서만 실패한다
대책:병렬 실행 설계·리소스 제약 재검토
① Auto-waiting을 정확히 이해하기
Playwright는 요소를 조작하기 전에 다음과 같은 Actionability checks(조작 가능성 체크)를 자동으로 수행합니다.
| 체크 항목 | 내용 |
|---|---|
| Attached | 요소가 DOM에 존재하는지 |
| Visible | 요소가 사용자에게 보이는 상태인지(display:none·visibility:hidden·크기 0 등 가시성과 관련된 요인을 포함해 판정) |
| Stable | 애니메이션 중이 아니고 위치가 안정되어 있는지 |
| Receives Events | 다른 요소에 가려지지 않아 클릭을 받을 수 있는지 |
| Enabled | disabled 속성이 붙어 있지 않은지 |
| Editable | (fill() 사용 시)입력 가능한 상태인지 |
즉 Playwright가 커버하는 것은 「요소가 조작 가능한 상태가 될 때까지」의 대기이며, 다음과 같은 경우는 Auto-waiting의 범위 밖입니다.
- 버튼을 클릭한 후 비동기로 API가 호출되어 데이터가 반영될 때까지의 대기
- 요소는 표시되어 있지만 내부 값이 아직 갱신되지 않은 상태의 판정
- 페이지 전환 후 목표 요소가 로드될 때까지의 대기(보통은 자동으로 커버되지만, 복잡한 SPA 전환에서는 주의가 필요)
❌ 흔한 실수:sleep으로 억지로 기다리기
import time
from playwright.sync_api import Page
def test_add_to_cart_bad(page: Page) -> None:
page.goto("/products/1")
page.get_by_role("button", name="장바구니에 담기").click()
# ❌ 고정 시간의 sleep은 CI 환경의 지연에 취약하며 Flaky Test의 원인이 되기 쉽다
time.sleep(2)
assert page.get_by_text("장바구니에 담겼습니다").is_visible()✅ 개선 예:expect()의 자동 재시도 어서션 사용
from playwright.sync_api import Page, expect
def test_add_to_cart_good(page: Page) -> None:
page.goto("/products/1")
page.get_by_role("button", name="장바구니에 담기").click()
# ✅ 조건을 만족할 때까지 자동으로 재시도하며 확인한다
expect(page.get_by_text("장바구니에 담겼습니다")).to_be_visible()time.sleep()이 Playwright 코드에 그대로 남아 있는 경우를 자주 봅니다. Playwright는 고정 sleep이 거의 필요 없는 설계이므로, 리뷰에서 발견하면 expect()로 교체를 검토할 가치가 있습니다.② 안정적인 Locator 선택하기
CSS 셀렉터나 XPath는 디자인 변경이나 클래스명 리팩터링만으로도 쉽게 깨집니다. Playwright는 사용자 관점의 Locator(role·label·text)를 권장하며, 이들은 구현 세부사항 변경에 강한 경향이 있습니다.
| Locator | 안정성 | 사용처 |
|---|---|---|
| get_by_role() | ◎ 높음 | 버튼·링크 등 접근성 role을 가진 요소(1순위 후보) |
| get_by_label() | ◎ 높음 | 폼 입력란 |
| get_by_test_id() | ◎ 높음 | role·label로 표현하기 어려운 요소(개발팀과 data-testid 운용 합의가 전제) |
| get_by_placeholder() | △ 중간 | 플레이스홀더가 있는 입력란(label이 없을 때의 대안. 문구 변경에 취약한 점 주의) |
| get_by_text() | △ 중간 | 정적인 텍스트(다국어 대응 시 변경에 취약한 점 주의) |
| CSS / XPath | ✕ 낮음 | 다른 방법으로 표현할 수 없을 때의 최후 수단 |
❌ 불안정한 예:CSS 클래스에 의존
# ❌ CSS 클래스명은 스타일 변경 때마다 깨질 위험이 있다
page.locator(".btn.btn-primary.submit-btn-v2").click()✅ 안정적인 예:role 기반
# ✅ 사용자가 보는 역할·이름을 기준으로 하므로 내부 구현 변경에 강하다
page.get_by_role("button", name="제출").click()get_by_test_id()가 현실적인 선택지가 됩니다. 다만 data-testid의 부여·유지는 개발팀과의 합의가 전제되므로, 도입 시 규칙을 정해두면 원활합니다.③ 타임아웃 설정 재검토하기
Playwright에는 여러 계층의 타임아웃이 존재합니다. 각각의 역할을 이해해두면 원인 조사가 수월해집니다.
| 종류 | 대상 | 기본값(기준) |
|---|---|---|
| 액션 타임아웃 | click()·fill() 등 개별 조작 | 30초 |
| expect() 타임아웃 | 어서션의 자동 재시도 | 액션 타임아웃과 별도로 설정되는 경우가 있음 |
| 테스트 전체 타임아웃 | 테스트 케이스 하나의 전체 실행 시간 | 프로젝트 설정에 따라 다름 |
타임아웃을 개별로 지정하는 예
from playwright.sync_api import Page, expect
def test_slow_api_response(page: Page) -> None:
page.goto("/reports")
page.get_by_role("button", name="리포트 생성").click()
# API 응답이 느린 것을 알고 있다면 해당 어서션만 개별로 타임아웃을 연장한다
# ms(밀리초) 단위 지정임에 주의
expect(page.get_by_text("생성 완료")).to_be_visible(timeout=60_000)④ pytest-rerunfailures로 재시도하기(응급처치로서)
근본 원인의 특정·수정이 이상적이지만, 릴리스 직전 등 즉시 대응하기 어려운 경우 리트라이로 일시적으로 완화하는 선택지도 있습니다. 다만 이는 pytest 표준 기능이 아니라 pytest-rerunfailures라는 플러그인에 의존한다는 점에 주의하세요.
설치
pip install pytest-rerunfailures사용법
# CLI 옵션으로 전체에 적용하는 경우
# 실패한 테스트를 최대 3회까지 재실행하며, 재실행 사이에 2초 대기한다
pytest --reruns 3 --reruns-delay 2특정 테스트에만 적용하고 싶다면 @pytest.mark.flaky를 데코레이터로 함수에 붙입니다.
import pytest
from playwright.sync_api import Page
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_login(page: Page) -> None:
page.goto("/login")
# ... 테스트 처리 ...- 리트라이는 증상을 감출 뿐, 근본 원인은 해결하지 않습니다. 남용하면 테스트 스위트 전체의 신뢰성이 떨어집니다
- 「어느 테스트가 몇 번 재시도되었는지」를 기록·모니터링하는 체계가 없으면 문제가 쌓이는 것을 알아차리지 못합니다
- 정말로 불안정한 원인(Auto-waiting·Locator 설계 등)은 리트라이로 감추지 말고 우선적으로 수정하는 것을 권장합니다
⑤ Trace Viewer로 원인 진단하기
「CI에서만 실패한다」 「가끔 실패한다」와 같이 재현성이 낮은 Flaky Test의 원인 조사에는 Trace Viewer가 특히 효과적입니다. 테스트 실행 시의 조작·스크린샷·네트워크 로그·콘솔 로그를 하나의 파일로 기록할 수 있습니다.
※ 아래는 pytest-playwright 플러그인을 사용하는 경우의 예입니다.
실패 시에만 Trace 저장하기
# 실패 시에만 Trace를 저장한다(pytest-playwright 플러그인 사용 시)
pytest --tracing retain-on-failure
# 저장된 trace.zip을 브라우저에서 확인한다
playwright show-trace trace.zippip install pytest-playwright)을 사용하는 경우의 예입니다. 표준 pytest 커맨드에는 --tracing 옵션이 없습니다. 실제 동작은 사용 중인 플러그인 버전에 따라 다르므로 최신 정보는 Playwright 공식 문서도 함께 확인해 주세요.Trace Viewer에서 확인해야 할 포인트
| 확인 위치 | 알 수 있는 것 |
|---|---|
| 각 단계의 스크린샷 | 실패 직전의 화면 상태·요소가 예상대로 표시되었는지 |
| Actionability 로그 | 어느 체크(Visible·Stable 등)에서 대기가 발생했는지 |
| 네트워크 타임라인 | API 응답이 지연되지 않았는지·예상치 못한 요청이 발생하지 않았는지 |
| 콘솔 로그 | JS 에러가 조작을 방해하지 않았는지 |
CI/CD 특유의 원인과 대책
「로컬에서는 재현되지 않는데 CI에서만 실패한다」면 CI 환경 특유의 요인을 의심해봐야 합니다. GitHub Actions 설정 자체가 원인이 아닌지도 함께 확인하면 효율적입니다(자세한 내용은 GitHub Actions로 Playwright를 CI/CD에 통합하는 가이드 참조).
| 요인 | 설명 | 대책 |
|---|---|---|
| 리소스 제약 | CI 러너의 CPU·메모리가 로컬보다 적은 경우가 많아 처리가 지연되기 쉽다 | 무거운 처리의 타임아웃을 개별로 연장·병렬 수 조정 |
| 병렬 실행에 의한 데이터 경합 | 여러 테스트가 같은 DB 데이터·계정을 공유하면 간섭이 발생한다 | 각 테스트가 독립된 테스트 데이터를 사용하도록 설계 |
| Headless 특유의 렌더링 차이 | 폰트·GPU 렌더링 차이로 레이아웃이 미세하게 달라질 수 있다 | 레이아웃에 의존하지 않는 Locator·어서션 사용 |
| 네트워크 지연 | CI 환경에서 외부 API 접근이 로컬보다 느릴 수 있다 | expect()의 자동 재시도를 활용하고 고정 대기에 의존하지 않는다 |
실무에서 자주 발생하는 실수
① 「동작하지 않으니까」라며 리트라이 설정으로 눈속임하기
실패할 때마다 reruns 횟수를 늘려 대응하면 근본 원인이 쌓여 결국 리트라이로도 통과하지 못하는 수준까지 악화됩니다. 리트라이 횟수를 늘리기 전에 실패 빈도가 높은 테스트부터 원인 조사를 우선하세요.
② Auto-waiting과 expect()의 자동 재시도를 혼동하기
「Playwright는 자동으로 기다려준다」는 이해만으로 일반적인 assert(Python 표준 어서션)로 상태를 확인해버리는 경우입니다. assert는 한 번만 평가되며 자동으로 재시도되지 않습니다. 상태가 변화하는 것을 기다리며 확인하고 싶다면 반드시 expect()를 사용하세요.
| assert(표준) | expect()(Playwright) | |
|---|---|---|
| 평가 타이밍 | 실행된 순간 1회만 | 타임아웃까지 조건을 만족할 때까지 반복 평가 |
| 비동기 상태 변화 | 변화 전에 평가해 Fail하기 쉽다 | 변화를 기다린 후 판정하므로 안정적이다 |
| 적합한 용도 | 변화하지 않는 값 확인(계산 결과 등) | 화면 표시·API 반영 등 변화를 동반하는 확인 |
③ 병렬 실행을 활성화하자마자 테스트가 불안정해지기
테스트가 같은 사용자 계정·같은 DB 레코드를 공유하고 있다면 병렬 실행 시 데이터 경합이 발생합니다. 병렬화하기 전에 각 테스트가 독립된 데이터로 동작하도록 설계되어 있는지 확인하세요.
④ Flaky Test를 방치하고 Skip 처리하기
「이 테스트는 자주 실패하니까」라며 @pytest.mark.skip을 붙여 비활성화해버리면, 그 테스트가 원래 검출해야 할 버그를 계속 놓치게 됩니다. Skip 처리하기 전에 Trace Viewer로 한 번 원인을 조사하는 습관을 들이는 것을 권장합니다.
📖 관련 글
FAQ
Q. Playwright를 사용하면 Flaky Test가 완전히 없어지나요?
완전히 없어지지는 않습니다. Auto-waiting 덕분에 「대기 처리를 빠뜨려서」 발생하는 Flaky Test는 크게 줄어들지만, CI 환경 특유의 리소스 제약·병렬 실행의 데이터 경합·애플리케이션 측의 비동기 처리 대기 등 Auto-waiting 범위 밖의 원인은 여전히 발생합니다.
Q. 리트라이(pytest-rerunfailures)는 사용하지 않는 편이 좋을까요?
완전히 금지할 필요는 없지만, 근본 원인 수정을 미루는 이유로 삼지 않는 것이 중요합니다. 응급처치로 사용할 경우에도 어느 테스트가 몇 번 리트라이되고 있는지 기록·모니터링하고, 리트라이가 자주 발생하는 테스트는 우선적으로 조사·수정하는 운용을 권장합니다.
Q. expect()와 Python 표준 assert는 무엇이 다른가요?
assert는 그 시점의 상태를 한 번만 평가합니다. 상태가 아직 변화 중이라면 올바른 결과가 나오기 전에 평가해버려 실패할 수 있습니다. expect()는 지정한 타임아웃 동안 조건을 만족할 때까지 자동으로 재평가(재시도)해주므로, 비동기로 변화하는 상태 확인에 적합합니다.
Q. 로컬에서는 재현되지 않는 CI 한정 Flaky Test는 어떻게 조사해야 하나요?
먼저 Trace Viewer를 CI 환경에서 실패 시 저장하도록 설정(--tracing retain-on-failure)하고, artifacts로 업로드하는 체계를 만드는 것을 권장합니다. 로컬에서 재현되지 않는 원인은 리소스 제약이나 타이밍 차이인 경우가 많으므로, 실제로 CI에서 실패했을 때의 Trace를 확인하는 것이 가장 확실합니다.
Q. 애니메이션이 원인인 Flaky Test는 어떻게 대처해야 하나요?
Playwright의 Actionability checks에는 「Stable」(위치가 안정되어 있는지)판정이 포함되어 있어, 대부분의 애니메이션은 자동으로 대기됩니다. 그래도 문제가 발생한다면 테스트 환경에서 애니메이션을 비활성화하는 설정(CSS나 애플리케이션 측 설정)을 검토하는 방법도 있습니다. 다만 애니메이션 자체의 동작 확인이 목적인 테스트에서는 적용하지 않는 판단도 필요합니다.
Q. 병렬 실행과 Flaky Test의 관계를 알려주세요
병렬 실행 자체가 Flaky Test를 일으키는 것이 아니라, 테스트 간에 데이터나 리소스를 공유하는 설계가 원인인 경우가 대부분입니다. 각 테스트가 독립된 사용자·데이터를 사용하도록 설계되어 있다면, 병렬 실행은 오히려 실행 시간 단축이라는 이점을 안전하게 누릴 수 있습니다.
📋 정리
- Auto-waiting은 요소가 조작 가능한 상태가 될 때까지의 대기를 자동화하지만, 비동기 API 응답 대기 등은 범위 밖이다
- role·test-id 기반 Locator를 사용하면 CSS 클래스 변경 등 구현 세부사항 변경에 강해진다
- Trace Viewer를 활용하면 CI에서만 재현되는 Flaky Test도 시각적으로 원인을 조사할 수 있다
- 리트라이는 응급처치이며, 근본 원인의 특정·수정을 우선하는 것이 장기적인 안정성으로 이어진다
🚀 먼저 시작할 3단계
- 실패 시 Trace를 저장하는 설정(
--tracing retain-on-failure)을 활성화한다 - 자주 실패하는 테스트부터 Trace를 확인하고, 원인을 4가지 패턴에 대입해 진단한다
- 해당하는 대책(Locator 재검토·expect()로 교체·타임아웃 조정)을 하나씩 적용한다
Flaky Test를 「어쩌다 그런 것」으로 끝내지 말고, 원인을 하나씩 진단해 해결해 나가면 테스트 스위트 전체의 신뢰성이 차곡차곡 쌓여갑니다. 먼저 Trace Viewer를 활성화해서 실패 시의 기록을 남기는 체계부터 시작해보세요.
