Engineering
불안정한 테스트를 신뢰로 바꾸는 과정: Playwright Flaky Test 개선기
릴리Lily(김서연) / 자동화QA팀여기어때
2026년 2월 12일
원문에서 보기 ↗안녕하세요. 여기어때컴퍼니 자동화QA팀 릴리입니다.
내 PC에선 성공하는데 CI 파이프라인에서는 실패하는 테스트, 원인을 알 수 없는 간헐적 오류들을 한 번쯤 겪어보셨나요?
오늘은 고질적인 Flaky Test 문제를 해결하기 위해 Robot Framework에서 Playwright로 마이그레이션하며 겪었던 시행착오와 이를 통해 테스트 신뢰도를 개선한 경험을 공유하려고 합니다.

Robot Framework가 남긴 숙제들
기존에는 Robot Framework를 사용하여 테스트 자동화를 진행했으나, 테스트 케이스가 늘어날수록 구조적 한계와 요소 탐색 불안정성이라는 벽에 부딪혔습니다.
① Headless 모드의 불안정성
CI 파이프라인을 이용하여 테스트 자동화를 진행하기 위해서는 브라우저 창을 띄우지 않는 Headless 모드가 필수적입니다. 하지만 기존의 Robot Framework + Selenium 환경에서는 유독 Headless 모드에서 요소 탐색 실패율이 급증했습니다. 결국 테스트 정확도를 위해 로컬 PC에서 GUI 모드로 실행해야 했고, 결과 리포트마저 수동으로 공유해야 했습니다.

② 기다림으로 때운 신뢰성, 그리고 기약 없는 디버깅
요소를 찾지 못해 실패하는 일을 막고자 스크립트 곳곳에 Sleep이나 Wait과 같은 대기 구문을 남발했습니다. 그 결과, 단순한 ‘홈 화면’ 검증 하나에만 약 7분이 소요되었습니다. 더 큰 문제는 ‘디버깅의 비효율’이었습니다. 요소 탐색 실패는 전체 실행 시에 주로 발하기 때문에, 오류 하나를 확인하기 위해 수십 분을 대기해야 했습니다.
그래서 여러 논의 끝에 저희 팀은 별도의 대기 코드 없이도 UI 상태를 자동으로 기다려주는 자동 대기(auto-wait) 메커니즘과 강력한 디버깅 도구, 그리고 안정적인 Headless 모드 실행을 지원하는 Playwright를 도입하게 되었습니다.
Flaky Test와의 전쟁: 도구는 거들 뿐, 핵심은 ‘로직’
도구를 바꾼다고 해서 모든 테스트가 성공하는 것은 아니었습니다. Playwright가 브라우저 제어의 불안정성은 해결해 주었지만, 데이터의 불안정성은 여전히 남아있었기 때문입니다. 날짜, 재고, 가격 등 시시각각 변하는 여행 서비스 특유의 변수들은 도구만으로 제어할 수 없었습니다. 지금부터는 도구의 전환을 넘어, 로직의 견고함을 더해가는 과정을 소개하겠습니다.
Case 1. 팝업 Interception 자동 처리 전략
E2E 테스트를 작성하다 보면 특정 조건에서만 노출되는 ‘안내 팝업’들이 있습니다. 여기어때 서비스의 경우, 패키지 캘린더 진입 시 “여행 기간 대신, 출발일만 선택해요”라는 안내 팝업이 최초 1회 노출되는 것이 대표적입니다.

