Selenium · Playwright 내부 구조와 사이트 판별법
크롤링을 하다 보면 반드시 만나는 벽이 있다.
Document doc = Jsoup.connect(url).get();
doc.select(".product-price"); // → 비어 있음브라우저에서는 잘 보이는데, Jsoup으로는 아무것도 안 나온다. 이때 등장하는 게 브라우저 자동화 도구들이다.
Selenium
Playwright
Puppeteer
이 글은 이 도구들이 어떻게 브라우저를 제어하는지 내부까지 파고, Jsoup으로 되는 사이트와 안 되는 사이트를 어떻게 판별하는지 정리한다.
시나리오는 그대로 이어간다.
예시 시나리오 예시 수집기의
jsoupCrawlStep이 특정 사이트에서 빈 결과만 뱉는다. 원인을 파악하고browserCrawlStep(Playwright)를 추가해야 한다.
Headless Browser가 뭘까
먼저 이 개념부터 정리하고 가자.
Headless Browser는 화면(head)이 없는 브라우저다. GUI를 그리지 않을 뿐, 내부적으로는 완전한 브라우저다.
일반 Chrome = 렌더링 엔진 + JS 엔진 + 네트워크 + GPU/화면 그리기 + UI
Headless Chrome = 렌더링 엔진 + JS 엔진 + 네트워크 + (화면은 메모리 안에서만)
- HTML 파싱, CSS 계산, JS 실행, 네트워크 요청은 똑같이 한다.
- 다만 픽셀을 실제 모니터에 그리지 않는다. 필요하면 메모리 버퍼에 그린 뒤 스크린샷으로 뽑을 수 있다.
- 그래서 CPU/메모리 소모가 일반 크롬보다 조금 적다. 하지만 여전히 무겁다.
왜 서버에서 이걸 쓰나?
- 서버에는 모니터가 없다. 일반 크롬을 띄우려면 가상 디스플레이(Xvfb 등)가 필요.
- Headless는 그 필요 없이 그냥 프로세스로 돌린다.
JavaScript 렌더링이 왜 필요한가
이 질문의 답이 브라우저 자동화가 존재하는 이유다.
옛날 웹은 이랬다.
[서버] → 완성된 HTML → [브라우저]
서버가 데이터를 채운 HTML을 다 만들어서 보내줬다. Jsoup로 GET만 하면 끝.
지금 웹의 상당수는 이렇다.
[서버] → 빈 HTML + JS 번들 → [브라우저]
│
▼
[JS 실행]
│
▼
[API 호출로 데이터 fetch]
│
▼
[DOM에 삽입]
│
▼
사용자 눈에 보이는 화면
Jsoup은 첫 번째 화살표까지만 본다. JS를 실행하지 않으므로, 두 번째 이후는 아예 못 본다.
브라우저 자동화 도구는 진짜 브라우저를 띄우기 때문에 JS 실행 결과까지 다 본다. 대신 무겁다. 이게 트레이드오프의 본질이다.
Selenium은 어떻게 브라우저를 제어하나
Selenium의 구조는 3계층이다.
┌──────────────────────────────┐
│ 내 코드 (Java) │
│ driver.get(url) │
│ driver.findElement(...) │
└──────────────┬───────────────┘
│ Selenium Java Client
│ (HTTP 요청으로 명령 전송)
▼
┌──────────────────────────────┐
│ WebDriver 서버 │
│ (예: ChromeDriver 프로세스) │
└──────────────┬───────────────┘
│ 브라우저별 자동화 API
│ (Chrome DevTools Protocol 등)
▼
┌──────────────────────────────┐
│ 실제 브라우저 (Chrome) │
└──────────────────────────────┘
WebDriver 프로토콜
핵심은 WebDriver Protocol이다. W3C 표준. HTTP 기반 REST-like 프로토콜이다.
POST /session/{id}/url → 페이지 이동
GET /session/{id}/element → 요소 찾기
POST /session/{id}/element/{elId}/click → 클릭
POST /session/{id}/execute/sync → JS 실행
내 코드가 driver.get(url)을 호출하면, Selenium Java Client가 HTTP 요청을 만들어서 ChromeDriver에게 보낸다. ChromeDriver는 그걸 받아서 실제 Chrome에 명령을 내린다.
이 구조 덕에 언어(Java/Python/JS) 무관, 브라우저(Chrome/Firefox/Safari) 무관하게 동작한다.
왜 느리고 불안정한가
두 가지 이유.
- HTTP over 로컬 소켓 — 명령마다 HTTP 요청이 오간다. Chrome DevTools Protocol을 직접 쓰는 도구에 비해 오버헤드가 있다.
- 대기(wait) 처리가 수동 —
driver.findElement(...)는 요소가 없으면 바로 예외를 던진다. 요소가 나타날 때까지 기다리려면WebDriverWait을 명시적으로 걸어야 한다. 이걸 빼먹으면 flaky test로 이어진다.
Playwright는 어떻게 다른가
Playwright의 구조는 이렇다.
┌──────────────────────────────┐
│ 내 코드 (Java) │
└──────────────┬───────────────┘
│ Playwright Client
│
▼
┌──────────────────────────────┐
│ Playwright Server (Node.js) │ ← 내부적으로 Node.js 프로세스
└──────────────┬───────────────┘
│ CDP (Chrome DevTools Protocol)
│ 또는 자체 프로토콜 (Firefox/WebKit)
▼
┌──────────────────────────────┐
│ 브라우저 (Chromium/FF/WK) │
└──────────────────────────────┘
CDP를 씀 (Chromium 계열)
Chrome DevTools Protocol은 크롬 개발자 도구가 브라우저 내부와 통신할 때 쓰는 프로토콜이다. WebSocket 기반, JSON-RPC.
WebDriver보다 훨씬 저수준이고 강력하다.
- 네트워크 요청을 가로채고 수정 가능
- 브라우저 이벤트를 실시간 구독
- 명령 왕복이 WebSocket이라 지연 낮음
Selenium 4부터는 Selenium도 CDP를 부분 지원한다. 다만 기본 프로토콜은 여전히 WebDriver.
자동 대기(Auto-waiting)
Playwright의 킬러 기능이다.
page.click(".submit"); // 요소가 나타나고, 화면에 보이고, 클릭 가능해질 때까지 자동 대기Selenium의:
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".submit")))
.click();같은 동작이 한 줄이다. flaky의 주된 원인인 wait 누락을 프레임워크가 처리해준다.
다른 특징들
- Context 격리:
browser.newContext()로 쿠키·세션이 완전히 분리된 세션을 만들 수 있다. 여러 계정으로 병렬 크롤링에 유리. - Network 가로채기:
page.route(...)로 특정 요청을 가로채 mock 응답을 주거나 로깅 가능. - Trace viewer: 실행 전체를 녹화해서 나중에 GUI로 리플레이 가능. 디버깅에 강력.
Playwright vs Puppeteer
거의 같은 뿌리(Google Puppeteer 팀의 사람들이 Microsoft로 옮겨서 Playwright를 만들었다). 차이는:
| 항목 | Puppeteer | Playwright |
|---|---|---|
| 언어 | Node.js 공식만 | Node/Python/Java/.NET |
| 브라우저 | Chrome(+실험적 Firefox) | Chromium/Firefox/WebKit 모두 정식 |
| Auto-wait | 부분적 | 강력 |
| 격리 컨텍스트 | Incognito context | Browser context (더 유연) |
Java/Spring Boot 백엔드에서는 사실상 Playwright(Java) 가 답이다. Puppeteer를 쓰려면 별도 Node.js 서비스를 세워야 한다.
Selenium vs Playwright (Java 관점)
| 항목 | Selenium | Playwright |
|---|---|---|
| 프로토콜 | WebDriver (HTTP) + CDP 부분 지원 | CDP / 자체 (WebSocket) |
| 자동 대기 | 수동 (WebDriverWait) | 자동 |
| 성능 | 보통 | 대체로 더 빠름 |
| Network 가로채기 | 제한적 | 강력 |
| 병렬 실행 | 세팅 복잡 | BrowserContext로 자연스러움 |
| 브라우저 지원 | Chrome/FF/Safari/Edge | Chromium/FF/WebKit |
| 커뮤니티 | 방대. 자료 많음 | 성장 중. 공식 문서 좋음 |
| Java 성숙도 | 아주 성숙 | 정식 지원, 실무 사용 가능 |
| E2E 테스트 뿌리 | 가장 오래된 표준 | Playwright Test로 급성장 |
결론(2026 시점): 새로 시작하면 Playwright, 기존이면 굳이 안 바꿔도 됨. 성능·안정성·개발자 경험은 Playwright가 우위.
사이트가 Jsoup으로 되는지 판별하는 법
크롤링 실무에서 가장 자주 물어보는 것 하나. "이 사이트, Jsoup으로 될까요?"
3단계 체크리스트로 정리하면 된다.
1단계: curl로 응답 HTML 확인
curl -A "Mozilla/5.0" https://example.com/list > page.html- 응답에 원하는 데이터가 텍스트로 포함되어 있으면 → Jsoup 가능. 여기서 끝내야 한다.
- 데이터가 없으면 → 2단계로.
Jsoup는 결국 이 HTML을 파싱하는 것과 같다. curl로 안 나오면 Jsoup으로도 안 나온다.
2단계: 브라우저 개발자 도구 → Network 탭
브라우저에서 페이지를 새로고침하고 Network 탭을 본다. XHR/Fetch 필터.
- 데이터를 채우는 내부 API 호출이 보이는가?
- 예:
GET /api/v1/products?page=1이 JSON을 리턴하고, 그 JSON이 페이지에 렌더링됨.
이게 보이면 → 브라우저 자동화 대신 그 API를 직접 호출해라. 훨씬 빠르고 안정적이고 파싱 필요 없음.
이때 필요한 것:
- 인증 헤더 (Cookie, Authorization)
- CSRF 토큰
- User-Agent 등 필수 헤더
브라우저 요청을 "Copy as cURL"로 복사해서 그대로 재현해보고, 최소 헤더만 남기며 실험한다.
3단계: 진짜 SPA — 브라우저 자동화
여기까지 안 되면 답은 하나. Selenium/Playwright.
이런 사이트의 특징:
- API 응답이 있는 게 아니라, 페이지 안에서 JS로 계산·조립됨.
- Shadow DOM, iframe이 얽혀 있음.
- 캡차/봇 탐지가 있어 헤더 조작만으로는 뚫리지 않음.
이 경우 Playwright를 쓰되, 비용을 반드시 계산하자.
- 브라우저 인스턴스 1개 = 대략 100~300MB 메모리
- 페이지 로드 = 수 초
- 100개 사이트 병렬 크롤 = Chromium 100개 프로세스
예시 수집기에 붙이기
앞선 글에서 정리한 구조는 이랬다.
[Batch Job: dataCollectionJob]
├── Step: apiCollectStep ── API 호출
├── Step: jsoupCrawlStep ── 정적 HTML
└── Step: browserCrawlStep ── JS 렌더링
browserCrawlStep을 Playwright로 붙일 때 신경 쓸 것들:
브라우저 생명주기 관리
절대 하지 말 것: 요소 하나 처리마다 브라우저 새로 띄우기. 시작 비용이 감당 안 됨.
권장: 애플리케이션 시작 시 브라우저 인스턴스를 하나 띄우고 재사용. BrowserContext를 요청 단위로 새로 만들어 세션만 격리.
@Component
public class PlaywrightHolder implements DisposableBean {
private final Playwright playwright = Playwright.create();
private final Browser browser = playwright.chromium().launch();
public Browser browser() { return browser; }
@Override
public void destroy() {
browser.close();
playwright.close();
}
}Chunk size는 작게
jsoupCrawlStep이 Chunk 20이었다면, browserCrawlStep은 2~5 정도로 잡는다. 병렬 브라우저 컨텍스트 수가 곧 메모리 사용량이다.
타임아웃과 재시도
- 페이지 로드 타임아웃: 15~30초. 무한대기 절대 금지.
- 재시도: 네트워크 오류는 Batch의
.retry()로. 페이지 자체가 이상하면 skip. - 정말 오래 걸리는 사이트는 아예 별도 Step으로 분리해 격리.
리소스 격리
프로덕션이라면 브라우저 자동화 Step은 별도 Pod/서비스로 빼는 것도 고려. 크롬이 죽거나 메모리를 다 먹어도 API 수집기 본체는 살아있게.
정리
- Headless Browser는 화면만 없을 뿐 완전한 브라우저다. JS 실행까지 정상 동작.
- 현대 웹의 상당수는 JS로 데이터를 렌더링하므로, Jsoup만으로는 부족한 경우가 많다.
- Selenium은 WebDriver 프로토콜(HTTP) 기반. 오래됐고 표준. 대기 처리는 수동.
- Playwright는 CDP 기반. 자동 대기, 네트워크 가로채기, 컨텍스트 격리가 강점.
- Java/Spring Boot 백엔드에서 새로 시작한다면 Playwright(Java) 가 무난.
- 사이트 판별은 3단계: curl → Network 탭 → 그래도 안 되면 브라우저 자동화.
- API 직접 호출로 우회 가능하면 그게 최선. 브라우저 자동화는 마지막 카드.
- 예시 수집기에서는 브라우저 인스턴스 재사용, 작은 Chunk size, 별도 격리가 핵심.
브라우저 자동화는 강력하지만 비싼 도구다. 필요할 때만, 최소한으로 쓰는 게 실무의 정석이다.
다음 글에서는 크롤링을 오래 지속하기 위해 반드시 짚어야 할 법·윤리·안티봇 대응을 정리한다.