카테고리: 업무 자동화

파이썬 업무 자동화#7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)

목차12개 항목

안녕하세요! Mojangcam입니다 😎

지난 파이썬 업무 자동화 #6 - 예약 실행 자동화! 매일 정해진 시간에 스스로 돌게 만들기 에서는 스크립트가 정해진 시간에 알아서 돌아가게 만들었어요. 이제 시간도 알아서 맞춰주니까 크롤링을 돌려봤는데… 이런 일 겪어보신 적 있죠?

"분명 브라우저에는 데이터가 보이는데, requests로 받아오면 텅 빈 껍데기만 와요!" 😱

요즘 사이트들은 화면을 자바스크립트로 나중에 그려요. 그래서 requests가 받아오는 HTML에는 아직 아무것도 안 들어있는 거예요. 게다가 로그인해야 보이는 페이지는 아예 접근조차 안 되고요.

이번 편에서는 진짜 크롬 브라우저를 파이썬이 조종하게 만들어볼게요. 자바스크립트로 그려지는 페이지도 긁고, 아이디·비밀번호를 넣어 로그인까지 자동으로 해봅니다! 🚀

📌 준비물

  • Python 3.10 이상 (⚠️ Selenium 4.47은 3.10 미만을 더 이상 지원하지 않아요)
  • 크롬(Chrome) 브라우저 — 설치만 되어 있으면 OK
  • 라이브러리 2개
터미널
pip install selenium python-dotenv
  • 이 글 기준 버전: selenium 4.47.0 (2026-08-10 릴리스), python-dotenv 1.x

💡 크롬드라이버(chromedriver)는 따로 안 받아도 돼요! 예전 글들을 보면 크롬 버전에 맞는 드라이버를 직접 내려받아 PATH에 넣으라고 하는데, Selenium 4.6 이후로는 Selenium Manager가 자동으로 처리해줘요. webdriver_manager 같은 외부 패키지도 이제 필요 없어요.

🧠 왜 Selenium이 필요할까요?

백문이 불여일견! 연습용 사이트로 직접 비교해볼게요. (Zyte가 운영하는 크롤링 연습 사이트라 마음껏 긁어도 됩니다 😌)

Python
import requests

html = requests.get("https://quotes.toscrape.com/js/").text
print("quote 개수:", html.count('class="quote"'))

실행하면 결과가 이렇게 나와요.

텍스트
quote 개수: 0

브라우저로 열면 명언이 10개나 보이는데 0개예요. 이 페이지는 서버가 빈 껍데기 HTML만 보내주고, 명언 목록은 브라우저가 자바스크립트를 실행한 뒤에 그려 넣기 때문이에요.

정리하면 이렇습니다.

  • requests + BeautifulSoup : 서버가 준 HTML 원본만 봄 → 빠르지만 JS 결과는 못 봄
  • Selenium : 진짜 브라우저를 띄워서 JS 실행이 끝난 화면을 봄 → 느리지만 다 보임

로그인도 마찬가지예요. 아이디·비밀번호를 넣고 버튼을 누르는 과정, 세션 쿠키 유지까지 브라우저가 알아서 해주니까 훨씬 편해요!

📌 1단계 - 크롬 띄우고 페이지 열기

먼저 브라우저를 띄우는 것부터요. 딱 4줄이에요.

Python
# selenium 4.47.0 / Python 3.10+
from selenium import webdriver

driver = webdriver.Chrome()          # 크롬 실행 (드라이버 자동 준비!)
driver.get("https://quotes.toscrape.com/js/")
print(driver.title)
driver.quit()                        # 반드시 닫아주기

