Selenium · Playwright · wait · 로그인 · 세션 · 무한 스크롤
박수현 · aSSIST University · 2026
🎨 이미지 프롬프트(배경): "A robot hand carefully typing into a login form on a glowing browser, with cookies and session tokens flowing as data streams, teal and purple tones, editorial illustration, 16:9, no text"
📖 도서 매핑: Ch.5 §2~3 + Ch.12 §1
정적 스크래핑의 한계와 동적 웹의 제약
키워드: 정적 웹 · 동적 웹 · 로그인 · JS 렌더링
S 정적 웹사이트
HTML 완성본
즉시 제공
✓ requests + BS4
바로 파싱
가능
D 동적 웹사이트
JS 실행 후
데이터 표시
→ 브라우저 자동화
Selenium / Playwright
필요
requests/BS4 로 충분어느 도구를 쓰든 패턴은 같다
키워드: launch · navigate · wait · extract
pip install playwright)| 항목 | Selenium | Playwright |
|---|---|---|
| 기본값 wait | 명시적 (코드에서 wait 직접 작성) | 자동 wait — 액션 직전에 요소 가시·안정 자동 대기 |
| Headless | OK | OK |
| 멀티탭 | OK | 컨텍스트 분리 (브라우저 창 단위 격리) |
| 네트워크 가로채기 | 어려움 | 쉬움 (route) |
| 학습곡선 | 완만 | 약간 가파름 |
🎯 신규 프로젝트라면 Playwright 추천. 기존 자료/팀 호환 → Selenium.
pip install playwright
playwright install chromium
💡 개발 환경 정리 팁 - venv 안에서
pip install playwright→ 강의별 가상환경 권장 -playwright install chromium은 용량이 크므로 강의 전 미리 한 번 실행 - Docker 나 서버에서는 Dockerfile 안에서 미리 설치 (도서 예제 참조)
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,
)
# 1) launch (브라우저 실행)
driver = webdriver.Chrome()
# 2) navigate (페이지 이동)
driver.get("https://books.toscrape.com/")
# 3) wait (요소 대기)
WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR,
"article.product_pod")))
# 4) extract (값 추출)
title = driver.find_element(
By.CSS_SELECTOR,
"article.product_pod h3 a"
).get_attribute("title")
print(title)
driver.quit()
from playwright.sync_api import (
sync_playwright,
)
with sync_playwright() as pw:
# 1) launch
browser = pw.chromium.launch()
page = browser.new_page()
# 2) navigate
page.goto(
"https://books.toscrape.com/")
# 3) wait
page.wait_for_selector(
"article.product_pod")
# 4) extract
title = page.locator(
"article.product_pod h3 a"
).first.get_attribute("title")
print(title)
browser.close()
🔑 두 코드의 import → launch → navigate → wait → extract → quit 순서는 동일. 함수 이름·인자 표기만 다름.
🎯 4단계는 동일합니다. API만 다른 방식이며, 한 도구를 익히면 다른 도구도 빠르게 학습할 수 있습니다.
💬 Antigravity / ChatGPT 입력 step01_login_save.py
📋 prompt prompts/ch05_prompts.md 📄 example examples/ch05/step01_login_save.py
Python 스크립트 step01_login_save.py 를 만들어 줘.
목적: Playwright 로 사람처럼 한 번 로그인한 뒤,
브라우저 인증 상태 (쿠키·로컬스토리지 포함) 를
auth.json 에 저장해 이후 스크립트가 재사용한다.
요구사항:
1. 라이브러리: playwright (동기 API), python-dotenv
2. 흐름:
1) load_dotenv() 로 .env 의 MY_ID, MY_PW 읽기
2) sync_playwright() 컨텍스트로 진입
3) p.chromium.launch(headless=False)
— 사람이 화면을 보며 CAPTCHA / 2차 인증 처리
4) browser.new_context(), ctx.new_page()
5) https://example.com/login 로 이동
6) page.fill("#username", ...)
page.fill("#password", ...)
page.click("#login-button")
7) page.wait_for_url("**/mypage**",
timeout=60_000)
— 사람이 2차 인증할 시간 확보
8) ctx.storage_state(path="auth.json") 저장
9) "로그인 상태를 auth.json 으로 저장했습니다."
출력 후 browser.close()
3. auth.json 경로는 현재 디렉토리.
보안 경고 주석 추가: 이 파일은 쿠키 포함,
.gitignore 에 등록 필수.
4. 에러 처리: 로그인 대기 타임아웃 시
예외 메시지 출력 후 정상 종료
환경: Python 3.11, venv. 코드만, 한국어 주석 최소.
headless=False 로 명시 — 사용자가 직접 CAPTCHA 및 2차 인증을 수행할 수 있도록 설정timeout=60_000 (60초) — OTP 입력 시간을 고려하여 60초로 설정storage_state(path="auth.json") — 쿠키와 로컬스토리지·세션스토리지를 모두 저장하여 다음 스크립트에서 로그인 없이 재사용.gitignore 안내 주석 추가 — auth.json 이 GitHub에 푸시되면 본인 계정이 탈취될 수 있으므로 반드시 .gitignore 에 추가합니다⚠️ 윤리·법적 책임:
1. 본인 계정과 본인 IP에서만 사용
2. 사이트 이용약관과robots.txt를 사전 확인
3. 요청 간격에 랜덤 지연(random.uniform(2, 4)초)을 추가하여 과도한 자동화 요청을 방지
회차 7과 8에서 추가 설명 예정입니다.
📖 출처:
ch05_prompts.md§5 — 로그인이 필요한 사이트.
90%의 셀레니움 에러 = wait 부족
| 종류 | 동작 | 권장 |
|---|---|---|
| Implicit (암묵적) | 모든 find 에 일괄 timeout | 시작 시 1회 |
| Explicit (명시적) | 특정 조건 충족까지 대기 | 주력 |
| Fluent (유연한) | polling (주기적으로 재확인) 간격까지 제어 | 까다로운 경우 |
# 1) 한 번 — 안전망
driver.implicitly_wait(5)
# 2) 매번 — 명시적
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, "button.submit")
)
)
# 3) 특수 — 갱신 폴링
WebDriverWait(driver, 30, poll_frequency=0.5).until(
lambda d: "loaded" in d.find_element(...).get_attribute("class")
)
⚠️
time.sleep(3)은 최후 수단. 빠른 PC 에선 충분, 느린 CI 에선 부족.
DOM = 브라우저가 만든 HTML 트리 객체. 페이지 로드 후 JS 가 추가하거나 수정 가능.
| 함수 | 언제 |
|---|---|
presence_of_element_located |
요소가 DOM 에 존재 (보일 필요 X) |
visibility_of_element_located |
요소가 눈에 보임 |
element_to_be_clickable |
클릭 가능 (보임 + 활성) |
text_to_be_present_in_element |
텍스트 등장 |
staleness_of |
요소가 사라짐 (페이지 전환) |
from selenium.webdriver.support import expected_conditions as EC
# "더 보기" 버튼이 클릭 가능해질 때까지
btn = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button.more"))
)
btn.click()
🎯 클릭은 visibility 가 아니라 clickable — 자주 까먹는 함정.
폼 입력 → 클릭 → 쿠키 추출 → requests

