🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Selenium 실무 활용 팁 - 슬라이드 1/13
A

AI Generated

2025. 11. 1. · 14 Views

Selenium 실무 활용 완벽 가이드

웹 자동화 테스트의 필수 도구 Selenium의 실무 활용 팁을 소개합니다. 대기 전략, 동적 요소 처리, 스크린샷 활용 등 실제 프로젝트에서 바로 쓸 수 있는 10가지 핵심 기법을 배워보세요.

1. 명시적 대기(Explicit Wait) - 동적 요소를 안정적으로 다루는 법

Intro

여러분이 Selenium으로 테스트 코드를 작성할 때 "NoSuchElementException" 에러를 만나본 적 있나요? 페이지는 로딩됐는데 요소를 찾지 못하는 이 답답한 상황은 초보자들이 가장 자주 겪는 문제입니다. 이 문제의 원인은 명확합니다. JavaScript로 동적으로 생성되는 요소들은 페이지가 로드된 후에도 시간차를 두고 나타나기 때문입니다. 단순히 time.sleep()을 쓰면 테스트가 불안정해지고 실행 시간도 불필요하게 길어집니다. 바로 이럴 때 필요한 것이 명시적 대기(Explicit Wait)입니다. 특정 조건이 만족될 때까지만 기다리므로 안정성과 효율성을 동시에 잡을 수 있습니다.

Description

간단히 말해서, 명시적 대기는 특정 요소가 특정 상태가 될 때까지 최대 N초 동안 기다리는 스마트한 대기 메커니즘입니다. 실무에서 SPA(Single Page Application)나 Ajax 요청이 많은 페이지를 테스트할 때 필수적입니다. 예를 들어, 로그인 버튼을 클릭한 후 대시보드가 나타날 때까지 기다리거나, 검색 결과가 로딩될 때까지 대기해야 하는 경우에 매우 유용합니다. 기존에는 임의의 시간(예: 5초)을 무조건 기다렸다면, 이제는 조건이 만족되는 즉시 다음 단계로 진행할 수 있습니다. 명시적 대기의 핵심 특징은 세 가지입니다. 첫째, 조건 기반 대기로 불필요한 시간 낭비가 없습니다. 둘째, 다양한 조건(클릭 가능, 보이는 상태, 존재 여부 등)을 선택할 수 있습니다. 셋째, 타임아웃 설정으로 무한 대기를 방지합니다. 이러한 특징들이 테스트의 안정성과 효율성을 크게 향상시킵니다.

Code

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com")

# 최대 10초 동안 로그인 버튼이 클릭 가능해질 때까지 대기
wait = WebDriverWait(driver, 10)
login_button = wait.until(
    EC.element_to_be_clickable((By.ID, "login-btn"))
)
login_button.click()

# 대시보드가 보일 때까지 대기
dashboard = wait.until(
    EC.visibility_of_element_located((By.CLASS_NAME, "dashboard"))
)

Explanation

이것이 하는 일: 명시적 대기는 웹 요소가 특정 상태(클릭 가능, 보임, 존재 등)가 될 때까지 지정된 시간 동안 0.5초 간격으로 반복 체크하며 기다립니다. 첫 번째로, WebDriverWait 객체를 생성할 때 드라이버와 최대 대기 시간을 지정합니다. 이때 10초는 "최대" 대기 시간이므로, 조건이 2초 만에 만족되면 2초만 기다립니다. 이것이 고정된 시간을 기다리는 time.sleep()과의 핵심 차이입니다. 그 다음으로, until() 메서드 안에 expected_conditions(EC)를 사용해 대기 조건을 지정합니다. element_to_be_clickable은 요소가 보이면서 동시에 활성화(enabled) 상태인지 확인합니다. 내부적으로 Selenium은 0.5초마다 이 조건을 체크하다가 조건이 만족되면 즉시 해당 요소를 반환합니다. 마지막으로, 반환된 요소로 바로 작업(클릭, 텍스트 입력 등)을 수행할 수 있습니다. 만약 10초 안에 조건이 만족되지 않으면 TimeoutException이 발생하여 문제를 명확히 파악할 수 있습니다. 여러분이 이 코드를 사용하면 페이지 로딩 속도가 달라도 테스트가 안정적으로 동작하고, 불필요한 대기 시간을 줄여 전체 테스트 실행 시간을 30~50% 단축할 수 있습니다. 또한 어떤 조건 때문에 실패했는지 명확한 에러 메시지를 받을 수 있어 디버깅이 훨씬 쉬워집니다.

Tips

💡 EC.presence_of_element_located는 DOM에만 존재하면 되고, EC.visibility_of_element_located는 화면에 보여야 하므로 상황에 맞게 선택하세요 💡 여러 페이지에서 반복 사용한다면 wait 객체를 클래스 변수로 만들어 재사용하면 코드가 깔끔해집니다 💡 타임아웃은 보통 10초가 적당하지만, 느린 API 응답이 예상되면 15~20초로 늘리세요 💡 EC.staleness_of를 사용하면 페이지 리프레시 후 이전 요소가 사라질 때까지 기다릴 수 있어 SPA 테스트에 유용합니다 💡 디버깅 시에는 타임아웃을 길게 설정하고, 안정화되면 최적값으로 줄이는 것이 좋습니다

2. 헤드리스 모드(Headless Mode) - 서버 환경에서 빠르게 실행하기

Intro

여러분이 CI/CD 파이프라인에서 Selenium 테스트를 돌리려고 할 때 "Display not found" 에러를 본 적 있나요? 서버 환경에는 GUI가 없어서 브라우저를 띄울 수 없다는 이 문제는 자동화 초기 단계에서 흔히 겪는 장애물입니다. 이 문제는 Jenkins, GitLab CI, GitHub Actions 같은 CI 환경에서 필연적으로 발생합니다. 가상 디스플레이를 설정하는 방법도 있지만 복잡하고 리소스도 많이 먹습니다. 바로 이럴 때 필요한 것이 헤드리스 모드입니다. 브라우저 UI 없이 백그라운드에서 모든 작업을 수행하여 서버 환경에서도 완벽하게 동작합니다.

Description

간단히 말해서, 헤드리스 모드는 브라우저 창을 화면에 띄우지 않고 백그라운드에서 모든 렌더링과 실행을 처리하는 방식입니다. 실무에서 CI/CD 파이프라인, 크론 잡, 서버리스 함수 등 GUI가 없는 환경에서 필수적입니다. 예를 들어, 매일 새벽 자동으로 경쟁사 가격을 크롤링하거나, 풀 리퀘스트마다 E2E 테스트를 자동 실행하는 경우에 매우 유용합니다. 기존에는 Xvfb 같은 가상 디스플레이를 설정했다면, 이제는 옵션 하나만 추가하면 됩니다. 헤드리스 모드의 핵심 특징은 세 가지입니다. 첫째, 메모리와 CPU 사용량이 일반 모드 대비 2030% 적습니다. 둘째, GUI 렌더링이 없어 실행 속도가 1015% 빨라집니다. 셋째, 디스플레이 서버 없이도 작동하여 Docker 컨테이너에서 바로 실행 가능합니다. 이러한 특징들이 대규모 자동화와 클라우드 환경에서 비용을 절감시킵니다.