하나씩 뜯어볼게요 💡

  • webdriver.Chrome() : 크롬을 실행하고 조종 핸들(driver)을 돌려줘요. 이때 Selenium Manager가 알아서 드라이버를 내려받아 캐시에 넣어둡니다.
  • driver.get(url) : 주소창에 URL 치고 엔터 누른 것과 같아요. 페이지 기본 로딩이 끝날 때까지 기다려줘요.
  • driver.title : 현재 페이지의 <title> 값. 잘 열렸는지 확인용으로 딱 좋아요.
  • driver.quit() : 브라우저를 완전히 종료해요. 이걸 빼먹으면 크롬 프로세스가 좀비처럼 계속 쌓여서 메모리를 잡아먹어요. 꼭 넣어주세요!

📌 2단계 - 동적 페이지 기다리기 (WebDriverWait)

브라우저는 떴는데, 바로 요소를 찾으면 아직 안 그려져 있어서 에러가 나요. 그래서 "이 요소가 나타날 때까지 기다려"라고 알려줘야 해요.

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

driver.get("https://quotes.toscrape.com/js/")

wait = WebDriverWait(driver, 10)                                   # 최대 10초
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".quote")))

for box in driver.find_elements(By.CSS_SELECTOR, ".quote"):
    text = box.find_element(By.CSS_SELECTOR, ".text").text
    author = box.find_element(By.CSS_SELECTOR, ".author").text
    print(f"{author} : {text}")

하나씩 뜯어볼게요 💡

  • WebDriverWait(driver, 10) : "최대 10초까지는 기다려줄게"라는 뜻이에요. 조건이 1초 만에 충족되면 1초만 기다리고 바로 넘어가요. time.sleep(10)처럼 무조건 10초 버리는 게 아니에요!
  • EC.presence_of_element_located : 요소가 DOM에 존재하면 통과. 화면에 보이는지까지 확인하려면 EC.visibility_of_element_located, 클릭 가능한지까지 보려면 EC.element_to_be_clickable을 쓰면 돼요.
  • By.CSS_SELECTOR : 셀렉터 방식. By.ID, By.NAME, By.CLASS_NAME, By.XPATH, By.LINK_TEXT 등이 있어요. 개인적으로는 CSS_SELECTOR가 제일 짧고 잘 안 깨져요.
  • find_element vs find_elements : 앞은 1개(없으면 에러), 뒤는 리스트(없으면 빈 리스트). 목록을 돌 땐 무조건 복수형이에요.
  • .text : 요소 안의 눈에 보이는 글자만 뽑아줘요.

⚠️ time.sleep()으로 버티는 방식은 느리면 실패하고, 빠르면 시간을 낭비해요. 웬만하면 WebDriverWait을 쓰세요!

📌 3단계 - 로그인 자동화 (.env로 계정 숨기기)

이제 하이라이트예요. 아이디·비밀번호를 넣고 로그인 버튼까지 눌러볼게요.

절대로 코드에 비밀번호를 그대로 쓰지 마세요! 깃허브에 올리는 순간 그대로 털립니다 😱 .env 파일에 따로 빼두는 게 정석이에요.

프로젝트 폴더에 .env 파일을 만들어주세요.

터미널
# .env  ← 이 파일은 절대 깃허브에 올리지 마세요!
SITE_USER=mojangcam
SITE_PASS=my_secret_password

그리고 .gitignore에 한 줄 추가! 💡

터미널
# .gitignore
.env

이제 파이썬에서 불러 쓰면 돼요.

Python
import os
from dotenv import load_dotenv

load_dotenv()                       # .env 파일을 읽어서 환경변수로 등록

USER = os.getenv("SITE_USER")
PASSWORD = os.getenv("SITE_PASS")

driver.get("https://quotes.toscrape.com/login")
wait.until(EC.presence_of_element_located((By.ID, "username")))

driver.find_element(By.ID, "username").send_keys(USER)
driver.find_element(By.ID, "password").send_keys(PASSWORD)
driver.find_element(By.CSS_SELECTOR, "input[type='submit']").click()

# 로그아웃 링크가 보이면 = 로그인 성공!
wait.until(EC.presence_of_element_located((By.LINK_TEXT, "Logout")))
print("✅ 로그인 성공!")

