웹 폼 입력 자동화 기초 — 셀레니움과 플레이라이트로 시작하기

매주 월요일 아침, 같은 사내 시스템에 접속해서 같은 양식에 50건을 입력합니다. 한 건에 40초씩만 잡아도 30분이 넘고, 중간에 한 번 실수하면 처음부터 확인해야 합니다. 웹 자동화 셀레니움이나 플레이라이트를 쓰면 이 30분이 브라우저를 켜두고 기다리는 2분으로 바뀝니다. 1년이면 스물다섯 시간, 사흘치 근무시간입니다.

이 글에서는 두 도구 중 무엇을 고를지, 로그인 폼 하나를 자동으로 채우는 최소 코드, 드롭다운·체크박스·파일 업로드 같은 입력 유형별 처리법, 그리고 초보자가 거의 반드시 겪는 “로컬에선 되는데 자동 실행하면 실패” 문제의 원인까지 순서대로 다룹니다.

셀레니움과 플레이라이트, 무엇을 고를까

둘 다 실제 브라우저를 프로그램으로 조종한다는 점은 같습니다. 결정적인 차이는 기다리는 방식입니다. 셀레니움은 “요소가 나타날 때까지 기다려라”를 개발자가 매번 써줘야 하고, 플레이라이트는 클릭이나 입력을 할 때 알아서 기다립니다. 자동화 스크립트가 불안정한 원인의 대부분이 이 대기 처리라서, 새로 시작한다면 플레이라이트를 권합니다.

항목셀레니움 (Selenium)플레이라이트 (Playwright)
첫 출시2004년2020년
드라이버 설치셀레니움 4.6+ 부터 자동 관리playwright install로 브라우저까지 함께 설치
자동 대기직접 WebDriverWait 작성기본 내장
지원 언어Python, Java, C#, JS, Ruby 등Python, Java, C#, JS/TS
코드 자동 생성별도 IDE 확장 필요playwright codegen 내장
자료의 양한국어 자료가 압도적으로 많음상대적으로 적지만 공식 문서가 충실

다만 회사에서 이미 셀레니움으로 만들어 둔 스크립트가 있거나, 검색해서 나오는 예제를 그대로 따라 하는 게 편하다면 셀레니움도 전혀 문제없습니다. 아래에서는 같은 작업을 두 도구로 나란히 보여드립니다.

설치

# 셀레니움 (파이썬)
pip install selenium

# 플레이라이트 (파이썬) — 브라우저까지 함께 받습니다
pip install playwright
playwright install chromium

셀레니움은 4.6 버전부터 크롬드라이버를 자동으로 내려받습니다. 예전 자료에 나오는 chromedriver.exe를 직접 받아 경로를 지정하는 과정은 이제 필요 없습니다. 플레이라이트는 playwright install이 전용 브라우저를 따로 설치하기 때문에, 내 크롬이 업데이트돼도 스크립트가 깨지지 않는 장점이 있습니다.

첫 자동화 — 로그인 폼 채우기

연습에는 공개 테스트 사이트를 쓰는 게 안전합니다. 아래 예제의 saucedemo.com은 자동화 학습용으로 공개된 사이트라 마음껏 돌려도 됩니다. 회사 시스템이나 남의 서비스에 연습 삼아 반복 요청을 보내지 마세요.

셀레니움 버전

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()

try:
    driver.get("https://www.saucedemo.com/")

    # 최대 10초까지 요소가 나타나기를 기다립니다.
    wait = WebDriverWait(driver, 10)

    user = wait.until(EC.presence_of_element_located((By.ID, "user-name")))
    user.send_keys("standard_user")

    driver.find_element(By.ID, "password").send_keys("secret_sauce")
    driver.find_element(By.ID, "login-button").click()

    # 로그인 성공 여부를 URL 변화로 확인합니다.
    wait.until(EC.url_contains("/inventory.html"))
    print("로그인 성공:", driver.current_url)
