왜 Selenium 테스트가 불안정한가 (그리고 영원히 해결하는 방법)

발행: (2025년 12월 15일 오후 10:08 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

이 기사에서 다루는 내용

  • 불안정성 문제time.sleep()WebDriverWait만으로는 충분하지 않은 이유
  • 불안정한 테스트의 원인 – UI 상태 변화와의 경쟁
  • 안정성 솔루션 – DOM, 네트워크, 애니메이션, 레이아웃 변화를 모니터링
  • 한 줄 통합stabilize()로 드라이버를 감싸기 — 테스트 수정 없이
  • 전체 진단 – 테스트가 차단된 정확한 이유 파악

Selenium을 일주일 이상 사용해 본 적이 있다면, 아마 다음과 같은 코드를 작성했을 것입니다:

driver.get("https://myapp.com/dashboard")
time.sleep(2)  # Wait for page to load
driver.find_element(By.ID, "submit-btn").click()
time.sleep(1)  # Wait for AJAX

잘못된 코드라는 걸 알면서도 “동작한다”는 안도감을 느낄 수 있습니다. 하지만 어느 순간 그 안도감은 사라집니다. CI 서버가 로컬 머신보다 10 % 느려지고, 갑자기 테스트가 20 % 정도 실패하기 시작합니다.

이것이 불안정한 테스트의 이야기이며, 왜 발생하는지, 그리고 waitless라는 라이브러리가 어떻게 이를 영원히 없앨 수 있는지 설명합니다.

불안정성 문제

실제 시나리오를 생각해 보세요: 사용자가 버튼을 클릭하고, API 호출이 이루어지고, 데이터가 반환되고, React가 다시 렌더링하고, 스피너가 사라지고, 테이블이 나타나는 React 대시보드. 전체 흐름은 약 400 ms가 걸리지만, 테스트는 다음과 같이 작성됩니다:

button = driver.find_element(By.ID, "load-data")
button.click()
table = driver.find_element(By.ID, "data-table")  # 💥 BOOM

테이블이 아직 존재하지 않기 때문에 Selenium은 NoSuchElementException을 발생시킵니다. 흔히 쓰이는 빠른 “수정”은 다음과 같습니다:

button.click()
time.sleep(2)
table = driver.find_element(By.ID, "data-table")  # Works… usually

time.sleep()의 문제

  • 불필요한 지연을 추가함 (예: 필요보다 2초 느림)
  • API가 예상보다 오래 걸릴 때 여전히 불안정함
  • 실패가 발생했을 때 원인 파악이 어려움

전통적인 해결책이 작동하지 않는 이유

time.sleep() — 순진한 접근법

고정된 시간만큼 대기하고 UI가 준비되길 기대합니다.

문제점:

  • 너무 짧음 → 테스트 실패
  • 너무 김 → 테스트 스위트가 느려짐
  • 실제 상태에 대한 피드백 없음

WebDriverWait — “올바른” 접근법

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "submit-btn"))
)

특정 조건을 기다리지만 하나의 요소만 확인합니다. 다음을 무시합니다:

  • 진행 중인 애니메이션으로 인한 오버레이
  • 대기 중인 AJAX 요청
  • 요소를 이동시키는 React 재렌더링

재시도 데코레이터 — 부정적인 접근법

@retry(tries=3, delay=1)
def test_dashboard():
    driver.find_element(By.ID, "submit-btn").click()

재시도는 불안정성을 숨길 뿐, 해결하지는 못합니다.

실제로 불안정한 테스트를 일으키는 원인은?

수백 개의 불안정한 테스트를 디버깅한 결과, 근본 원인은 UI와의 경쟁입니다:

수행한 작업실제 일어나는 일
버튼 클릭프레임워크에 의해 DOM이 변형 중
텍스트 내용 검증AJAX 응답이 아직 진행 중
모달과 상호작용CSS 전환이 아직 애니메이션 중
네비게이션 링크 클릭레이아웃 이동으로 요소가 움직임

진짜 질문은 “이 요소가 클릭 가능한가?”가 아니라 “전체 페이지가 안정적이고 상호작용할 준비가 되었는가?” 입니다.

“안정성” 정의

안정적인 UI를 나타내는 네 가지 핵심 신호가 있습니다:

  1. DOM 안정성 – 요소가 추가, 제거, 수정되지 않음.
    감지: MutationObserver로 문서 루트를 감시하고, 마지막 변형 이후 경과 시간을 추적합니다.

  2. 네트워크 유휴 – 모든 AJAX 요청이 완료됨.
    감지: fetch()XMLHttpRequest를 가로채고, 대기 중인 요청 수를 셉니다.

  3. 애니메이션 완료 – 모든 CSS 애니메이션 및 전환이 끝남.
    감지: animationstart, animationend, transitionstart, transitionend 이벤트를 청취합니다.

  4. 레이아웃 안정성 – 요소 이동이 멈추고 레이아웃 변동이 없음.
    감지: 인터랙티브 요소들의 바운딩 박스 위치를 시간에 따라 추적합니다.

아키텍처

JavaScript 계측 (브라우저에서 실행)

window.__waitless__ = {
    pendingRequests: 0,
    lastMutationTime: Date.now(),
    activeAnimations: 0,

    isStable() {
        if (this.pendingRequests > 0) return false;
        if (Date.now() - this.lastMutationTime < 100) return false;
        // Add additional checks for animations and layout if needed
        return true;
    }
};

스크립트는 execute_script()를 통해 주입되며 DOM 변형, 네트워크 활동, 애니메이션을 모니터링합니다.

Python 엔진 (안정성 평가)

class StabilizationEngine:
    def wait_for_stability(self):
        """Wait until all stability signals are satisfied."""
        # Checks performed automatically:
        # ✓ DOM mutations have settled
        # ✓ Network requests completed
        # ✓ Animations finished
        # ✓ Layout is stable

엔진은 isStable()True를 반환할 때까지 브라우저 상태를 반복적으로 조회합니다.

마법: 한 줄 통합

테스트를 수정할 필요가 전혀 없습니다. 드라이버를 감싸는 한 줄만 추가하면 됩니다:

from waitless import stabilize

driver = webdriver.Chrome()
driver = stabilize(driver)  # ← Only change needed

# Existing tests work unchanged
driver.find_element(By.ID, "button").click()  # Auto‑waits!

stabilize()find_element() 호출을 가로채는 StabilizedWebDriver를 반환합니다. 반환된 요소들은 StabilizedWebElement로 감싸지며, 그 click() 메서드는 먼저 안정성을 기다린 뒤 클릭을 수행합니다:

class StabilizedWebElement:
    def click(self):
        self._engine.wait_for_stability()  # Auto‑wait!
        return self._element.click()      # Then click

이제 테스트는 기다리고 있다는 사실을 인식하지 못하고, 단순히 실패하지 않을 뿐입니다.

엣지 케이스 처리

실제 애플리케이션은 스피너, 분석 폴링, WebSocket 하트비트 등 지속적인 활동을 가집니다. waitless는 구성 가능한 임계값을 제공합니다.

예시: 무한 애니메이션 무시

from waitless import StabilizationConfig, stabilize

config = StabilizationConfig(
    network_idle_threshold=2,      # Allow up to 2 pending requests
    animation_detection=False,    # Ignore spinners/continuous animations
    strictness='relaxed'          # Only check DOM mutations
)

driver = stabilize(driver, config=config)

테스트를 다시 작성하지 않고도 앱의 동작에 맞게 감지를 맞춤 설정할 수 있습니다.

Back to Blog

관련 글

더 보기 »