Selenium WebDriverWait 완벽 가이드 | 올바른 대기 처리로 Flaky Test 해결하기

Selenium에서 “요소를 찾을 수 없다”, “클릭할 수 없다”는 에러가 빈발하는 원인의 많은 부분은 대기 처리 문제입니다. time.sleep()으로 임시방편을 쓰면 Flaky Test가 되기 쉽습니다. WebDriverWaitexpected_conditions를 올바르게 사용하면 안정적인 테스트를 작성할 수 있습니다.

📌 이 글의 대상 독자

time.sleep()을 사용하고 있지만 올바른 방법을 알고 싶은 분
NoSuchElementException이나 ElementNotInteractableException으로 고민하는 분
WebDriverWait는 알지만 expected_conditions 사용법을 정리하고 싶은 분
✅ 환경에 따라 테스트가 성공하거나 실패하는 Flaky Test를 해소하고 싶은 분

📖 이 글을 읽으면 할 수 있는 것

✔ 암묵적 대기·명시적 대기·Fluent Wait적 설정의 차이와 사용 구분을 알 수 있다
expected_conditions의 주요 메서드를 상황별로 사용 구분할 수 있다
time.sleep()을 없애고 안정적인 테스트를 작성할 수 있게 된다
✔ 커스텀 대기 조건을 직접 만들 수 있게 된다

필자는 여러 프로젝트에서 Selenium을 사용한 브라우저 자동화 테스트를 실천해 왔습니다.
대기 처리 문제는 Flaky Test의 원인으로 가장 자주 마주치는 케이스 중 하나입니다.
이 글은 그 경험을 토대로 실무에서 사용할 수 있는 대기 처리 패턴을 체계적으로 정리한 것입니다.

✅ 이 글의 결론

  • time.sleep()은 금지. 대신 WebDriverWait + expected_conditions를 사용
  • 암묵적 대기와 명시적 대기를 혼재시키지 않는다
  • 대기 조건은 “무엇을 기다리는가”에 맞게 expected_conditions를 선택

왜 Selenium에서 대기 처리가 필요한가

현대의 웹 애플리케이션은 비동기 처리(Ajax·JavaScript)로 동작합니다. 페이지를 열자마자 요소가 아직 존재하지 않는 경우가 대부분입니다.

자주 있는 상황발생하는 에러
페이지 로딩 중에 요소를 취득하려 했다NoSuchElementException
요소는 존재하지만 비표시·비활성화 상태다ElementNotInteractableException
다른 요소가 애니메이션 중에 위에 겹쳐 있다ElementClickInterceptedException
요소가 오래된 DOM에서 분리됐다StaleElementReferenceException

이러한 에러를 피하기 위해 “요소가 준비될 때까지 기다리는” 처리가 필요합니다.

time.sleep()이 나쁜 이유

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/login")  # 샘플 URL
time.sleep(3)  # ← 이것이 문제
driver.find_element(By.ID, "email").send_keys("user@example.com")
문제점구체적인 영향
불필요한 대기 시간이 발생한다요소가 0.5초에 표시되어도 3초 기다림 → CI가 느려진다
환경에 따라서는 부족해진다CI 서버가 느린 날에는 3초도 부족해서 테스트가 실패한다
Flaky Test의 온상이 된다네트워크 속도에 따라 성공·실패가 달라진다
근본 해결이 되지 않는다sleep을 길게 할수록 느려질 뿐, 문제의 본질은 변하지 않는다

Selenium의 3가지 대기 방법

종류설정 방법권장도용도
명시적 대기WebDriverWait🔴 권장특정 조건이 충족될 때까지 기다린다
암묵적 대기implicitly_wait()⚠️ 한정적요소의 DOM 출현을 글로벌하게 기다린다
Fluent Wait적 설정WebDriverWait(상세 설정)🟡 고급자용폴링 간격·무시할 예외를 세밀하게 설정

명시적 대기 (WebDriverWait) 사용법

