대기 전략(Waiting strategies)
대기 전략(Waiting strategies)
브라우저 자동화에서 가장 흔한 난관이 뭘까요? 바로 '원하는 순간에 앱이 그 명령을 받아들일 수 있는 상태냐'를 보장하는 문제예요. 코드가 너무 빨리 실행돼 브라우저가 아직 준비되지 않았거나, 반대로 브라우저가 준비됐는데 코드가 늦게 도착하면 둘 다 원하는 대로 동작하지 않아요. 이렇게 타이밍이 어긋나며 생기는 문제를 경쟁 상태(race condition)라고 하고, 이게 불안정한 테스트(flaky test)의 주요 원인이 돼요.
모든 탐색 명령은 페이지 로드 전략(page load strategy)에 따라 특정 readyState 값을 기다린 뒤 코드에 제어권을 돌려줘요. 기본값은 "complete"예요. 그런데 이 readyState는 HTML에 정의된 자산만 신경 쓸 뿐이라, 자바스크립트로 로드된 요소는 아직 페이지에 없을 수 있어요. 특히 단일 페이지 앱(SPA)에서는 클릭에 따라 요소가 동적으로 추가되거나 보이기가 바뀌는데, Selenium이 요소와 상호작용하려면 그 요소가 페이지에 존재하고 *표시(displayed)*되어 있어야 해요. 그렇지 않으면 명령이 실패하죠.
이런 상황에서 많은 사람이 떠올리는 해결책은 특정 시간만큼 멈추는 sleep문을 넣는 거예요. 그런데 코드는 정확히 얼마나 기다려야 하는지 알 수 없어서, 너무 짧게 자면 실패하고, 너무 길게 자면 세션 시간이 지나치게 늘어나요. Selenium은 이보다 나은 두 가지 동기화 메커니즘을 제공해요.
본문
암시적 대기(Implicit waits)
Selenium에 내장된 자동 대기 방식이 암시적 대기예요. 암시적 대기 값은 브라우저 옵션의 timeouts 기능으로 설정하거나, 아래처럼 드라이버 메서드로 설정할 수 있어요.
이 설정은 세션 전체의 모든 요소 탐색 호출에 적용되는 전역 설정이에요. 기본값은 0이라서, 요소를 찾지 못하면 즉시 오류를 반환해요. 암시적 대기를 설정하면 드라이버는 오류를 반환하기 전에 그 값만큼 기다려요. 요소가 찾아지는 순간 드라이버는 요소 참조를 반환하고 코드가 계속 실행되므로, 암시적 대기 값을 크게 잡는다고 해서 반드시 세션 시간이 늘어나는 건 아니에요.
주의: 암시적 대기와 명시적 대기를 섞지 마세요. 예측할 수 없는 대기 시간이 생길 수 있어요. 예를 들어 암시적 대기를 10초, 명시적 대기를 15초로 설정하면 타임아웃이 20초 후에 발생할 수도 있어요.
암시적 대기로 아까 예시를 풀면 이렇게 돼요.
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));
driver.implicitly_wait(2)
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2);
driver.manage.timeouts.implicit_wait = 2
await driver.manage().setTimeouts({ implicit: 2000 });
명시적 대기(Explicit waits)
명시적 대기는 코드에 추가하는 반복 루프로, 특정 조건이 참(true)이 될 때까지 애플리케이션을 폴링(polling)하다가 그 조건이 충족되면 루프를 빠져나가 다음 명령을 실행해요. 조건이 정해진 타임아웃 값 안에 충족되지 않으면 타임아웃 오류를 반환해요. 애플리케이션이 원하는 상태가 아닐 경우가 워낙 다양하다 보니, 명시적 대기는 필요한 곳마다 정확한 대기 조건을 지정하기에 좋은 선택이에요. 또 Selenium의 Wait 클래스는 기본적으로 지정한 요소가 존재하기를 자동으로 기다려 주는 장점도 있어요.
아래 예시는 *람다(lambda)*로 대기 조건을 나타내요. Java에서는 예상 조건(Expected Conditions)도 지원해요.
Wait<WebDriver> wait = new WebDriverWait(driver, Duration.ofSeconds(2));
wait.until(d -> revealed.isDisplayed());
wait = WebDriverWait(driver, timeout=2)
wait.until(lambda _ : revealed.is_displayed())
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
wait.Until(d => revealed.Displayed);
wait = Selenium::WebDriver::Wait.new
wait.until { revealed.displayed? }
await driver.wait(until.elementIsVisible(revealed), 2000);
커스터마이징
Wait 클래스는 조건 평가 방식을 바꾸는 여러 매개변수로 생성할 수 있어요. 예를 들면 이런 것들이 있죠.
- 평가 간격(폴링 주기)을 얼마나 자주 할지
- 어떤 예외를 자동으로 처리할지
- 전체 타임아웃 길이
- 타임아웃 메시지 커스터마이징
만약 요소 상호작용 불가(element not interactable) 오류가 기본으로 재시도된다면, 실행 중인 코드 안의 메서드에 액션을 추가할 수 있어요. 단, 성공했을 때 true를 반환하도록 해야 해요. Java에서 Wait를 커스터마이징하는 가장 쉬운 방법은 FluentWait 클래스를 쓰는 거예요.
Wait<WebDriver> wait =
new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(2))
.pollingEvery(Duration.ofMillis(300))
.ignoring(ElementNotInteractableException.class);
wait.until(
d -> {
revealed.sendKeys("Displayed");
return true;
});
errors = [NoSuchElementException, ElementNotInteractableException]
wait = WebDriverWait(driver, timeout=2, poll_frequency=.2, ignored_exceptions=errors)
wait.until(lambda _ : revealed.send_keys("Displayed") or True)
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2))
{
PollingInterval = TimeSpan.FromMilliseconds(300),
};
wait.IgnoreExceptionTypes(typeof(ElementNotInteractableException));
wait.Until(d => {
revealed.SendKeys("Displayed");
return true;
});
errors = [Selenium::WebDriver::Error::NoSuchElementError,
Selenium::WebDriver::Error::ElementNotInteractableError]
wait = Selenium::WebDriver::Wait.new(timeout: 2,
interval: 0.3,
ignore: errors)
wait.until { revealed.send_keys('Displayed') || true }