finally:
    driver.quit()   # 이걸 빼먹으면 크롬 프로세스가 계속 쌓입니다

플레이라이트 버전

from playwright.sync_api import sync_playwright

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    page = browser.new_page()

    page.goto("https://www.saucedemo.com/")

    # fill()은 요소가 준비될 때까지 알아서 기다립니다.
    page.fill("#user-name", "standard_user")
    page.fill("#password", "secret_sauce")
    page.click("#login-button")

    page.wait_for_url("**/inventory.html")
    print("로그인 성공:", page.url)

    browser.close()

코드 길이 차이가 눈에 보입니다. 셀레니움 쪽에서 WebDriverWait와 expected_conditions가 차지하는 부분이, 플레이라이트에서는 fill()과 click() 안에 들어가 있습니다. 처음엔 셀레니움에서도 그냥 find_element만 써도 동작하는 것처럼 보이는데, 네트워크가 느린 날 갑자기 실패하기 시작합니다. 그래서 처음부터 대기를 붙여 쓰는 습관이 중요합니다.

입력칸을 정확히 찾는 법 — 선택자

자동화가 깨지는 가장 흔한 원인은 대기 문제 다음으로 선택자입니다. 브라우저에서 F12를 눌러 요소를 검사한 뒤, 아래 우선순위대로 고르세요.

# 우선순위 1 — id (가장 안정적)
page.fill("#email", "me@example.com")

# 우선순위 2 — name 속성
page.fill("input[name='email']", "me@example.com")

# 우선순위 3 — 사람이 보는 라벨 기준 (플레이라이트의 강점)
page.get_by_label("이메일").fill("me@example.com")
page.get_by_role("button", name="로그인").click()

# 피해야 할 것 — 자동 생성된 클래스명, 절대 XPath
# page.click(".css-1x2y3z4")
# page.click("/html/body/div[3]/div[2]/form/button")

개발자 도구에서 “Copy selector”나 “Copy XPath”로 복사한 값은 당장은 동작하지만, 사이트가 조금만 바뀌어도 전부 깨집니다. div:nth-child(3) > form > button 같은 경로는 화면에 배너 하나만 추가돼도 어긋납니다. 반대로 get_by_label("이메일")처럼 사람이 화면에서 읽는 글자를 기준으로 잡으면, 디자인이 바뀌어도 웬만해선 살아남습니다.

사이트에 data-testid 같은 테스트 전용 속성이 있다면 그게 1순위입니다. 개발팀이 “이건 안 바꾼다”고 약속한 표식이기 때문입니다. 내가 만든 서비스를 자동화한다면 지금이라도 붙여두면 좋습니다.

입력 유형별 처리법

텍스트 입력만 되면 절반은 끝이지만, 실제 업무 양식에는 드롭다운과 첨부파일이 섞여 있습니다. 유형별로 방법이 다릅니다.

# 1) 드롭다운(select) — 눈에 보이는 글자로 고르기
page.select_option("#city", label="서울")

# 2) 체크박스 / 라디오 — 이미 켜져 있으면 건너뜁니다
page.check("#agree-terms")

# 3) 파일 업로드 — 파일 선택창을 열지 않고 경로를 바로 넣습니다
page.set_input_files("#attachment", "C:/work/report.pdf")

# 4) 날짜 — date 타입은 달력을 클릭하지 말고 값을 직접 넣는 게 안정적입니다
page.fill("#start-date", "2026-09-17")

# 5) iframe 안의 입력칸 — frame을 먼저 붙잡아야 합니다
page.frame_locator("#payment-frame").locator("#card-number").fill("4242424242424242")

특히 파일 업로드는 많이들 헤매는 부분입니다. 업로드 버튼을 클릭하면 운영체제의 파일 선택창이 뜨는데, 이 창은 브라우저 바깥이라 자동화 도구가 조작할 수 없습니다. 클릭하지 말고 set_input_files()로 <input type="file">에 경로를 직접 넣어야 합니다. 셀레니움에서는 같은 요소에 send_keys("C:/work/report.pdf")를 쓰면 됩니다.