“출발일만 선택해요” 안내 팝업
초기에는 팝업이 노출되는 모든 위치를 파악해 ‘닫기’ 코드를 일일이 삽입하는 수동 방식을 택했습니다. 하지만 다음과 같은 문제들이 발생했습니다.
- 휴먼 에러: 팝업이 뜨는 모든 진입점을 완벽하게 파악해 코드를 넣어야 하는데, 실수로 로직을 빠뜨리는 경우가 종종 발생했습니다.
- 타이밍 이슈: 팝업이 다 노출되기 전에 스크립트가 뒤에 있는 요소를 먼저 클릭하려 하거나, 팝업 애니메이션 중에 클릭을 시도하여 실패하는 등의 현상이 발생했습니다.
이를 해결하기 위해 conftest.py에 전역 핸들러를 등록하여, 테스트 중 언제든 팝업이 감지되면 자동으로 닫히도록 했습니다. 또한, force=True 옵션과 명시적인 대기 로직을 통해 미세한 타이밍 이슈를 해결했습니다.
try:
popup_locator = page_instance.get_by_role("dialog", name="여행 기간 대신, 출발일만 선택해요")
def close_date_popup():
close_btn = page_instance.get_by_role("button", name="확인").first
if close_btn.is_visible():
close_btn.click(force=True)
page_instance.wait_for_timeout(1000)
if popup_locator.is_hidden():
print("\\n[Auto-Handler] '출발일만 선택해요' 팝업 닫음")
page_instance.add_locator_handler(popup_locator, close_date_popup)
except Exception as e:
print(f"\\n[Warning] 팝업 핸들러 등록 중 오류 발생: {e}")
Case 2. 보이는 것만 정확하게 타겟팅하기
자동화된 테스트 실패의 주된 원인 중 하나는 ‘사용자가 보는 화면’과 ‘DOM’의 불일치입니다. 웹페이지는 반응형 디자인이나 상태 관리를 위해, 화면에는 보이지 않지만 HTML상에는 존재하는 요소들이 많습니다. 모바일 뷰를 위해 숨겨둔 메뉴나, 닫혀있는 팝업의 잔재들이 DOM 트리에는 여전히 남아있는 경우가 대표적입니다.
이런 상황에서 단순히 “두 번째(nth(1)) 버튼을 눌러”라고 지시하면 어떻게 될까요? Playwright가 사람 눈에 보이지 않는 ‘숨겨진 첫 번째 버튼’을 타겟팅하게 되고, 결국 엉뚱한 요소를 클릭하거나 테스트 실패로 이어지게 됩니다.
이를 해결하기 위해 DOM의 순서가 아닌 UX에 기반한 Selector 전략을 사용했습니다. 단순히 텍스트 포함 여부만 확인하는 것을 넘어, 정규식으로 텍스트를 검증하고 Visibility 필터를 연속 적용했습니다.
page.locator("div").filter(has_text=re.compile(r"^총 인원 41$")).locator("visible=true").first.click()
Case 3. 재고와 날짜의 변수
날짜 선택 로직을 검증할 때 가장 큰 복병은 ‘달력’과 ‘재고’입니다. 패키지여행 서비스에는 “출발일은 최대 31일까지만 선택할 수 있어요”라는 제한 로직이 있습니다. 이를 검증하기 위해 처음에 저는 단순한 가설을 세웠습니다.
“오늘이 월말이라 이번 달 남은 날짜가 하루 이틀뿐이라도, 다음 달 전체(28~31일)를 더하면 무조건 31일은 넘어서 팝업이 뜨겠지?”
[문제 상황1]
테스트 실행 시점에 따라 ‘두 달’을 합쳐도 31일이 안 되는 경우가 있었습니다. 만약 오늘이 3월 31일이라고 가정해 본다면,
- 3월 전체 선택: 오늘인 31일, 딱 1일만 선택됩니다.
- 4월 전체 선택: 4월은 30일까지 있습니다.
- 결과: 31일이 선택되어, 제한 조건을 만족하지 못해 팝업이 뜨지 않고 테스트가 실패합니다.
[문제 상황2]
더 결정적인 원인은 재고였습니다. 앱에서 [N월 전체 선택] 버튼을 클릭했을 때, 달력의 모든 날짜가 선택되는 게 아니라, 실제 예약 가능한 상품이 있는 날짜만 선택됩니다.
- 오늘이 1월 1일이라도, 1월에 남은 패키지 상품이 5개뿐이라면 ‘1월 전체’를 눌러도 선택된 날짜는 5일뿐입니다.