Set-Cookie: ... 헤더로 ID 전달📖 도서 참조: Ch.5 §3 — "세션과 쿠키, 그리고 그들의 사회생활"
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
import requests, json
# 1) 셀레니움으로 로그인 (사람 손처럼)
driver = webdriver.Chrome()
driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.NAME, "username"))).send_keys("YOUR_ID")
# 입력 필드는 visibility_of_element_located 가 의미상 정확
# (slide 8 참고 — element_to_be_clickable 은 버튼처럼 클릭 대상에)
driver.find_element(By.NAME, "password").send_keys("YOUR_PW")
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type=submit]"))).click()
# 로그인 완료 신호 — URL 변경 또는 본문 변화
WebDriverWait(driver, 10).until(EC.url_contains("/home"))
# 2) 쿠키 추출
sel_cookies = driver.get_cookies()
cookies = {c["name"]: c["value"] for c in sel_cookies}
driver.quit()
# 3) requests 로 빠르게
HEADERS = {"User-Agent": "Mozilla/5.0 ... Chrome/120.0"}
sess = requests.Session()
sess.headers.update(HEADERS)
sess.cookies.update(cookies)
r = sess.get("https://example.com/api/me", timeout=10)
print(r.json()) # 로그인된 사용자 정보
⚠️ 본인 계정 만. 약관 + 회차 7 윤리 체크리스트 통과 필수.
💬 ChatGPT / Claude 입력 step01_login_save.py + step02_use_auth.py
📋 prompt prompts/ch05_prompts.md 📄 example examples/ch05/step01_login_save.py
Selenium 으로 로그인 → 쿠키 저장 → requests 로 재사용하는
2단계 스크립트 두 개를 만들어 줘.
step01_login_save.py
1. selenium + webdriver-manager (Chrome) 로 /login 접속
2. ID 입력: visibility_of_element_located 후 send_keys
3. PW 입력 + 제출: element_to_be_clickable 후 click
4. 로그인 완료 신호: WebDriverWait + url_contains("/home")
(timeout 10s, 실패 시 명확한 메시지 후 종료)
5. driver.get_cookies() → {name: value} dict 변환
6. cookies.json 으로 저장 (UTF-8, indent=2)
7. driver.quit()
8. 자격증명은 .env 의 SITE_ID / SITE_PW 로 (python-dotenv)
step02_use_auth.py
1. cookies.json 을 json.load
2. requests.Session 만들고 sess.cookies.update(...)
3. 사람 User-Agent 1개 헤더에 추가
4. sess.get("/api/me") 응답 .json() 출력
5. cookies.json 없으면 step01 먼저 실행 안내
환경: Python 3.11, venv. 코드만, 한국어 주석 최소.
url_contains("/home") 빠지면 빈 쿠키 추출 함정cookies.json 분리 — .gitignore 추가 필수. PW 도 .env 로 분리💡 본인 계정 만 — 다음 슬라이드 (안티 봇) 와 회차 7 윤리 체크리스트 통과 사이트에서.
--disable-blink-features=AutomationControlled (navigator.webdriver 플래그를 숨김)⚠️ 상업적 자동 우회(서드파티 캡차 풀기 서비스, 봇팜, IP 풀)는 거의 모두 약관 위반 + 형사 위험. 연구는 본인 손이 가능한 범위 안에서.
도서 Ch.12 §1 그대로 따라가기

