Selenium에서 “요소를 찾을 수 없다”, “클릭할 수 없다”는 에러가 빈발하는 원인의 많은 부분은 대기 처리 문제입니다. time.sleep()으로 임시방편을 쓰면 Flaky Test가 되기 쉽습니다. WebDriverWait와 expected_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_contains | URL에 지정 문자열이 포함된다 | 페이지 전환 완료를 기다릴 때 |
url_to_be | URL이 지정값과 완전 일치한다 | 로그인 후 전환 대상 확인 |
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 클래스는 없으며, WebDriverWait의 timeout·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 대기 처리에서 자주 있는 함정
- presence_of_element_located로 click하려다 에러가 난다
presence_of_element_located는 DOM에 존재하는 것만 확인하며, 표시·유효 상태는 확인하지 않습니다. 클릭하려면element_to_be_clickable을 사용합시다. - 암묵적 대기와 명시적 대기를 혼재시키고 있다
두 가지를 설정하면 대기 시간이 예기치 않은 형태로 합산·경합합니다. 어느 한 쪽으로 통일하고, 권장은 명시적 대기(WebDriverWait)만 사용하는 것입니다. - 타임아웃 후 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 - 로딩 중 요소를 취득해서 StaleElementReferenceException이 난다
페이지 갱신이나 Ajax 후 재취득이 필요한 요소는 취득 후 바로 오래된 것이 됩니다.staleness_of로 오래된 요소의 소멸을 기다린 후 재취득하거나, 매번find_element를 다시 호출하는 설계로 합시다. - 애니메이션 중 요소를 클릭해서 ElementClickInterceptedException이 난다
모달의 표시 애니메이션이나 툴팁이 요소 위에 겹쳐 있으면 클릭이 실패합니다. 먼저 로케이터·대기 조건·오버레이 요소의 유무를 확인하는 것이 근본 해결의 지름길입니다.ActionChains나 JavaScript 클릭(driver.execute_script("arguments[0].click();", element))으로 회피할 수 있는 경우도 있지만, 이것들은 문제를 숨길 수 있으므로 최후의 수단으로 검토합시다. - 타임아웃 값을 모두 같게 하고 있다
페이지 전환(5~10초), API 응답(3~5초), 애니메이션(1~2초) 등 대기 내용에 따라 적절한 타임아웃 값은 다릅니다. 일률적으로 10초 등으로 하면 느린 테스트를 양산하게 됩니다. - driver.get() 후 아무것도 기다리지 않고 요소를 취득하고 있다
driver.get()은 HTML 로딩 완료(document.readyState == "complete")까지 기다리지만, JavaScript에 의한 동적 콘텐츠 로딩은 기다리지 않습니다. 페이지 전환 후 요소를 조작할 때도WebDriverWait를 사용합시다.
자주 묻는 질문 (FAQ)
기본 폴링 간격은 0.5초입니다. 즉 조건이 충족됐는지를 0.5초마다 체크합니다. 이 간격은 poll_frequency 파라미터로 변경할 수 있습니다(예: WebDriverWait(driver, 10, poll_frequency=0.2)). 체크 빈도를 높이면 응답성이 향상되지만 브라우저에 대한 부하도 증가합니다.
pytest fixture에 내장하는 방법이 가장 심플합니다(이 글의 “pytest fixture와의 조합” 참조).
더 대규모 프로젝트에서는 Page Object Model(POM)의 BasePage 클래스에 wait를 갖게 해서 모든 페이지에서 공통 사용하는 패턴이 일반적입니다. 타임아웃 값은 conftest.py에서 상수 관리하면 변경이 쉬워집니다.
일반적인 기준은 보통의 대기로 5~10초, 네트워크가 관여하는 대기로 10~15초입니다. CI 환경은 로컬보다 느린 경우가 많으므로 여유 있게 설정합시다. 다만 길게 할수록 테스트가 느려지므로 실제 응답 시간을 측정해서 적절한 값을 설정하는 것이 중요합니다.
기본적으로는 피해 주세요. 다만 예외로서 애니메이션 완료 대기 등 “일정 시간이 걸리는 것이 확실하고, 조건으로 판단하기 어려운 케이스”에서는 짧은 sleep(예: 0.3초)을 사용하는 실무적인 판단도 있습니다. 단 이것은 최후의 수단이며, 먼저 expected_conditions나 커스텀 조건으로 해결할 수 없는지 검토합시다.
until(조건)은 조건이 True가 될 때까지 기다립니다. until_not(조건)은 조건이 False가 될 때까지 기다립니다. 로딩 스피너가 사라지는 것을 기다릴 때는 until(EC.invisibility_of_element_located(...))(또는 until_not(EC.visibility_of_element_located(...)))을 사용합니다.
Playwright는 자동 대기(Auto-waiting)가 표준 탑재되어 있으므로, Selenium과 같은 명시적 대기 설정이 불필요한 경우가 대부분입니다. page.click()·page.fill() 등의 조작은 요소가 조작 가능해질 때까지 자동으로 대기합니다. 특정 조건을 기다리는 경우 page.wait_for_selector()·page.wait_for_url() 등이 사용 가능합니다.
NoSuchElementException은 find_element를 호출한 순간 요소를 찾지 못했을 때 발생합니다. TimeoutException은 WebDriverWait의 대기 조건이 타임아웃 시간 내에 충족되지 않았을 때 발생합니다. 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를 추적하면 그 대부분이 대기가 부족하다·조건이 잘못됐다·로케이터가 깨져 있다 중 하나입니다. 먼저 대기 처리를 재검토하는 것이 Flaky Test 해소의 가장 빠른 방법입니다.
GitHub Actions·GitLab CI 등 CI 환경은 로컬 머신보다 처리가 느린 경우가 있습니다. 로컬에서는 문제없이 통과하는 테스트가 CI에서
TimeoutException이 되는 경우, 타임아웃 값을 조금 길게 하거나(예: 10초 → 15초) CI 전용 설정 파일에서 조정하는 방법이 효과적입니다.Selenium vs Playwright: 대기 처리 사상의 차이
| 비교 항목 | Selenium | Playwright |
|---|---|---|
| 대기의 기본 사상 | 명시적 대기 중심 (직접 작성) | Auto-waiting 중심 (표준 탑재) |
| 기본 동작 | 대기 없음 (즉시 탐색) | 조작 전에 자동으로 대기 |
| time.sleep() 의존 리스크 | △ 쓰기 쉬움 | ○ 적음 |
| 커스텀 대기 | WebDriverWait + EC / lambda | expect() / wait_for_selector() |
Playwright 입문|Python으로 브라우저 자동화를 시작하는 방법을 초보자를 위해 해설
