Selenium 은 브라우저를 제어하여 자동으로 테스트를 진행하는 도구이다.
하지만 최근에 Playwright 를 사용하는 환경도 많은데 두 도구의 차이와 기본 사용방법을 알아보려 한다.
먼저 Selenium 은 W3C WebDriver 프로토콜을 사용하기 때문에 코드가 실행될 때마다
HTTP 요청을 보내고 응답을 받는 방식으로, 각 명령마다 네트워크 오버헤드가 발생한다.
반면 Playwright 는 WebSocket 을 활용하여 브라우저와 단일 연결을 맺고 양방향 통신을 수행한다.
따라서 브라우저 내부 이벤트에 직접 접근할 수 있어 실행 속도가 빠르고 응답 지연이 적다.
Playwright 의 딱 하나 단점이 있다면, 최신 모던 브라우저 엔진만 지원하기 때문에
레거시 브라우저에서 테스트를 해야만 한다면 Selenium 을 활용하여
브라우저 버전에 맞는 WebDriver 개별적으로 다운로드하고 버전을 유지하는 것이 좋다.
그럼 이러한 특징을 가지고 있는 playwright의 기본 사용법을 알아보려고 한다.
1. 브라우저 & 페이지 기본 설정
먼저 테스트 환경에 맞춰 브라우저를 실행하고 페이지 해상도를 설정하는 기본 구조이다.
여기에서 Chromium, WebKit, Firefox 엔진을 선택하여 실행할 수 있고, 브라우저 크기도 설정할 수 있다.
def demo_browser_setup() -> None:
"""브라우저 실행부터 종료까지의 기본 흐름."""
with sync_playwright() as pw:
# Chromium / Firefox / Webkit 선택 가능
browser = pw.chromium.launch(
headless=False, # True: 화면 없이 실행 (CI 환경), False: 화면 표시
slow_mo=50, # 각 동작 사이 50ms 딜레이 (디버깅용)
)
# 새 탭(페이지) 생성
page = browser.new_page(
viewport={"width": 1920, "height": 1080}, # 해상도 설정
)
# 이후 동작들 ...
page.close()
browser.close()
2. 페이지 접속 (Navigation)
두번째로 페이지 이동 및 로딩 대기방법이다. page.goto()를 사용하여 페이지로 이동할 수 있고,
타임아웃 시간을 명시적으로 작성하거나 네트워크 상태에 따라 로딩 대기 방법을 선택할 수 있다.
def demo_navigation(page: Page) -> None:
"""URL 이동 및 페이지 로딩 대기."""
# 기본 접속
page.goto("https://blaze.qa.oncloud.hanwhavision.cloud/")
# 타임아웃 지정 (ms 단위, 기본값 30_000ms = 30초)
page.goto("https://blaze.qa.oncloud.hanwhavision.cloud/", timeout=60_000)
# ── 페이지 로딩 완료 기준 설정 ──────────────────────────────────
# "load" : window.onload 이벤트 발생 시 (기본값)
# "domcontentloaded" : HTML 파싱 완료 시 (JS 실행 전)
# "networkidle" : 500ms 동안 네트워크 요청이 없을 때 (SPA에서 유용)
page.goto("https://blaze.qa.oncloud.hanwhavision.cloud/", wait_until="networkidle")
# 뒤로 / 앞으로 / 새로고침
page.go_back()
page.go_forward()
page.reload()
# 현재 URL 확인
print(page.url)
3. 요소 찾기 (Locator)
다음으로는 요소를 탐색하는 방법이다. Playwright 는 사용자 중심의 로케이터 사용을 권장한다.
CSS 선택자나 XPath 는 유지보수에 취약하기 때문에 최후의 수단으로 사용하는 것이 좋다.
다만 나는 로케이터 찾는 것을 아예 함수로 만들어서 찾기 때문에 CSS selector 을 자주 사용하는 편이다.
def demo_locators(page: Page) -> None:
"""User-facing 로케이터 우선 사용 (XPath / CSS는 최후 수단)."""
# ── 권장 로케이터 ────────────────────────────────────────────────
# role: 버튼, 링크, 체크박스 등 ARIA 역할로 찾기
btn: Locator = page.get_by_role("button", name="로그인")
link: Locator = page.get_by_role("link", name="대시보드")
checkbox: Locator = page.get_by_role("checkbox", name="기억하기")
# label: <label>과 연결된 입력 필드 찾기
email_input: Locator = page.get_by_label("이메일")
password_input: Locator = page.get_by_label("비밀번호")
# placeholder: 입력창의 placeholder 텍스트로 찾기
search_input: Locator = page.get_by_placeholder("검색어를 입력하세요")
# text: 화면에 보이는 텍스트로 찾기
title: Locator = page.get_by_text("환영합니다")
exact_title: Locator = page.get_by_text("환영합니다", exact=True) # 정확히 일치
# test-id: data-testid 속성으로 찾기 (개발자와 협의 필요)
menu: Locator = page.get_by_test_id("main-menu")
# ── 차선 로케이터 (꼭 필요한 경우만) ────────────────────────────
# CSS selector
header: Locator = page.locator("h1.page-title")
# XPath (가장 최후 수단)
item: Locator = page.locator("//ul[@id='list']/li[1]")
# ── 로케이터 조합 (필터링) ────────────────────────────────────────
# 특정 텍스트를 포함하는 버튼만 필터링
delete_btn: Locator = page.get_by_role("button").filter(has_text="삭제")
# 부모 요소 안에서 자식 요소 찾기
card = page.locator(".card").filter(has_text="카메라 01")
card_btn: Locator = card.get_by_role("button", name="편집")
# 여러 개일 때 인덱스로 지정 (0-based)
first_row: Locator = page.locator("table tr").nth(1) # 첫 번째 데이터 행
last_row: Locator = page.locator("table tr").last
4. 상호작용
다음으로 테스트를 하기 위해 반드시 클릭이나 텍스트 입력과 같은 상호작용이 필요하다.
아래 내용을 보면 클릭, 더블클릭, 우클릭 혹은 좌표클릭까지 지원하고 있고 드래그앤 드롭도 가능하다.
또한 텍스트 입력도 단축키나 특수키도 입력할 수 있고 위의 요소찾기와 대부분 짝으로 사용된다.
def demo_click(page: Page) -> None:
"""다양한 클릭 동작."""
btn = page.get_by_role("button", name="저장")
# 기본 클릭
btn.click()
# 더블 클릭
btn.dblclick()
# 우클릭 (컨텍스트 메뉴)
btn.click(button="right")
# 특정 좌표 클릭 (요소 내 상대 위치)
btn.click(position={"x": 10, "y": 5})
# 클릭 전 강제 표시 여부 무시 (숨겨진 요소 강제 클릭, 사용 자제)
btn.click(force=True)
# 키를 누른 상태로 클릭
btn.click(modifiers=["Shift"]) # Shift+클릭
btn.click(modifiers=["Meta"]) # Cmd+클릭 (macOS)
# 드래그앤 드롭
page.drag_and_drop("#source", "#target")
def demo_input(page: Page) -> None:
"""텍스트 필드 입력 방법."""
email = page.get_by_label("이메일")
password = page.get_by_label("비밀번호")
# fill: 기존 값을 지우고 새 값 입력 ← 일반적으로 이걸 사용
email.fill("test@example.com")
password.fill("password123")
# clear: 입력값 지우기
email.clear()
# type: 키보드 타이핑처럼 한 글자씩 입력 (특수 입력 로직 트리거 필요 시)
email.type("test@example.com", delay=50) # 50ms 간격으로 입력
# 키보드 단축키 / 특수 키 입력
email.press("Tab") # Tab 키
email.press("Enter") # Enter 키
email.press("Control+a") # Ctrl+A (전체선택)
email.press("Backspace") # Backspace
# 드롭다운(select) 선택
dropdown = page.get_by_label("역할")
dropdown.select_option("admin") # value로 선택
dropdown.select_option(label="관리자") # 표시 텍스트로 선택
dropdown.select_option(index=1) # 인덱스로 선택
# 체크박스 토글
checkbox = page.get_by_label("이용약관 동의")
checkbox.check() # 체크
checkbox.uncheck() # 체크 해제
5. 대기 (Wait)
Playwright는 기본적으로 요소가 상호작용 가능해질 때까지 자동 대기(Auto-waiting)를 수행한다.
하지만 특정 네트워크 유휴 상태를 기다리거나, 요소의 숨김 상태를 확인하는 등
복잡한 상태 동기화가 필요할 때 명시적 대기를 활용한다.
def demo_wait(page: Page) -> None:
"""요소 및 상태 대기 방법 총정리."""
# ── A. expect() — 권장 방식 ──────────────────────────────────────
# Playwright 내장 Auto-retry 포함. 조건이 충족될 때까지 자동 재시도.
# 기본 타임아웃: pytest-playwright 기본값 5_000ms
locator = page.get_by_role("button", name="저장")
expect(locator).to_be_visible() # 화면에 표시될 때까지
expect(locator).to_be_enabled() # 클릭 가능한 상태가 될 때까지
expect(locator).to_be_hidden() # 숨겨질 때까지
expect(locator).to_be_checked() # 체크박스가 체크될 때까지
expect(locator).to_have_text("완료") # 텍스트가 일치할 때까지
expect(locator).to_have_value("입력값") # input value가 일치할 때까지
expect(locator).to_have_count(5) # 요소 개수가 일치할 때까지
expect(page).to_have_url("https://example.com/dashboard") # URL 확인
expect(page).to_have_title("대시보드") # 페이지 타이틀 확인
# 타임아웃 직접 지정
expect(locator).to_be_visible(timeout=10_000) # 10초 대기
# ── B. wait_for() — Locator 레벨 대기 ──────────────────────────
# expect()가 어려운 상황에서 사용
locator.wait_for(state="visible") # 표시될 때까지
locator.wait_for(state="hidden") # 숨겨질 때까지
locator.wait_for(state="attached") # DOM에 추가될 때까지
locator.wait_for(state="detached") # DOM에서 제거될 때까지
# ── C. page.wait_for_*() — 페이지 레벨 대기 ─────────────────────
# 페이지 로딩 상태 대기
page.wait_for_load_state("load") # window.onload (기본값)
page.wait_for_load_state("domcontentloaded") # DOM 파싱 완료
page.wait_for_load_state("networkidle") # 네트워크 유휴 상태 (SPA에 적합)
# 특정 URL 로 이동할 때까지 대기
page.wait_for_url("**/dashboard") # glob 패턴 지원
page.wait_for_url("https://example.com/dashboard", timeout=10_000)
# 특정 요소가 DOM에 나타날 때까지 대기 (CSS selector)
page.wait_for_selector(".loading-spinner", state="hidden") # 로딩 스피너 사라질 때까지
page.wait_for_selector("#result-table", state="visible") # 결과 테이블 나타날 때까지
# 특정 시간 강제 대기 (가능하면 사용 자제 — 테스트 불안정 원인)
page.wait_for_timeout(2_000) # 2초 고정 대기
6. 팝업 / 다이얼로그 / 새 탭 처리
다음으로 새 탭으로 열리는 페이지를 제어하거나 팝업으로 노출된 시스템 다이얼로그를 처리해야 하는 경우가 있다
이럴 때 아래와 같이 팝업이나 다이얼로그를 트리거하는 클릭 동작을 수행하기 이전에,
이를 가로채서 처리할 이벤트 리스너를 미리 등록(page.on)하는 방식으로 제어한다.
def demo_dialog_and_popup(page: Page) -> None:
"""브라우저 기본 다이얼로그 및 새 탭 처리."""
# ── alert / confirm / prompt 처리 ────────────────────────────────
# 이벤트 리스너를 클릭 '이전'에 등록해야 함
page.on("dialog", lambda dialog: dialog.accept()) # 확인 클릭
# page.on("dialog", lambda dialog: dialog.dismiss()) # 취소 클릭
# page.on("dialog", lambda dialog: dialog.accept("입력값"))# prompt 입력 후 확인
page.get_by_role("button", name="삭제").click()
# ── 새 탭(popup) 처리 ────────────────────────────────────────────
with page.expect_popup() as popup_info:
page.get_by_role("link", name="새 창으로 열기").click()
new_tab = popup_info.value
new_tab.wait_for_load_state("networkidle")
print(new_tab.url)
new_tab.close()
7. 검증 (Assertion)
다음으로 검증에 대해 이야기할 수 있는데, 테스트 과정 중 UI가 의도한 상태로 변경되었는지 확인하고,
해당 시점의 상태를 확실히 보장하여 테스트의 신뢰성을 높이기 위해 사용한다.
def demo_assertions(page: Page) -> None:
"""expect() 기반 검증 예시."""
# 텍스트 포함 여부
expect(page.get_by_role("heading")).to_contain_text("대시보드")
# CSS 클래스 보유 여부
expect(page.locator(".status-badge")).to_have_class("active")
# 속성 값 확인
expect(page.get_by_role("img", name="로고")).to_have_attribute("alt", "Company Logo")
# 요소가 존재하지 않음을 검증
expect(page.get_by_text("오류 메시지")).not_to_be_visible()
8. 스크린샷 & 디버깅
마지막으로 스크린샷 캡처 기능이다. 만약 headless 상태로 동작하다가 에러가 발생했지만
어떤 상태인지 화면을 볼 수 없거나 검증이 완료된 이후 매뉴얼로
다시한 번 확인을 위한 스크린샷이 필요할 때가 있는데 그럴때 유용하게 사용될 수 있다.
def demo_debug(page: Page) -> None:
"""디버깅 도구."""
# 전체 페이지 스크린샷
page.screenshot(path="screenshot.png", full_page=True)
# 특정 요소만 스크린샷
page.locator(".main-content").screenshot(path="element.png")
# 콘솔 로그 출력 (이벤트 등록은 page 생성 직후에 할 것)
page.on("console", lambda msg: print(f"[CONSOLE] {msg.type}: {msg.text}"))
# Playwright Inspector 실행 (로케이터 탐색용, headless=False 환경에서만 동작)
# page.pause() # ← 이 줄 주석 해제하면 Inspector 창이 열림
지금까지 Selenium과 비교한 Playwright의 핵심 특징과 기본 사용법을 정리해보았다.
Playwright는 내장된 자동 대기 기능과 빠른 양방향 통신 구조 덕분에,
기존 웹 자동화 스크립트 작성 시 겪어야 했던 '대기 시간 처리'의 피로도를 크게 줄여준다.
IE와 같은 레거시 브라우저 테스트가 강제되는 환경이 아니라면,
새로운 자동화 환경 구축 시 Playwright는 가장 효율적인 선택지가 될 것이라고 생각한다..
'python > python_selenium' 카테고리의 다른 글
| python + selenium, bs4 환경 세팅하기 (1) | 2023.11.22 |
|---|---|
| selenium_quiz2 (0) | 2023.05.01 |
| selenium_quiz1 (0) | 2023.04.30 |
| Headless 크롬 (0) | 2023.04.29 |
| Selenium 활용2 (0) | 2023.04.29 |