뉴질랜드 크라이스트처치
- 상품이 아예 없는 달은 체크박스 자체가 비활성화 되어 클릭 조차 할 수 없어 테스트가 실패합니다.

케냐 마사이마라
저는 불확실한 ‘이번 달’을 버리고, 상품이 안정적으로 확보된 미래의 두 달을 타겟팅하도록 수정했습니다.
- 데이터가 풍부한 도시 선정: 재고 부족 변수를 없애기 위해, 상품이 가장 많은 인기 도시(오사카, 도쿄 등)를 검색 조건으로 고정했습니다.

일본 오사카
- 캘린더 강제 이동: 애매한 월말을 피하기 위해 캘린더 넘김 버튼을 클릭하여 익월 및 익익월이 보이는 화면으로 강제 이동시켰습니다.
# 캘린더 이동
page.locator(".css-ujq9fl > button:nth-child(2)").click()
# 2. 미래의 두 달 전체 선택
page.get_by_role("checkbox", name=f"체크박스 {next_month}월 전체").click()
page.get_by_role("checkbox", name=f"체크박스 {month_after_next}월 전체").click()
# 3. 팝업 노출 검증
expect(page.get_by_text("출발일은 최대 31일까지만 선택할 수 있어요")).to_be_visible()
Case 4. 00시 00분의 시간차와 Pytest Hook
자동화된 테스트를 4시간 간격으로 스케줄링하여 운영하던 중, 자정 시간에만 테스트가 실패하는 현상이 발생했습니다. 문제가 된 케이스는 ‘일정 영역 확인’이었습니다. 이 테스트는 웹에 진입했을 때 국내숙소(오늘~내일 1박), 해외숙소(14일 뒤 가장 빠른 화요일) 날짜가 디폴트값으로 노출되고 있는지 텍스트를 검증합니다.
- Python 스크립트: 1월 23일 00시 00분에 실행.
today를 1월 23일로 인식 → "1.23 ~ 1.24" 텍스트 기대 - 웹: 00시가 되었지만, 서버나 프론트엔드의 날짜 갱신 로직이 도는 데 미세한 지연 발생. 여전히 1월 22일 기준으로 “1.22 ~ 1.23”을 노출
저는 이 00:00 ~ 00:10 구간을 ‘데이터 갱신을 위한 불안정 구간’이라고 정의하고, 이 시간대에는 날짜 관련 테스트를 수행하지 않도록 Pytest의 Marker와 Hook 기능을 활용했습니다.
① Custom Marker 생성
먼저, 자정에 실행하면 위험한 테스트에 붙일 @pytest.mark.skip_at_midnight를 만들었습니다.
② Conftest Hook 구현
conftest.py에 테스트가 실행되기 직전에 호출되는 Hook을 구현했습니다. 이 Hook은 현재 시간이 ‘자정 직후 10분’ 사이라면 해당 테스트를 skip 처리합니다.
def pytest_runtest_setup(item):
marker = item.get_closest_marker("skip_at_midnight")
if marker:
kst = ZoneInfo("Asia/Seoul")
now = datetime.now(kst)
if now.hour == 0 and now.minute <= 10:
pytest.skip("서버 날짜 갱신 지연(00:00~00:10)으로 인해 테스트를 스킵합니다.")
이 전략을 도입한 후, 매일 자정마다 발생하던 가짜 오류가 사라졌습니다. 무조건 테스트를 통과시키는 것만이 정답은 아닙니다. 시스템의 특성상 불가피하게 발생하는 지연 구간을 파악하고, “검증하지 않아야 할 때를 아는 것” 또한 테스트의 신뢰도를 높이는 중요한 부분입니다.
Case 5. 데이터가 하나뿐일 때의 예외 처리
마지막으로 필터 기능 검증 시 발생하는 오류입니다. 보통 필터 테스트는 다음 3단계로 진행됩니다.
목록 확인 → 선택 → 해제
문제는 해제 단계인데요, 만약 테스트하는 셀러카드의 상품을 A항공사가 독점하고 있다면 어떨까요?
- 필터 선택: ‘A 항공사’ 칩 클릭 → 목록에 ‘A 항공사’만 노출