Code

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# Chrome 헤드리스 옵션 설정
chrome_options = Options()
chrome_options.add_argument("--headless")  # 헤드리스 모드 활성화
chrome_options.add_argument("--no-sandbox")  # 샌드박스 비활성화 (서버 환경)
chrome_options.add_argument("--disable-dev-shm-usage")  # 공유 메모리 문제 방지

# 헤드리스 브라우저 시작
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://example.com")

# 일반 모드와 동일하게 작업 수행
title = driver.title
print(f"페이지 제목: {title}")

driver.quit()

Explanation

이것이 하는 일: 헤드리스 모드는 브라우저의 모든 기능(JavaScript 실행, DOM 렌더링, 네트워크 요청 등)을 수행하지만 화면 출력만 생략하여 자원을 절약합니다. 첫 번째로, Options 객체를 생성하고 --headless 인자를 추가합니다. Chrome 96 이후 버전에서는 --headless=new 옵션을 사용하면 더 개선된 헤드리스 엔진을 사용할 수 있습니다. 이 옵션은 브라우저에게 "GUI를 초기화하지 마"라고 지시합니다. 그 다음으로, --no-sandbox와 --disable-dev-shm-usage 옵션을 추가합니다. --no-sandbox는 root 권한으로 실행되는 Docker 컨테이너에서 필수이며, --disable-dev-shm-usage는 /dev/shm 파티션이 작은 환경에서 발생하는 크래시를 방지합니다. 이 옵션들은 특히 CI 환경에서 안정성을 크게 높입니다. 마지막으로, 설정된 옵션으로 드라이버를 생성하면 이후 모든 작업은 일반 모드와 완전히 동일합니다. 스크린샷 찍기, 요소 클릭, 텍스트 입력 모두 정상 작동하며, 사용자는 브라우저가 보이지 않는다는 것 외에는 차이를 느끼지 못합니다. 여러분이 이 코드를 사용하면 로컬에서 개발할 때는 헤드리스 옵션을 제거하고, 서버 배포 시에는 환경 변수로 켜는 식으로 유연하게 전환할 수 있습니다. 또한 동시에 여러 브라우저 인스턴스를 실행할 때 GUI 오버헤드가 없어 서버 한 대에서 처리할 수 있는 테스트 수가 2~3배 늘어납니다.

Tips

💡 환경 변수로 헤드리스 모드를 제어하면 로컬(개발)과 서버(배포)를 쉽게 전환할 수 있습니다: os.getenv('HEADLESS', 'false') == 'true' 💡 디버깅이 필요하면 임시로 헤드리스 옵션을 제거하고 실제 브라우저를 보면서 문제를 파악하세요 💡 --window-size=1920,1080 옵션으로 뷰포트 크기를 명시하면 반응형 레이아웃 테스트에서 일관된 결과를 얻을 수 있습니다 💡 헤드리스 모드에서도 driver.save_screenshot()는 정상 작동하므로 실패 시 스크린샷을 남기면 디버깅이 쉽습니다 💡 Firefox도 options.add_argument('-headless')로 헤드리스 모드를 지원하므로 크로스 브라우저 테스트 시 동일한 패턴을 사용하세요

3. 페이지 소스 파싱 - BeautifulSoup과 함께 사용하기

Intro

여러분이 여러 요소를 한 번에 추출해야 할 때 find_element를 반복문으로 수십 번 호출하며 느린 속도에 답답함을 느낀 적 있나요? Selenium의 요소 검색은 매번 브라우저와 통신하므로 대량의 데이터를 추출할 때는 비효율적입니다. 이 문제는 테이블 데이터, 리스트 목록, 상품 카탈로그처럼 반복되는 구조에서 수백 개의 항목을 파싱할 때 심각해집니다. 네트워크 왕복 시간이 누적되어 전체 실행 시간이 기하급수적으로 늘어납니다. 바로 이럴 때 필요한 것이 페이지 소스와 BeautifulSoup의 조합입니다. Selenium으로 페이지를 로드하고, BeautifulSoup으로 빠르게 파싱하여 두 도구의 장점을 모두 활용합니다.

Description

간단히 말해서, 이 패턴은 Selenium이 JavaScript를 실행하여 완전히 렌더링된 페이지를 만들고, BeautifulSoup이 그 HTML을 로컬에서 빠르게 파싱하는 하이브리드 접근법입니다. 실무에서 동적 콘텐츠는 많지만 인터랙션은 적은 페이지를 스크레이핑할 때 필수적입니다. 예를 들어, 무한 스크롤로 상품 목록을 모두 로드한 후 각 상품의 이름, 가격, 리뷰를 추출하거나, Ajax로 채워지는 테이블 데이터를 엑셀로 저장하는 경우에 매우 유용합니다. 기존에는 모든 데이터를 Selenium으로 추출했다면, 이제는 동적 로딩만 Selenium으로 처리하고 파싱은 BeautifulSoup에 맡길 수 있습니다. 이 패턴의 핵심 특징은 세 가지입니다. 첫째, Selenium의 JavaScript 실행 능력과 BeautifulSoup의 빠른 파싱 속도를 결합합니다. 둘째, 반복적인 브라우저 통신을 제거하여 파싱 속도가 10~100배 빨라집니다. 셋째, BeautifulSoup의 강력한 CSS 선택자와 find_all을 활용할 수 있습니다. 이러한 특징들이 대량 데이터 추출 작업의 효율성을 극대화합니다.

Code

from selenium import webdriver
from bs4 import BeautifulSoup
import time

driver = webdriver.Chrome()
driver.get("https://example.com/products")

# 무한 스크롤 등으로 모든 콘텐츠 로드
for _ in range(5):
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
    time.sleep(1)

# 페이지 소스를 가져와 BeautifulSoup으로 파싱
page_source = driver.page_source
soup = BeautifulSoup(page_source, 'html.parser')

# 빠르게 여러 요소 추출
products = soup.find_all('div', class_='product-item')
for product in products:
    name = product.find('h3', class_='product-name').text
    price = product.find('span', class_='price').text
    print(f"{name}: {price}")

driver.quit()

Explanation