iframe도 비슷하게 발목을 잡습니다. 결제 모듈이나 외부 위젯은 별도 문서로 들어가 있어서, 평범하게 찾으면 “요소가 없다”는 오류가 납니다. 프레임을 먼저 지정해야 안이 보입니다.

CSV 데이터로 반복 입력하기

여기서부터가 진짜 자동화입니다. 입력할 값을 CSV에 적어두고 한 줄씩 돌리면, 50건이든 500건이든 같은 코드로 처리됩니다.

import csv
import time
from playwright.sync_api import sync_playwright

# members.csv 예시
# name,email,dept
# 김철수,chulsoo@example.com,영업
# 이영희,younghee@example.com,개발

with open("members.csv", encoding="utf-8-sig", newline="") as f:
    rows = list(csv.DictReader(f))

failed = []

with sync_playwright() as pw:
    browser = pw.chromium.launch()
    page = browser.new_page()

    for i, row in enumerate(rows, start=1):
        try:
            page.goto("https://example.com/register")
            page.fill("#name", row["name"])
            page.fill("#email", row["email"])
            page.select_option("#dept", label=row["dept"])
            page.click("button[type='submit']")

            # 제출됐다는 확실한 신호를 기다립니다. 성공 문구가 가장 좋습니다.
            page.wait_for_selector("text=등록이 완료", timeout=10000)
            print(f"{i}/{len(rows)} 성공 - {row['name']}")
        except Exception as e:
            print(f"{i}/{len(rows)} 실패 - {row['name']}: {e}")
            page.screenshot(path=f"error-{i}.png")   # 실패 순간을 남겨둡니다
            failed.append(row)

        time.sleep(1)   # 서버에 부담을 주지 않도록 간격을 둡니다

    browser.close()

print("실패 건수:", len(failed))

이 코드에서 눈여겨볼 부분은 세 가지입니다. 첫째, try/except로 감싸서 한 건이 실패해도 나머지가 계속 돌아갑니다. 둘째, 실패한 순간 스크린샷을 남기기 때문에 나중에 원인을 눈으로 확인할 수 있습니다. 셋째, encoding="utf-8-sig"로 엑셀이 저장한 CSV 앞머리의 BOM을 처리합니다. 이걸 빼면 첫 번째 열 이름 앞에 보이지 않는 BOM 문자가 붙어 row["name"] 조회가 KeyError로 실패합니다.

반복 사이의 time.sleep(1)은 예의이자 안전장치입니다. 사람이 낼 수 없는 속도로 요청을 퍼부으면 서버에 부담이 되고, 어뷰징으로 판단돼 계정이 차단될 수도 있습니다.

“로컬에선 되는데 자동 실행하면 실패”의 원인

가장 많이 겪는 상황입니다. 내 화면에서 실행하면 잘 되는데, 작업 스케줄러에 걸어두면 실패합니다. 원인은 대개 아래 셋 중 하나입니다.

1) 고정 대기(sleep)로 버티고 있었던 경우

# 나쁜 예 — 3초는 어떤 날은 모자라고, 대부분의 날은 낭비입니다
time.sleep(3)
driver.find_element(By.ID, "submit").click()

# 좋은 예 (셀레니움) — 클릭 가능해지는 즉시 진행
WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "submit"))
).click()

# 좋은 예 (플레이라이트) — click() 자체가 기다려 줍니다
page.click("#submit")

내 PC에서 3초면 충분했던 로딩이 서버가 붐비는 시간에는 5초가 걸립니다. 시간을 늘리는 건 해결이 아닙니다. 조건을 기다려야 합니다.

2) 창 크기가 달라서 요소가 안 보이는 경우

헤드리스 모드는 기본 창 크기가 작아서, 반응형 사이트가 모바일 레이아웃으로 렌더링됩니다. 그러면 PC 화면에만 있던 버튼이 햄버거 메뉴 안으로 들어가 버려 클릭에 실패합니다. 창 크기를 명시하세요.