명시적 대기는 특정 조건이 충족될 때까지 최대 ○초 기다리는 방식입니다. 조건이 충족되면 즉시 처리를 계속하므로 불필요한 대기가 발생하지 않습니다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com/login")  # 샘플 URL

# WebDriverWait 기본형
# WebDriverWait(driver, 타임아웃 초).until(조건)
wait = WebDriverWait(driver, 10)  # 최대 10초 기다린다

# 요소가 표시될 때까지 기다린다 (입력 필드 등)
email_input = wait.until(
    EC.visibility_of_element_located((By.ID, "email"))
)
email_input.send_keys("user@example.com")

# 요소가 표시되고 클릭 가능해질 때까지 기다린다
submit_btn = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-button']"))
)
submit_btn.click()

expected_conditions 주요 메서드 목록

메서드대기 조건사용 장면
presence_of_element_located요소가 DOM에 존재한다 (비표시여도 OK)텍스트나 값을 취득하고 싶을 때
visibility_of_element_located요소가 표시되어 있다 (크기 > 0)입력 필드 조작·표시 확인·스크린샷
element_to_be_clickable요소가 표시되어 있고 유효 (클릭 가능)버튼을 클릭할 때 (가장 자주 사용)
invisibility_of_element_located요소가 비표시가 됐다로딩 스피너가 사라지는 것을 기다릴 때
text_to_be_present_in_element요소의 텍스트에 지정 문자열이 포함된다“저장됐습니다” 등 완료 메시지 대기
url_containsURL에 지정 문자열이 포함된다페이지 전환 완료를 기다릴 때
url_to_beURL이 지정값과 완전 일치한다로그인 후 전환 대상 확인
presence_of_all_elements_located지정 로케이터에 일치하는 요소가 1개 이상 있다리스트·테이블 로딩 대기
staleness_of요소가 DOM에서 분리됐다 (stale이 됐다)페이지 재로딩 후 요소 갱신 대기
alert_is_present얼럿 다이얼로그가 표시됐다confirm·alert 처리 전

상황별 코드 예시

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, 10)

# ① 버튼을 클릭한다 (가장 자주 사용)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='submit-btn']")))
btn.click()

# ② 로딩 스피너가 사라지는 것을 기다린다
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))

# ③ 완료 메시지가 표시되는 것을 기다린다
wait.until(EC.text_to_be_present_in_element(
    (By.CSS_SELECTOR, "[data-testid='status-message']"),
    "저장됐습니다"
))

# ④ 페이지 전환을 기다린다
wait.until(EC.url_contains("/dashboard"))

# ⑤ 리스트가 로드되는 것을 기다린다 (1건 이상)
items = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".item-list li"))
)
assert len(items) > 0

# ⑥ 얼럿을 처리한다
wait.until(EC.alert_is_present())
alert = driver.switch_to.alert
alert.accept()

암묵적 대기 (implicitly_wait)와 주의점

암묵적 대기는 모든 find_element에 글로벌 타임아웃을 설정합니다. 한 번 설정하면 모든 조작에 자동으로 적용됩니다.

driver = webdriver.Chrome()
driver.implicitly_wait(10)  # 모든 find_element에서 최대 10초 기다린다
driver.get("https://example.com/login")  # 샘플 URL

# 이후의 find_element는 최대 10초 기다린다
email_input = driver.find_element(By.ID, "email")
⚠️ 암묵적 대기와 명시적 대기를 혼재시키지 않는다
두 가지를 동시에 사용하면 예기치 않은 동작이 발생할 수 있습니다(대기 시간이 합산되거나 경합하는 등).
대규모 프로젝트에서는 명시적 대기(WebDriverWait)로 통일하는 경우가 많습니다. 어느 한 쪽으로 통일합시다.

WebDriverWait 상세 설정 (이른바 Fluent Wait적 사용법)