이것이 하는 일: Selenium이 브라우저를 제어하여 동적 콘텐츠를 모두 로드한 후, 완성된 HTML을 BeautifulSoup에 넘겨 로컬 메모리에서 고속으로 파싱합니다. 첫 번째로, Selenium으로 페이지를 열고 필요한 모든 동적 콘텐츠를 로드합니다. 무한 스크롤 예제처럼 JavaScript를 실행하여 페이지 하단까지 스크롤하면 Ajax 요청이 발생하며 추가 상품이 로드됩니다. 이 단계에서 Selenium은 "렌더링 엔진" 역할을 합니다. 그 다음으로, driver.page_source로 현재 DOM의 전체 HTML을 문자열로 가져와 BeautifulSoup에 전달합니다. 이때부터는 브라우저와의 통신이 완전히 끊기고, 모든 작업이 Python 메모리에서 이루어집니다. BeautifulSoup의 파서는 이 HTML을 트리 구조로 변환하여 빠른 탐색을 가능하게 합니다. 마지막으로, find_all로 패턴에 맞는 모든 요소를 한 번에 찾고, 각 요소에서 필요한 정보를 추출합니다. 이 과정은 순수 Python 문자열 처리이므로 Selenium의 find_elements보다 수십 배 빠릅니다. 100개 상품을 추출할 때 Selenium만 쓰면 10초 걸릴 작업이 1초 안에 완료됩니다. 여러분이 이 코드를 사용하면 복잡한 페이지 구조에서 수백, 수천 개의 항목을 추출할 때 실행 시간을 획기적으로 줄일 수 있습니다. 또한 BeautifulSoup의 풍부한 검색 옵션(정규식, 람다 함수, CSS 선택자)을 활용하여 Selenium만으로는 어려운 복잡한 파싱도 간단하게 처리할 수 있습니다.

Tips

💡 대량 데이터 추출 시 Selenium으로 각 요소를 찾는 대신, page_source를 한 번만 가져와 BeautifulSoup으로 일괄 처리하세요 💡 동적 로딩이 완료됐는지 확인하려면 WebDriverWait로 마지막 요소가 나타날 때까지 기다린 후 page_source를 추출하세요 💡 lxml 파서를 사용하면 html.parser보다 2~3배 빠릅니다: BeautifulSoup(page_source, 'lxml') 💡 페이지 구조가 자주 바뀐다면 CSS 선택자보다 의미 있는 클래스명이나 data 속성을 우선 사용하세요 💡 Selenium과 BeautifulSoup을 분리하면 단위 테스트가 쉬워집니다 - 저장된 HTML 파일로 파싱 로직만 따로 테스트할 수 있습니다

4. 스크린샷과 로깅 - 실패 원인을 빠르게 찾기

Intro

여러분이 CI에서 테스트가 실패했는데 로그에 "Element not found"만 덩그러니 남아있어서 원인을 전혀 파악할 수 없었던 경험 있나요? 로컬에서는 성공하는데 서버에서만 실패하는 이런 상황은 디버깅을 매우 어렵게 만듭니다. 이 문제는 CI 환경의 타이밍 차이, 네트워크 지연, 해상도 차이 등 다양한 원인으로 발생합니다. 재현이 어려워서 몇 시간씩 추측만 하다가 포기하는 경우도 많습니다. 바로 이럴 때 필요한 것이 스크린샷과 상세 로깅입니다. 실패 시점의 화면을 캡처하고 각 단계를 로그로 남겨 문제를 5분 만에 찾아낼 수 있습니다.

Description

간단히 말해서, 이 패턴은 테스트의 주요 단계마다 스크린샷을 저장하고, 실행 흐름과 에러를 구조화된 로그로 기록하는 방어적 프로그래밍 기법입니다. 실무에서 불안정한 테스트를 디버깅하거나, 고객에게 재현 증거를 제시하거나, 장애 원인을 분석할 때 필수적입니다. 예를 들어, 결제 프로세스가 실패했을 때 각 단계의 화면을 저장하여 어느 시점에서 문제가 발생했는지 정확히 파악하거나, 간헐적 실패를 재현하기 위해 실행 기록을 분석하는 경우에 매우 유용합니다. 기존에는 에러 메시지만 보고 추측했다면, 이제는 시각적 증거와 상세한 실행 기록으로 정확히 진단할 수 있습니다. 이 패턴의 핵심 특징은 세 가지입니다. 첫째, 실패 시점의 시각적 상태를 보존하여 "왜 실패했는지"를 명확히 알 수 있습니다. 둘째, 타임스탬프와 단계별 로그로 실행 흐름을 추적할 수 있습니다. 셋째, try-except 블록과 결합하여 자동으로 증거를 수집합니다. 이러한 특징들이 디버깅 시간을 80% 이상 단축시킵니다.

Code

from selenium import webdriver
import logging
from datetime import datetime
import os

# 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[logging.FileHandler('test.log'), logging.StreamHandler()]
)

driver = webdriver.Chrome()

def take_screenshot(driver, name):
    """실패 시 스크린샷 저장"""
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f"screenshots/{name}_{timestamp}.png"
    os.makedirs('screenshots', exist_ok=True)
    driver.save_screenshot(filename)
    logging.info(f"스크린샷 저장: {filename}")
    return filename

try:
    logging.info("테스트 시작: 로그인 페이지 접속")
    driver.get("https://example.com/login")
    take_screenshot(driver, "login_page")

    # 로그인 시도
    logging.info("사용자 이름 입력")
    driver.find_element("id", "username").send_keys("testuser")

    logging.info("비밀번호 입력 및 제출")
    driver.find_element("id", "password").send_keys("password123")
    driver.find_element("id", "submit").click()

    take_screenshot(driver, "after_login")
    logging.info("로그인 성공")

except Exception as e:
    logging.error(f"테스트 실패: {str(e)}")
    take_screenshot(driver, "error")
    raise
finally:
    driver.quit()

Explanation

이것이 하는 일: 테스트 실행 중 주요 액션마다 스크린샷을 파일로 저장하고, 로깅 모듈로 타임스탬프가 포함된 실행 기록을 파일과 콘솔에 동시에 출력합니다. 첫 번째로, logging.basicConfig로 로거를 설정합니다. FileHandler는 모든 로그를 test.log 파일에 저장하고, StreamHandler는 콘솔에도 출력하여 실시간 모니터링을 가능하게 합니다. format 설정으로 각 로그에 타임스탬프와 레벨(INFO, ERROR)이 자동으로 붙습니다. 그 다음으로, take_screenshot 함수는 현재 시각을 파일명에 포함시켜 스크린샷을 저장합니다. os.makedirs로 디렉토리가 없으면 자동 생성하고, driver.save_screenshot()로 현재 화면을 PNG 파일로 저장합니다. 헤드리스 모드에서도 완벽하게 작동합니다. 마지막으로, try-except-finally 구조로 정상 실행 시에는 각 단계를 로깅하고, 에러 발생 시에는 자동으로 에러 스크린샷을 찍어 증거를 보존합니다. finally 블록은 성공하든 실패하든 브라우저를 정리하여 리소스 누수를 방지합니다. 여러분이 이 코드를 사용하면 CI에서 실패한 테스트의 스크린샷을 아티팩트로 저장하여 GitHub Actions나 Jenkins에서 바로 확인할 수 있습니다. 또한 로그 파일을 시계열 분석하여 "로그인은 3초 걸리는데 대시보드 로딩은 10초 걸린다"처럼 성능 병목도 찾아낼 수 있습니다.