하나씩 뜯어볼게요 💡

  • load_dotenv() : 같은 폴더의 .env를 읽어 환경변수로 올려줘요. 이 한 줄이 없으면 os.getenv가 계속 None을 돌려줍니다.
  • send_keys(값) : 키보드로 타이핑하는 것과 똑같아요. 입력 전에 기존 값을 지우려면 .clear()를 먼저 호출하세요.
  • click() : 마우스 클릭. 로그인 버튼이 <button>이면 By.CSS_SELECTOR, "button[type='submit']"로 바꿔주면 돼요.
  • 로그인 성공 판정 : 성공했을 때만 나타나는 요소(여기선 Logout 링크)를 기다리는 게 가장 확실해요. 비밀번호가 틀리면 TimeoutException이 나서 바로 알 수 있거든요.

📎 이 연습 사이트는 아무 아이디·비번이나 넣어도 로그인돼요. 마음 편히 테스트해보세요!

📌 전체 코드 (복붙 OK)

로그인 → 동적 페이지 수집 → CSV 저장까지 한 번에 도는 완성본이에요.

Python
# quotes_selenium.py
# selenium 4.47.0 / python-dotenv 1.x / Python 3.10+
import csv
import os
from datetime import date

from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException

load_dotenv()

LOGIN_URL = "https://quotes.toscrape.com/login"
TARGET_URL = "https://quotes.toscrape.com/js/"
USER = os.getenv("SITE_USER")
PASSWORD = os.getenv("SITE_PASS")
HEADLESS = os.getenv("HEADLESS", "false").lower() == "true"

def make_driver(headless: bool = False):
    """크롬 드라이버를 만들어서 돌려줘요."""
    options = Options()
    if headless:
        options.add_argument("--headless=new")
    options.add_argument("--window-size=1920,1080")
    options.add_argument("--lang=ko-KR")
    options.add_argument("--disable-gpu")
    return webdriver.Chrome(options=options)

def login(driver, wait):
    """아이디·비밀번호를 넣고 로그인해요."""
    if not USER or not PASSWORD:
        raise RuntimeError(".env에 SITE_USER / SITE_PASS를 먼저 넣어주세요!")

    driver.get(LOGIN_URL)
    wait.until(EC.presence_of_element_located((By.ID, "username")))

    driver.find_element(By.ID, "username").send_keys(USER)
    driver.find_element(By.ID, "password").send_keys(PASSWORD)
    driver.find_element(By.CSS_SELECTOR, "input[type='submit']").click()

    try:
        wait.until(EC.presence_of_element_located((By.LINK_TEXT, "Logout")))
    except TimeoutException:
        raise RuntimeError("로그인 실패! 계정 정보나 셀렉터를 확인해주세요.")

    print("✅ 로그인 성공")

def collect_quotes(driver, wait, max_page: int = 3):
    """자바스크립트로 그려지는 명언 목록을 페이지 넘겨가며 수집해요."""
    rows = []
    driver.get(TARGET_URL)

    for page in range(1, max_page + 1):
        wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".quote")))

        boxes = driver.find_elements(By.CSS_SELECTOR, ".quote")
        for box in boxes:
            rows.append({
                "author": box.find_element(By.CSS_SELECTOR, ".author").text,
                "quote": box.find_element(By.CSS_SELECTOR, ".text").text,
            })
        print(f"📄 {page}페이지 수집 완료 ({len(boxes)}개)")

        next_links = driver.find_elements(By.CSS_SELECTOR, "li.next > a")
        if not next_links:
            print("➡️ 마지막 페이지예요")
            break
        next_links[0].click()
        wait.until(EC.staleness_of(boxes[0]))   # 이전 페이지가 사라질 때까지 대기

    return rows

def save_csv(rows, filename: str):
    with open(filename, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=["author", "quote"])
        writer.writeheader()
        writer.writerows(rows)
    print(f"💾 {filename} 저장 완료 (총 {len(rows)}줄)")