폴링 간격과 무시할 예외를 세밀하게 지정할 수 있는 WebDriverWait의 상세 설정 버전입니다.
Selenium Python에는 Java와 같은 독립된 FluentWait 클래스는 없으며, WebDriverWaittimeout·poll_frequency·ignored_exceptions를 조정함으로써 이른바 Fluent Wait적인 동작을 실현합니다.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException

# WebDriverWait 상세 설정
wait = WebDriverWait(
    driver,
    timeout=15,             # 최대 대기 시간 (초)
    poll_frequency=0.5,     # 체크 간격 (초). 기본값은 0.5초
    ignored_exceptions=[    # 무시할 예외 (이 예외가 발생해도 계속 기다린다)
        NoSuchElementException,
        StaleElementReferenceException,
    ]
)

element = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='dynamic-btn']"))
)
element.click()
💡 이 상세 설정은 어떤 때 사용할까?
일반적인 WebDriverWait로 충분한 경우가 대부분입니다. 상세 설정이 도움이 되는 것은 다음과 같은 상황입니다.

  • 폴링 간격을 세밀하게 조정해 응답성을 높이고 싶을 때
  • StaleElementReferenceException이 빈발하는 동적 페이지에서 기다리고 싶을 때
  • 대기 중에 특정 예외를 무시하고 싶을 때

커스텀 대기 조건 만들기

expected_conditions에 없는 조건을 기다리고 싶을 때는 커스텀 조건을 함수 또는 클래스로 정의할 수 있습니다.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

# ① lambda를 사용한 간단한 방법
wait = WebDriverWait(driver, 10)

# 요소의 속성값이 바뀌는 것을 기다린다
wait.until(lambda d: d.find_element(By.ID, "status").get_attribute("data-state") == "loaded")

# ② 클래스를 사용한 재사용 가능한 방법
class element_has_class:
    """지정한 요소가 특정 CSS 클래스를 가질 때까지 기다린다"""
    def __init__(self, locator: tuple, css_class: str) -> None:
        self.locator = locator
        self.css_class = css_class

    def __call__(self, driver):
        element = driver.find_element(*self.locator)
        return self.css_class in element.get_attribute("class")


# 사용 예: 버튼이 "active" 클래스를 가질 때까지 기다린다
wait.until(element_has_class(
    (By.CSS_SELECTOR, "[data-testid='submit-btn']"),
    "active"
))

pytest fixture와의 조합

실무에서는 WebDriverWait를 pytest fixture에 내장함으로써 테스트 코드를 간결하게 유지할 수 있습니다.

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait


@pytest.fixture
def driver():
    d = webdriver.Chrome()
    d.implicitly_wait(0)   # 암묵적 대기는 비활성화해 둔다
    yield d
    d.quit()


@pytest.fixture
def wait(driver):
    return WebDriverWait(driver, 10)
# test_login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC


def test_login_success(driver, wait) -> None:
    driver.get("https://example.com/login")  # 샘플 URL

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("user@example.com")
    wait.until(EC.visibility_of_element_located((By.ID, "password"))).send_keys("password123")
    wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-btn']"))).click()

    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url

대기 처리에서 막히기 쉬운 포인트 7가지