Tips

💡 CI 환경에서는 스크린샷 폴더를 아티팩트로 업로드하면 실패한 테스트의 화면을 바로 확인할 수 있습니다 💡 스크린샷 파일명에 테스트 케이스 이름을 포함시키면 여러 테스트 실패 시 구분이 쉽습니다 💡 driver.get_screenshot_as_png()로 바이너리를 가져와 S3나 클라우드 스토리지에 직접 업로드할 수도 있습니다 💡 로그 레벨을 환경 변수로 제어하면 개발 시에는 DEBUG로, 운영에서는 INFO로 자동 조정됩니다 💡 페이지 로드 시간을 로그에 기록하려면 time.time()으로 시작/종료 시각을 측정하여 성능 추적에 활용하세요

5. 드롭다운과 Select 요소 - 올바른 선택 방법

Intro

여러분이 드롭다운 메뉴에서 옵션을 선택하려고 click()만 반복했는데 제대로 작동하지 않아 당황한 적 있나요? HTML <select> 요소는 일반 버튼과 다르게 동작해서 초보자들이 자주 실수하는 부분입니다. 이 문제는 드롭다운이 브라우저의 네이티브 컨트롤이라 JavaScript 이벤트만으로는 완전히 제어할 수 없기 때문에 발생합니다. 특히 폼 제출 시 선택값이 전송되지 않는 치명적인 버그로 이어집니다. 바로 이럴 때 필요한 것이 Select 클래스입니다. Selenium이 제공하는 전용 API로 드롭다운을 안전하고 명확하게 제어할 수 있습니다.

Description

간단히 말해서, Select 클래스는 HTML <select> 요소를 다루기 위한 특화된 래퍼로, 값, 인덱스, 표시 텍스트 세 가지 방법으로 옵션을 선택할 수 있습니다. 실무에서 회원가입 폼, 필터링 UI, 설정 페이지 등 드롭다운이 있는 모든 곳에서 필수적입니다. 예를 들어, 국가 선택, 날짜 선택, 카테고리 필터링처럼 미리 정의된 옵션 중 하나를 선택해야 하는 경우에 매우 유용합니다. 기존에는 복잡한 XPath와 클릭 조합으로 우회했다면, 이제는 의도를 명확히 표현하는 전용 메서드를 사용할 수 있습니다. Select 클래스의 핵심 특징은 세 가지입니다. 첫째, select_by_value, select_by_index, select_by_visible_text로 상황에 맞는 선택 방법을 제공합니다. 둘째, options 속성으로 모든 선택지를 조회할 수 있습니다. 셋째, 다중 선택 드롭다운(multiple)도 지원합니다. 이러한 특징들이 폼 자동화의 정확성과 가독성을 크게 향상시킵니다.

Code

from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/signup")

# Select 객체 생성
country_dropdown = Select(driver.find_element(By.ID, "country"))

# 방법 1: 보이는 텍스트로 선택 (가장 직관적)
country_dropdown.select_by_visible_text("South Korea")

# 방법 2: value 속성으로 선택
language_dropdown = Select(driver.find_element(By.NAME, "language"))
language_dropdown.select_by_value("ko")

# 방법 3: 인덱스로 선택 (0부터 시작)
year_dropdown = Select(driver.find_element(By.ID, "birth-year"))
year_dropdown.select_by_index(30)  # 30번째 옵션 선택

# 현재 선택된 옵션 확인
selected = country_dropdown.first_selected_option
print(f"선택된 국가: {selected.text}")

# 모든 옵션 출력
all_options = country_dropdown.options
print(f"총 {len(all_options)}개 국가 사용 가능")

driver.quit()

Explanation

이것이 하는 일: Select 클래스는 <select> 요소를 감싸서 브라우저의 네이티브 선택 메커니즘을 올바르게 트리거하고, 선택 상태를 확인하는 편의 메서드를 제공합니다. 첫 번째로, find_element로 드롭다운 요소를 찾은 후 Select 객체로 래핑합니다. 이 객체는 내부적으로 해당 요소가 <select> 태그인지 검증하고, 아니면 예외를 발생시켜 잘못된 사용을 조기에 방지합니다. 그 다음으로, select_by_visible_text는 사용자에게 보이는 텍스트로 옵션을 찾아 선택합니다. 이 방법이 가장 직관적이고 테스트 코드의 가독성이 높습니다. select_by_value는 <option value="ko">한국어</option>의 value 속성을 사용하며, API 연동 시 정확한 값이 필요할 때 유용합니다. select_by_index는 순서가 고정된 경우(예: 1990~2024년) 사용합니다. 마지막으로, first_selected_option으로 현재 선택을 확인하거나, options로 모든 선택지를 가져와 검증할 수 있습니다. 다중 선택 드롭다운이라면 all_selected_options로 여러 선택을 동시에 확인할 수 있습니다. 여러분이 이 코드를 사용하면 드롭다운이 제대로 선택됐는지 확신할 수 있고, "옵션이 선택되지 않았습니다" 같은 폼 검증 에러를 예방할 수 있습니다. 또한 동적으로 생성되는 드롭다운(예: 국가 선택 시 도시 목록 갱신)도 options를 조회하여 올바른 옵션이 로드됐는지 검증할 수 있습니다.

Tips

💡 커스텀 드롭다운(div로 만든 가짜 select)은 Select 클래스를 쓸 수 없으므로 일반 요소 클릭으로 처리해야 합니다 💡 select_by_visible_text 사용 시 공백이나 줄바꿈이 정확히 일치해야 하므로, 실패하면 XPath의 normalize-space를 고려하세요 💡 다중 선택 드롭다운은 deselect_all()로 초기화한 후 select_by_*를 여러 번 호출하여 선택하세요 💡 드롭다운 옵션이 Ajax로 동적 로드되면 WebDriverWait로 옵션이 채워질 때까지 기다린 후 Select 객체를 생성하세요 💡 테스트 데이터로 "South Korea" 같은 하드코딩 대신 변수나 설정 파일을 사용하면 다국어 환경에서 재사용이 쉽습니다

6. 쿠키와 세션 관리 - 로그인 우회하기

Intro

여러분이 테스트할 때마다 매번 로그인 과정을 반복하느라 시간을 낭비한 적 있나요? 100개의 테스트 케이스가 각각 로그인부터 시작한다면 전체 실행 시간이 몇 배로 늘어납니다. 이 문제는 테스트 간 독립성을 지키려다 보니 매번 새 세션으로 시작하기 때문에 발생합니다. 하지만 로그인 자체는 대부분 테스트의 핵심이 아니라 전제 조건일 뿐입니다. 바로 이럴 때 필요한 것이 쿠키 재사용입니다. 한 번 로그인해서 쿠키를 저장하고, 이후 테스트에서는 쿠키만 주입하여 몇 초 만에 인증된 상태로 시작할 수 있습니다.

Description