def main():
    driver = make_driver(headless=HEADLESS)
    wait = WebDriverWait(driver, 10)

    try:
        login(driver, wait)
        rows = collect_quotes(driver, wait, max_page=3)
        save_csv(rows, f"quotes_{date.today():%Y%m%d}.csv")
    finally:
        driver.quit()          # 에러가 나도 브라우저는 꼭 닫아요

if __name__ == "__main__":
    main()

📌 실행 결과

터미널
python quotes_selenium.py

크롬 창이 스윽 뜨더니 혼자서 타이핑하고 버튼을 누르는 모습을 볼 수 있어요. 처음 보면 좀 신기해요 😆

텍스트
✅ 로그인 성공
📄 1페이지 수집 완료 (10개)
📄 2페이지 수집 완료 (10개)
📄 3페이지 수집 완료 (10개)
💾 quotes_20260826.csv 저장 완료 (총 30줄)

💡 CSV를 encoding="utf-8-sig"로 저장한 이유는 엑셀에서 한글이 깨지지 않게 하기 위해서예요. 그냥 utf-8로 저장하면 엑셀에서 외계어가 됩니다!

💡 보너스 - 창 없이 백그라운드로 돌리기 (헤드리스)

자동화 스크립트를 서버나 예약 실행으로 돌릴 땐 창이 안 뜨는 게 좋겠죠? .env에 한 줄만 추가하면 돼요.

터미널
# .env
HEADLESS=true

위 전체 코드에 이미 반영돼 있어요. 핵심은 이 부분이에요.

Python
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
  • --headless=new : 크롬의 새 헤드리스 모드예요. 예전 --headless는 별도 바이너리로 분리돼서 동작이 달라요. 이제는 =new를 붙이는 게 표준입니다.
  • --window-size : 헤드리스는 기본 창이 작아서 반응형 사이트가 모바일 레이아웃으로 뜰 수 있어요. 크게 지정해두면 셀렉터가 안 깨져요.

지난 #6 예약 실행 편의 schedule과 합치면 매일 아침 9시에 알아서 로그인해서 데이터를 긁어오는 봇이 완성돼요! 🙌

📌 자주 나는 에러

1. SessionNotCreatedException: This version of ChromeDriver only supports Chrome version XXX

  • 원인 : 크롬 브라우저와 드라이버 버전이 안 맞아요. 예전에 수동으로 받아둔 드라이버가 PATH에 남아있는 경우가 많아요.
  • 해결 : pip install -U selenium으로 최신 버전을 깔고, PATH에 있는 옛날 chromedriver.exe를 지우세요. 그래도 안 되면 Selenium Manager 캐시(~/.cache/selenium, 윈도우는 C:\Users\사용자명\.cache\selenium)를 통째로 삭제하고 다시 실행하면 새로 받아요.

2. NoSuchElementException: Unable to locate element

  • 원인 : ① 요소가 아직 안 그려졌거나 ② 셀렉터가 틀렸거나 ③ 요소가 iframe 안에 있어요.
  • 해결 : ①은 WebDriverWait으로 해결. ②는 크롬에서 F12 → 요소 우클릭 → Copy → Copy selector로 확인. ③은 driver.switch_to.frame("프레임이름")으로 먼저 들어가야 해요.

3. TimeoutException

  • 원인 : 기다리는 조건이 제한 시간 안에 충족되지 않았어요. 로그인 실패일 때도 이게 떠요.
  • 해결 : WebDriverWait(driver, 20)으로 시간을 늘려보고, 그래도 안 되면 로그인 자체가 실패한 건 아닌지 헤드리스를 끄고 눈으로 확인해보세요.

4. ElementClickInterceptedException

  • 원인 : 쿠키 동의 배너, 팝업 광고 같은 게 버튼을 가리고 있어요.
  • 해결 : 팝업을 먼저 닫거나, 자바스크립트로 강제 클릭하세요. → driver.execute_script("arguments[0].click();", element)