⚠️ Selenium 대기 처리에서 자주 있는 함정

  1. presence_of_element_located로 click하려다 에러가 난다
    presence_of_element_located는 DOM에 존재하는 것만 확인하며, 표시·유효 상태는 확인하지 않습니다. 클릭하려면 element_to_be_clickable을 사용합시다.
  2. 암묵적 대기와 명시적 대기를 혼재시키고 있다
    두 가지를 설정하면 대기 시간이 예기치 않은 형태로 합산·경합합니다. 어느 한 쪽으로 통일하고, 권장은 명시적 대기(WebDriverWait)만 사용하는 것입니다.
  3. 타임아웃 후 TimeoutException이 발생해서 알아채지 못한다
    WebDriverWait는 조건이 충족되지 않는 경우 TimeoutException을 발생시킵니다. 테스트가 실패했을 때 에러 메시지를 읽고 “무엇을 기다리고 있었는가”를 확인합시다. 스크린샷을 찍어 상태를 기록하는 것도 효과적입니다.

    from selenium.common.exceptions import TimeoutException
    try:
        wait.until(EC.element_to_be_clickable((By.ID, "submit")))
    except TimeoutException:
        driver.save_screenshot("timeout_debug.png")
        raise
  4. 로딩 중 요소를 취득해서 StaleElementReferenceException이 난다
    페이지 갱신이나 Ajax 후 재취득이 필요한 요소는 취득 후 바로 오래된 것이 됩니다. staleness_of로 오래된 요소의 소멸을 기다린 후 재취득하거나, 매번 find_element를 다시 호출하는 설계로 합시다.
  5. 애니메이션 중 요소를 클릭해서 ElementClickInterceptedException이 난다
    모달의 표시 애니메이션이나 툴팁이 요소 위에 겹쳐 있으면 클릭이 실패합니다. 먼저 로케이터·대기 조건·오버레이 요소의 유무를 확인하는 것이 근본 해결의 지름길입니다. ActionChains나 JavaScript 클릭(driver.execute_script("arguments[0].click();", element))으로 회피할 수 있는 경우도 있지만, 이것들은 문제를 숨길 수 있으므로 최후의 수단으로 검토합시다.
  6. 타임아웃 값을 모두 같게 하고 있다
    페이지 전환(5~10초), API 응답(3~5초), 애니메이션(1~2초) 등 대기 내용에 따라 적절한 타임아웃 값은 다릅니다. 일률적으로 10초 등으로 하면 느린 테스트를 양산하게 됩니다.
  7. driver.get() 후 아무것도 기다리지 않고 요소를 취득하고 있다
    driver.get()은 HTML 로딩 완료(document.readyState == "complete")까지 기다리지만, JavaScript에 의한 동적 콘텐츠 로딩은 기다리지 않습니다. 페이지 전환 후 요소를 조작할 때도 WebDriverWait를 사용합시다.

자주 묻는 질문 (FAQ)

Q. WebDriverWait의 기본 폴링 간격은?

기본 폴링 간격은 0.5초입니다. 즉 조건이 충족됐는지를 0.5초마다 체크합니다. 이 간격은 poll_frequency 파라미터로 변경할 수 있습니다(예: WebDriverWait(driver, 10, poll_frequency=0.2)). 체크 빈도를 높이면 응답성이 향상되지만 브라우저에 대한 부하도 증가합니다.

Q. WebDriverWait를 복수 테스트에서 공통화하는 방법은?

pytest fixture에 내장하는 방법이 가장 심플합니다(이 글의 “pytest fixture와의 조합” 참조).
더 대규모 프로젝트에서는 Page Object Model(POM)의 BasePage 클래스에 wait를 갖게 해서 모든 페이지에서 공통 사용하는 패턴이 일반적입니다. 타임아웃 값은 conftest.py에서 상수 관리하면 변경이 쉬워집니다.

Q. 타임아웃 값은 몇 초로 하면 되나요?

일반적인 기준은 보통의 대기로 5~10초, 네트워크가 관여하는 대기로 10~15초입니다. CI 환경은 로컬보다 느린 경우가 많으므로 여유 있게 설정합시다. 다만 길게 할수록 테스트가 느려지므로 실제 응답 시간을 측정해서 적절한 값을 설정하는 것이 중요합니다.

Q. WebDriverWait와 time.sleep을 병용해도 되나요?

기본적으로는 피해 주세요. 다만 예외로서 애니메이션 완료 대기 등 “일정 시간이 걸리는 것이 확실하고, 조건으로 판단하기 어려운 케이스”에서는 짧은 sleep(예: 0.3초)을 사용하는 실무적인 판단도 있습니다. 단 이것은 최후의 수단이며, 먼저 expected_conditions나 커스텀 조건으로 해결할 수 없는지 검토합시다.

Q. until과 until_not의 차이는 무엇인가요?