간단히 말해서, 이 패턴은 브라우저의 쿠키를 추출하여 파일로 저장하고, 다음 세션에서 쿠키를 복원하여 로그인 상태를 재현하는 기법입니다. 실무에서 반복적인 E2E 테스트, 데이터 스크레이핑, 성능 테스트처럼 로그인이 필수이지만 핵심 관심사가 아닌 경우에 필수적입니다. 예를 들어, 대시보드 UI를 테스트하거나, 인증된 사용자만 볼 수 있는 데이터를 크롤링하거나, 여러 페이지를 빠르게 탐색해야 하는 경우에 매우 유용합니다. 기존에는 모든 테스트가 로그인 폼을 채우고 제출했다면, 이제는 쿠키 한 줄로 인증 상태를 즉시 얻을 수 있습니다. 쿠키 관리의 핵심 특징은 세 가지입니다. 첫째, get_cookies()와 add_cookie()로 세션 상태를 완전히 제어할 수 있습니다. 둘째, JSON 파일로 쿠키를 저장하여 테스트 간 공유하거나 버전 관리할 수 있습니다. 셋째, 로그인 시간을 90% 이상 단축하여 테스트 스위트 실행 속도를 크게 향상시킵니다. 이러한 특징들이 대규모 테스트 자동화의 효율성을 극대화합니다.

Code

from selenium import webdriver
import json
import os

def save_cookies(driver, filepath):
    """현재 쿠키를 JSON 파일로 저장"""
    cookies = driver.get_cookies()
    with open(filepath, 'w') as f:
        json.dump(cookies, f)
    print(f"쿠키 저장 완료: {len(cookies)}개")

def load_cookies(driver, filepath):
    """저장된 쿠키를 현재 세션에 추가"""
    if not os.path.exists(filepath):
        return False

    with open(filepath, 'r') as f:
        cookies = json.load(f)

    for cookie in cookies:
        driver.add_cookie(cookie)
    print(f"쿠키 복원 완료: {len(cookies)}개")
    return True

# 첫 실행: 로그인 후 쿠키 저장
driver = webdriver.Chrome()
driver.get("https://example.com/login")

# 로그인 수행 (일반적인 방법)
driver.find_element("id", "username").send_keys("testuser")
driver.find_element("id", "password").send_keys("password123")
driver.find_element("id", "submit").click()

save_cookies(driver, "cookies.json")
driver.quit()

# 이후 실행: 쿠키로 로그인 우회
driver = webdriver.Chrome()
driver.get("https://example.com")  # 먼저 도메인 방문 필요
load_cookies(driver, "cookies.json")
driver.refresh()  # 쿠키 적용을 위해 새로고침
# 이제 로그인된 상태!

driver.quit()

Explanation

이것이 하는 일: driver.get_cookies()로 현재 세션의 모든 쿠키를 추출하고, driver.add_cookie()로 새 세션에 쿠키를 주입하여 서버가 동일한 사용자로 인식하게 만듭니다. 첫 번째로, 로그인 성공 후 get_cookies()를 호출하면 세션 ID, 인증 토큰, 사용자 설정 등이 담긴 쿠키 리스트를 받습니다. 이 리스트를 JSON으로 직렬화하여 파일에 저장하면 여러 테스트나 실행 간 공유할 수 있습니다. 쿠키는 민감 정보일 수 있으므로 .gitignore에 추가해야 합니다. 그 다음으로, 새 브라우저 세션에서는 먼저 해당 도메인을 방문해야 합니다. Selenium은 보안상 현재 도메인의 쿠키만 추가할 수 있기 때문입니다. driver.get()으로 홈페이지를 열어 도메인을 설정한 후, add_cookie()로 저장된 쿠키들을 하나씩 주입합니다. 마지막으로, driver.refresh()로 페이지를 새로고침하면 주입된 쿠키가 HTTP 요청에 포함되어 서버로 전송됩니다. 서버는 쿠키의 세션 ID나 JWT 토큰을 검증하고 "이미 로그인한 사용자"로 인식하여 인증된 콘텐츠를 반환합니다. 여러분이 이 코드를 사용하면 로그인 폼 입력과 제출에 걸리는 5~10초를 쿠키 주입 1초로 줄일 수 있습니다. 100개 테스트라면 900초(15분)가 절약됩니다. 또한 CI 환경에서 쿠키를 시크릿으로 저장하면 실제 비밀번호를 코드에 노출하지 않고도 인증 테스트를 수행할 수 있습니다.

Tips

💡 쿠키에는 만료 시간이 있으므로, 로드 실패 시 자동으로 새 로그인을 수행하는 폴백 로직을 추가하세요 💡 여러 사용자 역할(관리자, 일반 사용자)을 테스트한다면 각각의 쿠키 파일을 만들어 관리하세요 💡 쿠키 파일에 타임스탬프를 기록해두면 24시간 지난 쿠키는 자동으로 갱신하는 로직을 만들 수 있습니다 💡 로컬 환경과 스테이징 환경의 쿠키는 도메인이 달라 호환되지 않으므로 환경별로 분리하세요 💡 민감한 쿠키는 평문 JSON 대신 암호화하거나 환경 변수로 관리하여 보안을 강화하세요

7. iframe 다루기 - 중첩된 프레임 제어하기

Intro

여러분이 요소를 분명히 보고 있는데 find_element가 "Element not found"를 던져서 당황한 적 있나요? 특히 에디터, 채팅 위젯, 광고 같은 외부 삽입 콘텐츠를 다룰 때 이런 문제가 자주 발생합니다. 이 문제는 해당 요소가 메인 페이지가 아닌 iframe(내장 프레임) 안에 있기 때문입니다. Selenium은 기본적으로 메인 문서만 보므로 iframe 내부는 "보이지 않는" 별도 문서로 취급됩니다. 바로 이럴 때 필요한 것이 switch_to.frame()입니다. 컨텍스트를 iframe으로 전환하여 내부 요소에 접근하고, 작업 후 다시 메인으로 돌아올 수 있습니다.

Description

간단히 말해서, iframe은 웹 페이지 안에 또 다른 HTML 문서를 임베드하는 기술이며, Selenium은 switch_to.frame()으로 포커스를 이동하여 각 문서를 독립적으로 제어합니다. 실무에서 WYSIWYG 에디터(TinyMCE, CKEditor), 결제 게이트웨이, 소셜 미디어 임베드, 서드파티 위젯을 테스트할 때 필수적입니다. 예를 들어, 블로그 에디터에서 본문을 작성하거나, PayPal 결제 폼을 채우거나, YouTube 플레이어를 제어하는 경우에 매우 유용합니다. 기존에는 XPath로 iframe을 우회하려 했다면, 이제는 명시적으로 컨텍스트를 전환하여 의도를 분명히 할 수 있습니다. iframe 제어의 핵심 특징은 세 가지입니다. 첫째, switch_to.frame()으로 iframe 안으로 들어가고, switch_to.default_content()로 메인으로 돌아옵니다. 둘째, 인덱스, 이름/ID, WebElement 세 가지 방법으로 프레임을 지정할 수 있습니다. 셋째, 중첩된 iframe(iframe 안의 iframe)도 순차적으로 전환하여 접근합니다. 이러한 특징들이 복잡한 페이지 구조에서도 정확한 요소 제어를 가능하게 합니다.

