TIL/Playwright

[TIL][Playwright] expect vs wait_for — disabled 버튼 때문에 30초 날린 이야기

아람2 2026. 6. 7. 20:00
반응형

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() 뿐이다

반응형