5. ModuleNotFoundError: No module named 'selenium'

  • 원인 : 가상환경을 안 켰거나, 다른 파이썬에 설치했어요.
  • 해결 : 가상환경을 활성화한 뒤 pip install selenium. venv가 헷갈리면 파이썬 가상환경(venv) 완벽 정리 편을 참고해주세요!

6. 눈으로 볼 땐 되는데 헤드리스에서만 실패해요

  • 원인 : 창 크기가 작아 레이아웃이 바뀌었거나, 봇으로 탐지됐어요.
  • 해결 : --window-size=1920,1080 지정 + options.add_argument("--user-agent=...")로 일반 브라우저 UA를 넣어보세요.

💬 requests vs Selenium, 언제 뭘 쓸까요?

상황 추천
서버가 완성된 HTML을 그대로 줌 requests + BeautifulSoup (10배 이상 빠름)
공식 API(JSON)가 있음 requests — 무조건 이게 최고예요
스크롤·클릭해야 내용이 나옴 Selenium
로그인·세션이 필요함 Selenium (또는 requests.Session)
파일 다운로드 버튼을 눌러야 함 Selenium

💡 팁 : 크롬 F12 → Network 탭 → Fetch/XHR을 보면, 화면을 그리는 원본 JSON API 주소가 그대로 보이는 경우가 많아요. 그 주소를 찾으면 Selenium 없이 requests만으로 훨씬 빠르게 끝낼 수 있어요! 항상 여기부터 확인해보세요.

⚠️ 그리고 꼭 지켜주세요. robots.txt 확인 · 이용약관 확인 · 요청 사이 간격 두기. 남의 서비스에 로그인 자동화를 돌리는 건 계정 정지 사유가 될 수 있어요. 연습은 연습용 사이트에서! 🙏

📌 마무리

오늘은 이런 걸 해봤어요.

  • requests로는 못 긁는 자바스크립트 렌더링 페이지를 Selenium으로 수집
  • WebDriverWait으로 똑똑하게 기다리기 (time.sleep 탈출!)
  • .env + python-dotenv로 계정 정보 안전하게 관리하며 로그인 자동화
  • 헤드리스 모드로 창 없이 백그라운드 실행

이제 로그인 뒤에 있는 데이터까지 자동으로 가져올 수 있게 됐어요! 여기에 #6의 예약 실행을 붙이면 완전 무인 자동화가 되는 거죠 😎

다음 편인 #8 - PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 에서는 쌓여만 가는 PDF 파일들을 파이썬으로 한 방에 정리해볼게요. 기대해주세요! 🙌

혹시 막히는 사이트가 있다면 댓글로 남겨주세요. 같이 셀렉터 찾아드릴게요 💬

📚 파이썬 업무 자동화 시리즈

  1. #1 - 엑셀 자동화 입문 (openpyxl)
  2. #2 - 웹 크롤링 입문 (requests + BeautifulSoup)
  3. #3 - 파일 정리 자동화 (이름 일괄 변경)
  4. #4 - 디스코드 웹훅(Webhook)으로 알림 보내기
  5. #5 - 로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약
  6. #6 - 예약 실행 자동화 (schedule·APScheduler)
  7. #7 - 동적 페이지·로그인 자동화 (Selenium) ← 지금 이 글!
  8. #8 - PDF 자동 처리 (pypdf) (예정)
  9. #9 - 마우스·키보드 GUI 자동화 (PyAutoGUI) (예정)

연재 · 14편 중 7번째

파이썬 업무 자동화

  1. #1엑셀 자동화 입문 (openpyxl)!
  2. #2웹 크롤링 입문 (requests + BeautifulSoup)!
  3. #3파일 정리 자동화 (이름 일괄 변경)!
  4. #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)!
  5. #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI)
  6. #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)
  7. #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)지금 읽는 글
  8. #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄
  9. #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI)
  10. #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄
  11. #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬
  12. #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗
  13. #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기
  14. #14파워포인트(PowerPoint) 보고서 자동 생성! 엑셀 숫자만 바꾸면 발표자료 완성 (python-pptx) 📊
전체 14편 보기접기

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그