Code

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com/editor")

# 방법 1: 인덱스로 전환 (0부터 시작)
driver.switch_to.frame(0)

# 방법 2: name 또는 id 속성으로 전환
driver.switch_to.default_content()  # 먼저 메인으로 돌아가기
driver.switch_to.frame("editor-frame")

# 방법 3: WebElement로 전환 (가장 안전)
driver.switch_to.default_content()
iframe_element = driver.find_element(By.CSS_SELECTOR, "iframe.editor")
driver.switch_to.frame(iframe_element)

# iframe 내부 요소 제어
editor_body = driver.find_element(By.ID, "tinymce")
editor_body.clear()
editor_body.send_keys("Selenium으로 작성한 글입니다!")

# 메인 문서로 돌아가기
driver.switch_to.default_content()

# 메인 페이지의 저장 버튼 클릭
driver.find_element(By.ID, "save-button").click()

driver.quit()

Explanation

이것이 하는 일: switch_to.frame()은 Selenium의 "시야"를 메인 문서에서 지정된 iframe 문서로 이동시켜, 이후 모든 find_element 호출이 iframe 내부에서만 검색하도록 만듭니다. 첫 번째로, iframe을 지정하는 세 가지 방법 중 WebElement를 사용하는 것이 가장 안전합니다. 인덱스는 페이지 구조가 바뀌면 깨지기 쉽고, name/id는 속성이 없을 수도 있습니다. find_element로 iframe을 먼저 찾으면 명시적 대기도 적용할 수 있어 동적 로딩에도 안정적입니다. 그 다음으로, switch_to.frame()을 호출하면 내부적으로 드라이버의 컨텍스트가 변경됩니다. 이제 driver.find_element()는 메인 문서가 아닌 iframe 문서의 DOM을 탐색합니다. TinyMCE 같은 에디터는 iframe 안에 <body id="tinymce">를 만들어 사용하므로, 전환 없이는 절대 접근할 수 없습니다. 마지막으로, switch_to.default_content()는 컨텍스트를 최상위 메인 문서로 리셋합니다. 이것을 호출하지 않으면 계속 iframe 안에 갇혀서 메인 페이지의 요소를 찾지 못하는 에러가 발생합니다. 중첩 iframe에서는 switch_to.parent_frame()으로 한 단계만 올라갈 수도 있습니다. 여러분이 이 코드를 사용하면 에디터 자동화, 결제 테스트, 임베드 콘텐츠 검증 등 iframe을 피할 수 없는 상황에서도 정확하게 요소를 제어할 수 있습니다. 또한 "요소를 찾을 수 없습니다" 에러가 나면 먼저 DevTools로 iframe 여부를 확인하는 디버깅 습관을 기를 수 있습니다.

Tips

💡 iframe 여부를 확인하려면 브라우저 DevTools에서 요소를 검사했을 때 상단에 "top ▶ iframe"처럼 표시되는지 보세요 💡 iframe이 동적으로 로드되면 WebDriverWait로 frame_to_be_available_and_switch_to_it() 조건을 사용하면 대기와 전환을 한 번에 처리합니다 💡 여러 iframe을 오가며 작업할 때는 전환 로직을 헬퍼 함수로 분리하면 코드가 깔끔해집니다 💡 중첩 iframe(iframe 안의 iframe)은 바깥쪽부터 순서대로 전환해야 하며, 한 번에 건너뛸 수 없습니다 💡 shadow DOM은 iframe과 다른 개념이므로 execute_script로 shadowRoot에 접근해야 합니다

8. JavaScript 실행 - Selenium으로 불가능한 작업 처리하기

Intro

여러분이 무한 스크롤을 구현하려고 send_keys(Keys.END)를 눌렀는데 새 콘텐츠가 로드되지 않아 막막했던 경험 있나요? 또는 숨겨진 요소를 클릭하려는데 "Element not visible" 에러가 계속 나는 상황도 있을 겁니다. 이런 문제들은 Selenium의 일반 메서드로는 브라우저의 깊은 레벨까지 제어하기 어렵기 때문에 발생합니다. 특히 React나 Vue 같은 프레임워크로 만든 SPA는 DOM 조작이 복잡해서 더욱 그렇습니다. 바로 이럴 때 필요한 것이 execute_script()입니다. JavaScript를 직접 실행하여 Selenium API로는 불가능한 고급 제어를 수행할 수 있습니다.

Description

간단히 말해서, execute_script()는 브라우저 콘솔에서 실행하는 것처럼 임의의 JavaScript 코드를 Selenium이 주입하여 실행하고, 결과를 Python으로 반환하는 메서드입니다. 실무에서 스크롤 제어, 요소 속성 변경, 이벤트 강제 발생, DOM 직접 조작, 숨겨진 데이터 추출 등 Selenium 표준 API의 한계를 넘어야 할 때 필수적입니다. 예를 들어, 무한 스크롤 페이지에서 끝까지 내려가거나, 읽기 전용 필드를 수정하거나, canvas 요소 데이터를 추출하는 경우에 매우 유용합니다. 기존에는 "Selenium으로 불가능합니다"라고 포기했다면, 이제는 JavaScript로 직접 해결할 수 있습니다. JavaScript 실행의 핵심 특징은 세 가지입니다. 첫째, 브라우저의 모든 Web API(DOM, window, document 등)에 제한 없이 접근할 수 있습니다. 둘째, arguments로 Python 변수를 JavaScript에 전달하고, return으로 결과를 받아올 수 있습니다. 셋째, 비동기 작업은 execute_async_script()로 처리합니다. 이러한 특징들이 Selenium의 가능성을 무한대로 확장시킵니다.

Code

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome()
driver.get("https://example.com/feed")

# 1. 페이지 끝까지 스크롤 (무한 스크롤 로딩)
for _ in range(5):
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(2)  # 새 콘텐츠 로딩 대기

# 2. 특정 요소로 스크롤 (부드러운 스크롤 효과)
element = driver.find_element(By.ID, "target-section")
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth'});", element)

# 3. 숨겨진 요소 클릭 (visibility 무시)
hidden_button = driver.find_element(By.ID, "hidden-btn")
driver.execute_script("arguments[0].click();", hidden_button)

# 4. 요소 속성 변경 (읽기 전용 해제)
readonly_input = driver.find_element(By.ID, "readonly-field")
driver.execute_script("arguments[0].removeAttribute('readonly');", readonly_input)
readonly_input.send_keys("수정된 값")

# 5. 데이터 추출 (localStorage, sessionStorage)
user_data = driver.execute_script("return localStorage.getItem('user');")
print(f"저장된 사용자 데이터: {user_data}")