until(조건)은 조건이 True가 될 때까지 기다립니다. until_not(조건)은 조건이 False가 될 때까지 기다립니다. 로딩 스피너가 사라지는 것을 기다릴 때는 until(EC.invisibility_of_element_located(...))(또는 until_not(EC.visibility_of_element_located(...)))을 사용합니다.

Q. Playwright에도 같은 대기 처리가 필요한가요?

Playwright는 자동 대기(Auto-waiting)가 표준 탑재되어 있으므로, Selenium과 같은 명시적 대기 설정이 불필요한 경우가 대부분입니다. page.click()·page.fill() 등의 조작은 요소가 조작 가능해질 때까지 자동으로 대기합니다. 특정 조건을 기다리는 경우 page.wait_for_selector()·page.wait_for_url() 등이 사용 가능합니다.

Q. NoSuchElementException과 TimeoutException의 차이는?

NoSuchElementExceptionfind_element를 호출한 순간 요소를 찾지 못했을 때 발생합니다. TimeoutExceptionWebDriverWait의 대기 조건이 타임아웃 시간 내에 충족되지 않았을 때 발생합니다. Flaky Test에서는 많은 경우 TimeoutException이 나오므로 대기 조건이나 시간을 재검토합시다.

정리

포인트내용
time.sleep()은 사용하지 않는다불필요한 대기·Flaky Test의 원인이 된다
명시적 대기를 사용한다WebDriverWait(driver, 10).until(조건)이 기본형
EC를 상황에 맞게 사용 구분클릭→element_to_be_clickable / 입력→visibility_of_element_located / 소멸 대기→invisibility_of_element_located
혼재시키지 않는다암묵적 대기와 명시적 대기는 동시에 사용하지 않는다
WebDriverWait 상세 설정폴링 간격·무시할 예외를 세밀하게 설정하고 싶을 때 사용 (Python에 독립 FluentWait 클래스는 없음)
커스텀 조건도 만들 수 있다lambda 또는 클래스로 독자적인 대기 조건을 정의할 수 있다
fixture에 내장한다WebDriverWait를 pytest fixture로 만들어 각 테스트에서 재사용

대기 처리를 올바르게 작성하는 것은 Selenium 테스트의 안정성을 높이는 가장 효과적인 방법 중 하나입니다.
우선 보유한 테스트 코드에서 time.sleep()을 검색해 element_to_be_clickable이나 visibility_of_element_located로 교체하는 것부터 시작해 봅시다.

💡 Flaky Test의 7~8할은 대기 처리나 로케이터 문제
실무 경험상, 원인 불명의 Flaky Test를 추적하면 그 대부분이 대기가 부족하다·조건이 잘못됐다·로케이터가 깨져 있다 중 하나입니다. 먼저 대기 처리를 재검토하는 것이 Flaky Test 해소의 가장 빠른 방법입니다.
💡 CI 환경에서는 타임아웃을 길게 설정한다
GitHub Actions·GitLab CI 등 CI 환경은 로컬 머신보다 처리가 느린 경우가 있습니다. 로컬에서는 문제없이 통과하는 테스트가 CI에서 TimeoutException이 되는 경우, 타임아웃 값을 조금 길게 하거나(예: 10초 → 15초) CI 전용 설정 파일에서 조정하는 방법이 효과적입니다.

Selenium vs Playwright: 대기 처리 사상의 차이

비교 항목SeleniumPlaywright
대기의 기본 사상명시적 대기 중심 (직접 작성)Auto-waiting 중심 (표준 탑재)
기본 동작대기 없음 (즉시 탐색)조작 전에 자동으로 대기
time.sleep() 의존 리스크△ 쓰기 쉬움○ 적음
커스텀 대기WebDriverWait + EC / lambdaexpect() / wait_for_selector()
📎 Playwright의 자동 대기에 대해 자세히 알고 싶은 분은
Playwright 입문|Python으로 브라우저 자동화를 시작하는 방법을 초보자를 위해 해설
제목과 URL을 복사했습니다