# 셀레니움 — 창 없이 실행
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")   # 창 크기를 지정해야 레이아웃이 깨지지 않습니다
driver = webdriver.Chrome(options=options)

# 플레이라이트 — headless가 기본값입니다
# browser = pw.chromium.launch()               → 창 없음
# browser = pw.chromium.launch(headless=False) → 창 보임 (디버깅용)

3) 로그인 세션이 없는 경우

평소 브라우저에는 로그인 쿠키가 남아 있지만, 자동화가 띄우는 브라우저는 매번 완전히 새 상태입니다. 로그인 단계를 스크립트에 포함시키거나, 플레이라이트의 storage_state로 로그인 상태를 파일에 저장해 재사용해야 합니다.

손으로 안 짜고 받아 적게 하기

플레이라이트에는 내 조작을 그대로 코드로 옮겨주는 기능이 있습니다. 선택자를 직접 찾는 수고를 크게 줄여줍니다.

# 브라우저를 띄우고, 내가 하는 조작을 그대로 파이썬 코드로 받아 적습니다.
playwright codegen https://example.com/login

# 생성된 코드를 파일로 바로 저장
playwright codegen -o login.py --target python https://example.com/login

명령을 실행하면 브라우저와 코드 창이 함께 뜹니다. 평소처럼 입력하고 클릭하면 오른쪽에 코드가 쌓입니다. 다만 생성된 코드는 초안입니다. 불필요한 클릭이 섞여 있고 선택자가 지나치게 구체적인 경우가 많아서, 앞에서 말한 기준으로 한 번 정리한 뒤에 쓰는 걸 권합니다.

주의할 점

  • 대상 사이트의 이용약관을 먼저 확인하세요. 자동화된 접근을 금지하는 곳이 있고, 위반하면 계정이 정지될 수 있습니다. 사내 시스템이라면 담당 부서에 알리고 진행하는 게 안전합니다.
  • 계정 정보를 코드에 직접 적지 않습니다. .env 파일이나 환경변수로 분리하세요. 스크립트를 공유하거나 깃에 올리는 순간 비밀번호가 그대로 노출됩니다.
  • 먼저 한 건으로 검증하고 전체를 돌립니다. 50건짜리 루프를 바로 실행했다가 잘못된 값이 50건 등록되면, 지우는 데 자동화로 아낀 시간보다 더 걸립니다.
  • 캡차(CAPTCHA)와 2단계 인증이 있으면 멈춥니다. 이건 사람임을 확인하려고 만든 장치라 우회 대상이 아닙니다. 해당 사이트가 API를 제공하는지 먼저 찾아보는 편이 훨씬 빠릅니다.
  • 브라우저를 반드시 닫습니다. driver.quit()이나 browser.close()를 빼먹으면 실행할 때마다 프로세스가 쌓여 메모리를 잡아먹습니다. 파이썬이라면 try/finally나 with 문으로 보장하세요.
  • API가 있으면 API를 씁니다. 브라우저 자동화는 다른 방법이 없을 때 쓰는 수단입니다. 같은 작업을 HTTP 요청으로 할 수 있다면 그쪽이 열 배 빠르고 스무 배 안정적입니다.

마무리

첫걸음은 5분이면 됩니다. pip install playwright와 playwright install chromium을 실행하고, 위의 로그인 예제를 그대로 복사해 돌려보세요. 브라우저가 저절로 열려 아이디와 비밀번호가 채워지는 걸 한 번 보고 나면, 평소에 반복하던 작업 중 무엇을 넘길 수 있을지 자연스럽게 떠오릅니다. 그다음엔 playwright codegen으로 실제 업무 사이트를 한 번 녹화해 보시길 권합니다.

다음 글에서는 자주 쓰는 명령어를 별칭(alias)과 함수로 등록해 두는 법을 다룹니다. 위에서 매번 길게 친 playwright codegen ... 같은 명령도 두 글자로 줄일 수 있습니다.

댓글 남기기