driver.quit()

Explanation

이것이 하는 일: execute_script()는 첫 번째 인자로 받은 JavaScript 문자열을 브라우저의 현재 페이지 컨텍스트에서 실행하고, arguments 배열로 Python 객체를 전달합니다. 첫 번째로, window.scrollTo()나 scrollIntoView() 같은 Web API를 직접 호출하여 정밀한 스크롤 제어가 가능합니다. Selenium의 send_keys(Keys.PAGE_DOWN)은 포커스에 따라 동작이 달라지지만, JavaScript는 확실하게 원하는 위치로 이동합니다. 무한 스크롤은 scrollHeight를 사용해 페이지가 늘어날 때마다 끝으로 이동합니다. 그 다음으로, arguments 배열로 WebElement를 전달하면 JavaScript에서 실제 DOM 요소로 변환됩니다. arguments[0].click()처럼 사용하면 visibility나 클릭 가능 여부를 체크하지 않고 강제로 클릭할 수 있습니다. 이것은 오버레이에 가려진 버튼이나 display:none 요소를 다룰 때 유용하지만, 실제 사용자 행동과는 다르므로 신중히 사용해야 합니다. 마지막으로, return 문을 사용하면 JavaScript 실행 결과를 Python으로 가져올 수 있습니다. localStorage 데이터, 계산된 스타일, 요소 개수 등을 추출하여 검증에 활용할 수 있습니다. 반환되는 값은 자동으로 Python 타입으로 변환됩니다(JS 객체 → dict, 배열 → list). 여러분이 이 코드를 사용하면 Selenium 표준 메서드로 며칠 걸릴 문제를 몇 줄의 JavaScript로 즉시 해결할 수 있습니다. 또한 브라우저 DevTools 콘솔에서 먼저 JavaScript를 테스트한 후 execute_script()로 옮기는 워크플로우로 빠르게 개발할 수 있습니다.

Tips

💡 복잡한 JavaScript는 별도 .js 파일로 관리하고 파일을 읽어와 execute_script()에 전달하면 유지보수가 쉽습니다 💡 비동기 작업(fetch, setTimeout)은 execute_async_script()를 사용하고 마지막 인자로 받는 콜백을 호출해야 합니다 💡 요소가 여러 개면 arguments[0], arguments[1]처럼 순서대로 접근하되, Python에서 인자를 동일 순서로 전달하세요 💡 return 문 없이 실행만 하는 경우(스크롤, 클릭)는 반환값이 None이므로 체크하지 마세요 💡 크로스 브라우저 호환성을 위해 ES6+ 문법보다는 ES5 문법을 사용하는 것이 안전합니다

9. 다중 윈도우/탭 관리 - 팝업과 새 창 제어하기

Intro

여러분이 "새 탭에서 열기"를 클릭했는데 Selenium이 여전히 원래 페이지를 보고 있어서 새 탭의 요소를 찾지 못한 경험 있나요? 팝업 로그인, 결제 창, 외부 링크 같은 경우 자주 겪는 문제입니다. 이 문제는 브라우저가 여러 윈도우를 동시에 열어도 Selenium은 자동으로 따라가지 않기 때문입니다. 수동으로 포커스를 원하는 윈도우로 전환해야 합니다. 바로 이럴 때 필요한 것이 window_handles와 switch_to.window()입니다. 열린 모든 윈도우를 추적하고, 원하는 윈도우로 전환하여 제어할 수 있습니다.

Description

간단히 말해서, 각 윈도우/탭은 고유한 핸들(ID)을 가지며, switch_to.window()로 포커스를 옮겨 해당 윈도우의 콘텐츠를 제어하는 방식입니다. 실무에서 OAuth 로그인(팝업), 결제 게이트웨이(새 창), 파일 다운로드 확인, 광고 차단 테스트 등 여러 윈도우가 관여하는 플로우를 테스트할 때 필수적입니다. 예를 들어, "Google로 로그인" 팝업을 처리하거나, 링크를 새 탭에서 열어 콘텐츠를 검증하는 경우에 매우 유용합니다. 기존에는 새 창이 열리면 어떻게 해야 할지 몰라 포기했다면, 이제는 윈도우 간 자유롭게 전환할 수 있습니다. 다중 윈도우 관리의 핵심 특징은 세 가지입니다. 첫째, window_handles로 현재 열린 모든 윈도우의 핸들 리스트를 얻을 수 있습니다. 둘째, switch_to.window(핸들)로 포커스를 즉시 전환합니다. 셋째, close()로 현재 윈도우를 닫고 다른 윈도우로 돌아갈 수 있습니다. 이러한 특징들이 복잡한 다중 윈도우 시나리오를 완벽히 제어 가능하게 만듭니다.

Code

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome()
driver.get("https://example.com")

# 현재 윈도우 핸들 저장
main_window = driver.current_window_handle
print(f"메인 윈도우: {main_window}")

# 새 탭을 여는 링크 클릭
link = driver.find_element(By.LINK_TEXT, "새 탭에서 열기")
link.click()
time.sleep(1)  # 새 탭 로딩 대기

# 모든 윈도우 핸들 조회
all_windows = driver.window_handles
print(f"총 {len(all_windows)}개 윈도우 열림")

# 새 탭으로 전환 (마지막에 열린 윈도우)
for window in all_windows:
    if window != main_window:
        driver.switch_to.window(window)
        break

# 새 탭에서 작업 수행
print(f"새 탭 제목: {driver.title}")
driver.find_element(By.ID, "new-tab-button").click()

# 새 탭 닫기
driver.close()

# 메인 윈도우로 돌아가기
driver.switch_to.window(main_window)
print(f"메인 윈도우로 복귀: {driver.title}")

driver.quit()  # 남은 모든 윈도우 종료

Explanation

이것이 하는 일: 각 브라우저 윈도우는 고유한 핸들 문자열로 식별되며, switch_to.window()가 Selenium의 컨텍스트를 해당 윈도우로 이동시켜 모든 명령이 그 윈도우에서 실행되도록 합니다. 첫 번째로, current_window_handle로 현재 포커스된 윈도우의 핸들을 저장합니다. 이것은 나중에 "원래 창으로 돌아가기" 위한 북마크 역할을 합니다. 핸들은 "CDwindow-XXXXX" 형태의 임의 문자열이므로 직접 해석할 필요는 없습니다. 그 다음으로, 새 윈도우가 열린 후 window_handles를 호출하면 현재 열린 모든 윈도우의 핸들이 리스트로 반환됩니다. 순서는 열린 순서를 보장하지 않으므로, 집합 차이(set)를 이용해 새로 추가된 핸들을 찾는 것이 안전합니다: new = set(driver.window_handles) - {main_window}. 마지막으로, switch_to.window()로 전환하면 이후 모든 find_element, click 등은 해당 윈도우에서만 동작합니다. driver.close()는 현재 포커스된 윈도우만 닫고, driver.quit()는 모든 윈도우를 닫고 드라이버를 종료합니다. 여러분이 이 코드를 사용하면 OAuth 팝업에서 로그인하고 메인 페이지로 돌아오거나, 여러 탭을 동시에 열어 데이터를 수집하는 복잡한 워크플로우를 자동화할 수 있습니다. 또한 예상치 못한 광고 팝업이 뜨더라도 당황하지 않고 닫고 원래 작업으로 복귀할 수 있습니다.

