networkidle 대기가 타임아웃으로 끝날 때, 진짜 봐야 할 지점

networkidle 대기가 타임아웃으로

로그인 여부를 가리려고 page.wait_for_load_state("networkidle", timeout=30000)을 걸어 뒀는데, 로그를 열어 보면 거의 매번 30000ms를 꽉 채우고 타임아웃이 떨어졌습니다. 네트워크 탭을 같이 열어 보니 요청이 멈추지 않아서가 아니라, 광고 스크립트 하나가 몇 초 간격으로 계속 비콘을 쏘고 있어서 “500ms 동안 조용한 순간”이 끝까지 오지 않는 상태였습니다. networkidle 대기가 타임아웃으로 끝날 때 정말 기다려야 하는 건 네트워크가 아니라, 화면에 실제로 나타난 요소인 경우가 많습니다.

networkidle이 ‘조용하다’고 판단하는 기준

Playwright 공식 문서는 networkidle 상태를 네트워크 연결이 최소 500ms 동안 새로 발생하지 않는 시점으로 정의합니다. 페이지가 로드된 뒤 요청이 완전히 끊기는 순간을 잡으려는 목적인데, 요청이 끊기지 않으면 이 조건 자체가 성립하지 않습니다.

문제는 요즘 대부분의 페이지가 이 조건을 잘 만족하지 않는다는 점입니다. 채팅 위젯, 광고 트래킹, 실시간 알림 같은 기능이 백그라운드에서 계속 요청을 보내면 networkidle은 수십 초가 지나도 오지 않고, 결국 걸어 둔 timeout 값만큼 기다리다 그대로 예외를 던집니다. 자세한 정의와 옵션은 Playwright의 load state 문서에 나와 있습니다.

타임아웃이 반복되는 흔한 원인

가장 많이 보는 패턴은 지속적인 연결입니다. 웹소켓이 열려 있거나, 짧은 간격으로 서버에 상태를 물어보는 폴링 요청이 돌고 있으면 네트워크는 사실상 영원히 조용해지지 않습니다. 이런 방식 자체는 널리 쓰이는 폴링 기법 중 하나이고, 서버 부담과 응답 지연 사이의 절충점으로 이해하면 왜 주기적으로 요청이 나가는지 짐작할 수 있습니다.

두 번째는 서드파티 스크립트입니다. 방문자 수 집계, 광고 네트워크, 댓글 위젯 같은 외부 리소스가 페이지 본문과 상관없이 자체 주기로 통신을 이어 가면, 본문은 다 떴는데도 networkidle만 계속 대기 상태로 남습니다. 세 번째는 파일 다운로드나 장시간 API 응답처럼 의도적으로 네트워크가 열려 있어야 하는 상황인데, 이 경우는 networkidle이 아예 맞지 않는 대기 조건을 쓰고 있는 셈입니다.

브라우저 개발자 도구 네트워크 탭 화면

try/except로 감싼 대기가 숨기는 문제

로그인 상태를 자동 감지하는 스크립트에서 자주 보이는 형태는 이런 코드입니다.

try:
    page.wait_for_load_state("networkidle", timeout=timeout_ms)
except Exception:
    pass

html = page.content()
is_logged_in = "로그아웃" in html

이 코드는 타임아웃이 나도 예외를 그냥 삼키고 다음 줄로 넘어갑니다. 당장 스크립트가 멈추지 않으니 편하지만, networkidle이 실패한 시점의 페이지가 로그인 처리 중간 상태일 수도 있고, 광고 스크립트 때문에 늦게 멈췄을 수도 있어서 is_logged_in 판정이 타이밍에 따라 들쭉날쭉해질 수 있습니다. 예외를 무시하는 대신, 타임아웃이 난 이유를 로그로 한 줄 남겨 두면 나중에 디버깅할 때 네트워크 문제인지 셀렉터 문제인지 구분하기 쉬워집니다.

networkidle 대신 기다릴 수 있는 신호들

로그인 여부처럼 결과가 특정 요소의 등장으로 확인되는 경우라면, 네트워크가 아니라 그 요소 자체를 기다리는 쪽이 훨씬 안정적입니다.

from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

try:
    page.get_by_text("로그아웃").wait_for(state="visible", timeout=timeout_ms)
    is_logged_in = True
except PlaywrightTimeoutError:
    is_logged_in = False

이렇게 바꾸면 광고 스크립트가 몇 초마다 요청을 보내는 것과 상관없이, “로그아웃” 텍스트가 실제로 화면에 떴는지만 보고 판정합니다. 네 가지 대기 방식을 정리하면 아래와 같습니다.

파이썬 자동화 스크립트 코드 디버깅

방식 실제로 기다리는 대상 트레이드오프
networkidle 500ms 동안 새 네트워크 연결 없음 폴링·광고 스크립트가 있으면 영영 안 끝남
locator.wait_for(state=”visible”) 특정 요소가 화면에 보이는 시점 요소 자체가 안 뜨면 똑같이 타임아웃
wait_for_function 커스텀 JS 조건식의 결과 조건을 잘못 짜면 거짓 성공 가능
time.sleep 고정 대기 정해 둔 시간 느린 네트워크엔 부족, 빠른 네트워크엔 낭비

표에서 보듯 locator 대기도 완벽한 해법은 아닙니다. 기다리는 요소가 어떤 이유로든 끝까지 나타나지 않으면 똑같이 타임아웃까지 가므로, 어떤 방식을 쓰든 실패했을 때의 기본값(로그인 안 됨으로 간주할지, 재시도할지)을 코드에 명시해 두는 편이 안전합니다.

그래도 networkidle이 맞는 경우

정적인 콘텐츠 페이지, 외부 트래킹 스크립트가 거의 없는 내부 관리 도구라면 networkidle만으로도 충분히 안정적으로 끝납니다. 요청이 몇 개 뜨고 자연스럽게 멈추는 구조에서는 500ms 조건이 금방 채워지기 때문입니다.

반대로 실시간성이 있는 서비스형 페이지에서는 networkidle에 기대지 말고, 기다릴 요소를 명확히 정해서 그 요소 기준으로 바꾸는 쪽이 유지보수에도 유리합니다. 어떤 대기 방식이든 timeout 숫자를 바꾸는 것만으로는 근본 원인이 바뀌지 않는다는 점은 같습니다.

타임아웃 값만 늘리면 해결될까요?

timeout_ms를 30000에서 60000으로 늘리면 당장 에러는 덜 보일 수 있지만, 네트워크가 계속 열려 있는 구조라면 60초 뒤에도 똑같이 타임아웃이 납니다. 숫자를 늘리는 건 증상을 늦추는 것이고, 원인은 여전히 “조용해지지 않는 네트워크”에 있다는 점을 기억해 두시는 게 좋습니다.

networkidle 대기가 타임아웃으로 끝날 때는 먼저 네트워크 탭에서 어떤 요청이 반복되고 있는지 확인해 보시고, 그 요청이 본문과 무관한 스크립트라면 과감히 요소 기반 대기로 바꿔 보시는 걸 권해 드립니다.

CI에서만 테스트가 깨질 때, 로컬과 다른 네 가지를 좁히는 순서

Leave a Comment