반응형
E2E 검증에서 파일 업로드 후 "다음" 버튼 선택하는 코드가 있었다
30초 타임아웃 걸리면서 터졌다
원인 찾아보니 버튼이 disabled 상태인데 클릭을 시도한 거였다
상황: 뭐가 터졌나
파일 업로드 모달에서 SDF 파일을 선택하고 "다음" 버튼을 선택하는 Step 이었다
Error: playwright._impl_.errors.TimeoutError: Locator.click: Timeout 30000ms exceeded.
- waiting for get_by_role("button", name="다음").last
- locator resolved to <button type="submit" class="disabled:cursor-not-allowed ...">
- 56 × waiting for element to be visible, enabled and stable
- retrying click action
56번 재시도하다가 30초 만에 포기한 거다
원인: wait_for_timeout 으로 기도하기
기존 코드를 보자
# 파일 선택
file_input.first.set_input_files(file_path)
# 1초 기다리고
self.wait_for_timeout(1000) # 1초면 되겠지
# 바로 클릭
next_btn.last.click() # 아직 disabled 인데
파일을 업로드하면 서버에서 유효성 검사를 한다
검사가 끝나야 "다음" 버튼이 활성화되는데
1초 고정 대기로는 서버 응답이 느릴 때 터진다
빠른 서버: 0.3초면 검증 끝남 → 0.7초 낭비
느린 서버: 3초 걸림 → 1초 대기로는 부족 → 터짐
어떤 값을 넣어도 최적이 아니다
해결: expect 로 조건 기반 대기
from playwright.sync_api import expect
# 버튼이 활성화될 때까지 대기 (최대 15초)
expect(next_btn).to_be_enabled(timeout=15000)
# 활성화 확인되면 클릭
next_btn.click()
expect().to_be_enabled() 는 폴링 방식으로 동작한다
0ms → disabled? → 대기
100ms → disabled? → 대기
200ms → disabled? → 대기
300ms → enabled! → 통과 (0.3초 만에 끝남)
서버가 빠르면 빨리 끝나고, 느리면 15초까지 기다려준다
고정 시간이 아니라 조건이 충족되면 바로 넘어간다
expect vs wait_for 뭐가 다른데?
Playwright 에는 비슷해 보이는 대기 방법이 3가지 있다
1. wait_for_timeout() — 무조건 기다리기
page.wait_for_timeout(3000) # 3초 무조건 대기
- 조건 없음, 그냥 시간만 기다림
- 서버 빠르면 시간 낭비, 느리면 부족
- 쓰지 마라 (Playwright 공식 문서에서도 비추)
2. locator.wait_for() — 요소 존재/소멸 대기
btn.wait_for(state="visible") # 보일 때까지
btn.wait_for(state="hidden") # 사라질 때까지
btn.wait_for(state="attached") # DOM 에 붙을 때까지
btn.wait_for(state="detached") # DOM 에서 빠질 때까지
- 지원하는 상태가 4가지뿐
enabled/disabled구분 못 함- 로딩 스피너 사라질 때까지 기다리기 같은 용도
3. expect() — 상태 검증 + 자동 대기
expect(btn).to_be_enabled() # 활성화 될 때까지
expect(btn).to_be_visible() # 보일 때까지
expect(btn).to_be_hidden() # 숨겨질 때까지
expect(btn).to_have_text("완료") # 텍스트 바뀔 때까지
expect(btn).to_have_attribute("aria-selected", "true") # 속성 바뀔 때까지
expect(btn).to_be_checked() # 체크될 때까지
expect(page).to_have_url("*/dashboard") # URL 바뀔 때까지
- 상태 조건이 다양함
- 조건 충족까지 자동 폴링 (재시도)
- 실패 시
AssertionError+ 상세 메시지
비교 표
wait_for_timeout |
wait_for() |
expect() |
|
|---|---|---|---|
| 조건 기반 | X (고정 시간) | O (4가지) | O (10가지+) |
| enabled 대기 | X | X | O |
| 텍스트 변경 대기 | X | X | O |
| 속성 변경 대기 | X | X | O |
| 자동 폴링 | X | O | O |
| 추천 용도 | 디버깅 임시용 | 요소 출현/소멸 | 상태 변화 전반 |
실전 패턴
패턴 1: disabled 버튼 활성화 대기 (이번 케이스)
# Bad
self.wait_for_timeout(1000)
btn.click()
# Good
expect(btn).to_be_enabled(timeout=15000)
btn.click()
패턴 2: 로딩 끝난 후 텍스트 확인
# Bad
page.wait_for_timeout(3000)
text = btn.text_content()
assert text == "완료"
# Good
expect(btn).to_have_text("완료", timeout=10000)
패턴 3: 페이지 이동 대기
# Bad
page.wait_for_timeout(2000)
assert "dashboard" in page.url
# Good
expect(page).to_have_url("**/dashboard", timeout=10000)
패턴 4: 모달 닫힘 대기
# Good — wait_for 가 적합한 케이스 (요소 소멸)
modal.wait_for(state="hidden", timeout=5000)
이건 wait_for() 가 맞다
요소가 DOM 에서 사라지는 걸 기다리는 거니까
언제 뭘 쓰나
"버튼이 활성화될 때까지" → expect().to_be_enabled()
"텍스트가 바뀔 때까지" → expect().to_have_text()
"체크박스가 체크될 때까지" → expect().to_be_checked()
"요소가 사라질 때까지" → wait_for(state="hidden")
"요소가 나타날 때까지" → wait_for(state="visible") 또는 expect().to_be_visible()
"그냥 N 초 기다리기" → wait_for_timeout() ← 가능하면 쓰지 마라
상태 변화가 목적이면 expect(), 존재 여부가 목적이면 wait_for()
wait_for_timeout() 은 디버깅할 때 임시로만 쓰고 커밋하지 마라
결론
wait_for_timeout(1000) 은 "1초면 되겠지" 라는 기도다
서버가 느리면 터지고, 빠르면 시간 낭비다
Playwright 에 조건 기반 대기가 있으니까 그걸 쓰자
- 상태 변화 →
expect() - 요소 출현/소멸 →
wait_for() - 고정 시간 대기 → 쓰지 마라
특히 expect().to_be_enabled() 는 wait_for() 로 못 하는 거다
disabled → enabled 전환을 기다릴 수 있는 건 expect() 뿐이다
반응형
'TIL > Playwright' 카테고리의 다른 글
| [TIL][Playwright] AI 챗봇 응답 감지 #2 — follow-up 함정과 nth 인덱스 (0) | 2026.06.21 |
|---|---|
| [TIL][Playwright] AI 챗봇 응답 완료를 E2E 로 감지하는 방법 (0) | 2026.06.14 |
| [TIL][Claude] GitHub Actions 에서 Playwright E2E 매일 자동 실행하기 (0) | 2026.05.17 |
| [TIL][Playwright] SVG 요소는 선택으로 검증하면 안 된다 — DOM 구조 검증 (0) | 2026.03.22 |
| 로그인 인증 상태 저장 및 재사용 방법 w/ Playwright (6) | 2025.08.12 |