Tips

💡 새 윈도우가 열릴 때까지 대기하려면 WebDriverWait로 len(driver.window_handles) == 2 조건을 체크하세요 💡 윈도우 전환 후 driver.title이나 driver.current_url로 올바른 윈도우에 있는지 검증하면 안전합니다 💡 여러 탭을 순회하려면 for handle in driver.window_handles: driver.switch_to.window(handle) 패턴을 사용하세요 💡 팝업을 닫을 때 close() 후 반드시 switch_to.window()로 다른 윈도우로 전환해야 이후 명령이 에러 없이 실행됩니다 💡 driver.switch_to.new_window('tab') 또는 'window'로 프로그래밍 방식으로 새 탭/창을 열 수도 있습니다 (Selenium 4+)

10. ActionChains - 마우스와 키보드 고급 제어

Intro

여러분이 드래그 앤 드롭, 마우스 호버 메뉴, 더블 클릭, 우클릭 같은 인터랙션을 구현하려고 했는데 일반 click()으로는 안 되어서 막막했던 경험 있나요? 복잡한 UI 인터랙션은 단순 클릭과 텍스트 입력만으로는 재현할 수 없습니다. 이 문제는 Selenium의 기본 메서드가 단일 액션만 처리하도록 설계됐기 때문입니다. 연속된 동작이나 복합 입력(Shift+클릭 등)은 표현할 방법이 없습니다. 바로 이럴 때 필요한 것이 ActionChains입니다. 여러 액션을 체이닝하여 복잡한 사용자 행동을 정밀하게 재현할 수 있습니다.

Description

간단히 말해서, ActionChains는 마우스 이동, 클릭, 드래그, 키 조합 등의 저수준 입력을 순차적으로 조립하여 perform()으로 한 번에 실행하는 빌더 패턴 클래스입니다. 실무에서 드래그 앤 드롭 UI, 컨텍스트 메뉴, 호버 툴팁, 슬라이더, 캔버스 그리기 등 고급 인터랙션을 테스트할 때 필수적입니다. 예를 들어, 파일을 드래그해서 업로드 영역에 놓거나, 메뉴 위에 마우스를 올려 서브메뉴를 표시하거나, 이미지 크롭 영역을 드래그로 선택하는 경우에 매우 유용합니다. 기존에는 "이건 자동화할 수 없어요"라고 했던 작업들도, 이제는 실제 사용자처럼 정밀하게 재현할 수 있습니다. ActionChains의 핵심 특징은 세 가지입니다. 첫째, 메서드 체이닝으로 복잡한 시퀀스를 읽기 쉽게 표현합니다. 둘째, 마우스(move, click, drag_and_drop)와 키보드(key_down, key_up)를 모두 제어합니다. 셋째, perform() 호출 전까지는 실제 실행되지 않아 안전하게 조립할 수 있습니다. 이러한 특징들이 현대적인 웹 UI의 모든 인터랙션을 자동화 가능하게 만듭니다.

Code

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome()
driver.get("https://example.com/interactive")

actions = ActionChains(driver)

# 1. 드래그 앤 드롭
source = driver.find_element(By.ID, "draggable")
target = driver.find_element(By.ID, "droppable")
actions.drag_and_drop(source, target).perform()

# 2. 마우스 호버 (서브메뉴 표시)
menu = driver.find_element(By.ID, "menu")
actions.move_to_element(menu).perform()
submenu = driver.find_element(By.ID, "submenu-item")
submenu.click()

# 3. 더블 클릭
element = driver.find_element(By.ID, "double-click-area")
actions.double_click(element).perform()

# 4. 우클릭 (컨텍스트 메뉴)
element = driver.find_element(By.ID, "right-click-area")
actions.context_click(element).perform()

# 5. 키 조합 (Ctrl+A로 전체 선택)
input_field = driver.find_element(By.ID, "text-input")
input_field.click()
actions.key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()

# 6. 복잡한 시퀀스 (클릭하고 드래그)
actions.click_and_hold(source).move_to_element(target).release().perform()

driver.quit()

Explanation

이것이 하는 일: ActionChains는 각 메서드 호출을 내부 큐에 저장하고, perform()이 호출되면 브라우저에 저수준 입력 이벤트를 순서대로 전송하여 실제 사용자 행동을 시뮬레이션합니다. 첫 번째로, ActionChains 객체를 생성할 때 driver를 전달하여 어느 브라우저를 제어할지 지정합니다. 이후 drag_and_drop(), move_to_element() 같은 메서드를 체이닝하면 각 액션이 큐에 추가됩니다. 아직 실행되지는 않습니다. 그 다음으로, perform()을 호출하는 순간 큐에 저장된 모든 액션이 순서대로 실행됩니다. 예를 들어, click_and_hold()는 마우스 버튼을 누른 상태를 유지하고, move_to_element()는 마우스를 이동시킨 후, release()가 버튼을 놓습니다. 이 세 동작이 연속으로 실행되어 드래그 효과를 만듭니다. 마지막으로, key_down()과 key_up()으로 Ctrl, Shift 같은 수정키를 누르고 있는 동안 다른 키를 입력하여 단축키를 재현합니다. send_keys()는 각 문자를 타이핑하는 것처럼 순차 입력합니다. 여러분이 이 코드를 사용하면 Jira의 드래그 앤 드롭 칸반 보드, 이미지 에디터의 크롭 기능, 지도 UI의 패닝/줌 같은 복잡한 인터랙션도 E2E 테스트에 포함시킬 수 있습니다. 또한 hover로만 나타나는 삭제 버튼이나 툴팁도 테스트할 수 있어 커버리지가 크게 향상됩니다.

Tips

💡 각 perform() 호출 후 ActionChains는 초기화되므로, 여러 시퀀스를 실행하려면 매번 새로 생성하거나 다시 체이닝하세요 💡 드래그가 실패하면 click_and_hold() → pause(0.5) → move_to_element() → pause(0.5) → release()처럼 지연을 추가하세요 💡 move_by_offset(x, y)로 상대 좌표 이동이 가능하여 슬라이더나 리사이즈를 정밀하게 제어할 수 있습니다 💡 복잡한 시퀀스는 perform() 호출 전에 프린트로 디버깅하거나, 단계별로 나눠서 실행하며 테스트하세요 💡 macOS에서는 Keys.COMMAND를, Windows/Linux에서는 Keys.CONTROL을 사용해야 Cmd+C, Ctrl+C가 올바르게 작동합니다

#Python#Selenium#WebDriver#Automation#Testing

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스