종목별 토론 글 리스트.
페이지네이션이 링크가 아닌 JS 로 동작 — 페이지 번호 클릭 시 자바스크립트가 데이터만 갈아끼움.

각 글이 한 행 (tr).
제목·작성자·날짜·조회수 컬럼.
본문은 상세 페이지 이동 후 로드.
🔑 두 단계 스크래핑: 리스트 → 상세 페이지 → 본문/댓글.
Chrome 콘솔의
$=document.querySelector(첫 1개),$$=document.querySelectorAll(전체, Array 반환) — DevTools 내장 단축 함수

$$("table.type2 tr").length
// → 토론 글 행 개수
$$("table.type2 a.tit")
.map(a => a.innerText)
// → 제목 리스트
$("a.next").href
// → 다음 페이지

페이지 클릭 시 숨은 GET 발견.
페이지네이션이 ?page=N 쿼리 변경.
→ requests 로 충분 (Selenium 불필요).
🎯 늘 그렇듯 — API/JSON 우선, Selenium 최후.
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
import pandas as pd, time
CODE = "005930" # 삼성전자
URL = f"https://finance.naver.com/item/board.naver?code={CODE}"
# 주의: 옛 .nhn 도메인은 2020년 폐기 → board.naver 사용
opts = webdriver.ChromeOptions()
opts.add_argument("--headless=new") # 화면 없이
opts.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=opts)
rows = []
try:
for page in range(1, 6):
driver.get(f"{URL}&page={page}")
WebDriverWait(driver, 8).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "table.type2 tr")))
# 실제 종목토론실 테이블 컬럼: 날짜 / 제목 / 글쓴이 / 조회 / 공감 / 비공감 (6열)
for tr in driver.find_elements(By.CSS_SELECTOR, "table.type2 tr"):
tds = tr.find_elements(By.TAG_NAME, "td")
if len(tds) < 6: continue
rows.append({
"date": tds[0].text.strip(),
"title": tds[1].text.strip(),
"writer": tds[2].text.strip(),
"views": tds[3].text.strip(),
"agree": tds[4].text.strip(),
"disagree": tds[5].text.strip(),
})
time.sleep(1.0) # 매너
finally:
driver.quit()
df = pd.DataFrame(rows)
df.to_csv(f"naver_discuss_{CODE}.csv", index=False, encoding="utf-8-sig")
🎯 약 15초 / 100건. 사람 손보다 30배 빠름, 매너 sleep 포함.
💬 ChatGPT / Claude 입력 coupang_selenium.py
📋 prompt prompts/ch05_prompts.md 📄 example examples/ch05/coupang_selenium.py
Selenium 으로 검색 결과 페이지 5장을 돌면서 상품 카드
정보를 100건 모아 CSV 로 저장하는 coupang_selenium.py
를 만들어 줘.
요구사항:
1. selenium + webdriver-manager (headful Chrome)
2. 검색어는 함수 인자, default "노트북"
3. 페이지 이동: URL 의 ?page=N 변경 (1..5)
4. 각 페이지 진입 후
WebDriverWait(driver, 10).until(
presence_of_all_elements_located(...))
5. 카드 셀렉터(.search-product 가정)는 모듈 상단의
상수로 분리 — 사이트 DOM 변경 시 한 줄만 수정
6. 카드별 추출: 제목 / 가격(\d 정규식) / 리뷰수(없으면 0)
/ 상품 링크 (a[href])
7. 카드별 try/except — 한 카드 깨져도 나머지 99개 보장,
skip 카운트 출력
8. CSV 저장: pandas.to_csv("coupang_<검색어>.csv",
index=False, encoding="utf-8-sig")
9. 페이지 사이 time.sleep(1.0) — 서버 부담 감소
종료 후:
- driver.quit() finally 블록
- 수집 / 스킵 카운트 콘솔 출력
환경: Python 3.11, venv. 코드만, 한국어 주석 최소.
utf-8-sig — 한국어 엑셀 호환 (BOM 자동) — 학회 발표용 시연 안전time.sleep(1.0) — 윤리 5계명의 느린 호출 . 차단 회피 + 서버 부담 ↓💡 무한 스크롤 사이트면
?page=N대신 다음 슬라이드의Keys.END반복 패턴으로 교체 — 끝 감지는prev_h == cur_h.
URL 이 안 바뀌는 페이지의 패턴
| 신호 | 코드 |
|---|---|
| 요소 개수가 안 늘어남 | prev_count == cur_count 두 번 연속 (한 번 같으면 로딩 지연 가능 — 두 번 같아야 끝) |
scrollHeight 가 안 변함 |
prev_h == cur_h (scrollHeight = 페이지 전체 높이 픽셀) |
| "더 이상 없음" 표시 | text_to_be_present_in_element |
| N건 도달 | len(items) >= TARGET |
from selenium.webdriver.common.keys import Keys
prev_h = 0
for _ in range(50): # 안전 상한
driver.find_element(By.TAG_NAME, "body").send_keys(Keys.END)
time.sleep(1.5)
cur_h = driver.execute_script("return document.body.scrollHeight")
if cur_h == prev_h:
break
prev_h = cur_h
items = driver.find_elements(By.CSS_SELECTOR, ".feed-item")
print(len(items))
📌 안전 상한 (
for _ in range(50)) 빠지면 무한 루프.
| 에러 | 원인 | 해결 |
|---|---|---|
NoSuchElementException |
아직 안 그려짐 | Explicit Wait |
ElementClickInterceptedException |
다른 요소가 가림 | el.scrollIntoView() (요소를 화면 안으로 스크롤) |
StaleElementReferenceException |
Stale = 페이지 갱신으로 옛 요소 참조가 무효화됨 | 다시 find |
TimeoutException |
wait 짧거나 셀렉터 오타 | timeout ↑ + 콘솔 검증 |
WebDriverException (드라이버) |
Chrome ↔ driver 버전 차이 | pip install -U selenium (Selenium 4.6+ 부터 Selenium Manager 가 driver 자동 관리 — 별도 ChromeDriver 다운로드 불필요) |
driver.save_screenshot("dbg.png") — 그 시점 화면driver.page_source — 현재 HTML--headless=new → 처음엔 빼고 눈으로 확인btn = driver.find_element(By.CSS_SELECTOR, "button.submit")
# ❌ 위에 sticky header 가 가리고 있을 때
btn.click() # ElementClickInterceptedException
# ✅ 1) 스크롤로 노출
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
time.sleep(0.3)
btn.click()
# ✅ 2) JS 직접 호출 (최후)
driver.execute_script("arguments[0].click();", btn)
🎯 두 번째는 사람 행동과 멀어지므로 최후 수단. 봇 검출 가능성.
대상:
https://makemyproject.net/shop/— 실서비스 경로 기반 동적 수집 실습
https://makemyproject.net/shop//shop/api/me)핵심 차이 — 요즘 쇼핑몰은 HTML 한 번 + JS 가 데이터를 따로 불러와 채우는 2단 구조
| 구분 | requests | 브라우저 자동화 |
|---|---|---|
| HTML 받기 | ✅ | ✅ |
| JS 실행 | ❌ | ✅ |
| 비동기 API 호출 | ❌ | ✅ |
| 결과 | 빈 골격 | 완성된 화면 |
정적 페이지 →
requests + BS4로 충분
동적·로그인 페이지 → Selenium / Playwright 가 필수
/shop 기준 인증 절차user123 / password1234)POST /shop/api/loginSet-Cookie로 세션 쿠키 발급GET /shop/api/me로 로그인 상태 확인실습 포인트: - 비로그인 20개 / 로그인 50개 차이 확인 - 로그인 후 페이지네이션/상세 접근 가능 확인
체크리스트: - [ ] 페이지 전환 wait 안정화 (요소 대기 적용) - [ ] 중복/누락 없는 50개 수집 확인 - [ ] CSV 결과를 분석·LLM 단계로 그대로 연결
본인 계정·본인 IP·합법적 트래픽 — 자동화의 3대 안전선.
회차 7 의 개인정보·윤리 주제로 자연스럽게 이어집니다.
"도구가 강력할수록 합법·매너·책임 의 무게도 커진다."