항공사 필터칩이 1개인 경우
- 필터 해제: ‘A 항공사’ 칩 재선택 → 전체보기가 되었지만, 데이터가 ‘A 항공사’ 뿐이라 상품 목록은 그대로

PLP에 노출된 상품의 항공사가 모두 동일함
이 경우, 필터를 해제했음에도 화면상의 변화가 전혀 없습니다. 필터가 정상적으로 해제된 것인지, 아니면 기능 고장으로 해제가 안 된 것인지 스크립트가 판단하기 어렵습니다. 검증의 기준이 모호해지기 때문에, 테스트를 통과시킨다면 테스트의 신뢰도가 떨어질 수 있습니다.
신뢰도를 높이기 위해 ‘데이터 개수’를 기준으로 테스트 수행 여부를 결정하는 Dynamic Skip 로직을 추가했습니다. 첫 번째 테스트에서 감지된 항공사 개수가 1개라면, 필터를 껐다 켜는 비교 검증이 무의미하다고 판단하여, skip처리 했습니다.
def test_필터_항공사_필터재선택(self, page: PageActions):
if TestAirline.total_airline_count == 1:
pytest.skip("항공사 필터가 1개여서 필터 해제 검증을 진행할 수 없습니다.")
# ... (이하 필터 해제 및 다른 상품 노출 확인 로직) ...
if found_other_airline:
pass
else:
raise AssertionError(f"필터 해제 실패: 목록이 갱신되지 않았습니다.")
Flaky Test와의 전쟁을 마치며
이번 글에서 소개한 5가지 사례는 여기어때 자동화QA팀에서 4시간 간격으로 스케줄링된 통합검증 테스트 자동화를 운영하며 쌓인 리포트 중 가장 빈번하게 발생했던 반복적인 Fail 케이스를 선별한 기록입니다.
DOM은 변하고, 날짜는 흐르고, 재고는 사라집니다. 정적인 코드로 역동적인 서비스를 검증하려는 시도 자체가 어쩌면 모순일지도 모릅니다. 하지만 저는 그 모순 속에서 “예측할 수 없다면, 대응할 수 있게 만들어라”라는 해답을 찾았습니다. 무작정 기다리는 대신 조건을 확인하고, 없는 데이터를 억지로 테스트하기보다 과감히 건너뛰는 전략. 이것이 제가 찾은 테스트의 비결입니다.
처음 빈 스크립트를 마주했을 때는 막막함이 앞섰습니다. 초기에는 그저 제 눈에 보이는 대로만 코드를 작성했기 때문에 실제 컴퓨터가 마주하는 환경과의 차이로 많은 시행착오를 겪어야 했습니다. 하지만 이 과정 덕분에 저만의 스크립트 작성 루틴이 정립되었고, 이제는 테스트 자동화 단계가 안정기에 접어들었습니다.
물론, 현재 케이스들이 모두 Pass 된다고 해서 제 코드가 정답은 아닐 것입니다. 내일 당장 UI 디자인이 바뀌거나 로직이 변경되면 테스트는 다시 실패할 것이고, 저는 또다시 유지보수를 해야 합니다. 어쩌면 아직 발견하지 못한 간헐적 Fail 케이스가 숨어있을지도 모릅니다.
앞으로도 이런 불확실성을 성장의 발판으로 삼고 어떠한 환경 변화에도 흔들림 없는 견고한 자동화 환경을 목표로 끊임없이 고민해나가겠습니다. 긴 글 읽어주셔서 감사합니다.