데이터 크롤링

Selenium · Playwright 내부 구조와 사이트 판별법

2026. 08. 05.
SeleniumPlaywrightCrawlingHeadless

크롤링을 하다 보면 반드시 만나는 벽이 있다.

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) 무관하게 동작한다.

왜 느리고 불안정한가

두 가지 이유.

  1. HTTP over 로컬 소켓 — 명령마다 HTTP 요청이 오간다. Chrome DevTools Protocol을 직접 쓰는 도구에 비해 오버헤드가 있다.
  2. 대기(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를 만들었다). 차이는:

항목PuppeteerPlaywright
언어Node.js 공식만Node/Python/Java/.NET
브라우저Chrome(+실험적 Firefox)Chromium/Firefox/WebKit 모두 정식
Auto-wait부분적강력
격리 컨텍스트Incognito contextBrowser context (더 유연)

Java/Spring Boot 백엔드에서는 사실상 Playwright(Java) 가 답이다. Puppeteer를 쓰려면 별도 Node.js 서비스를 세워야 한다.


Selenium vs Playwright (Java 관점)

항목SeleniumPlaywright
프로토콜WebDriver (HTTP) + CDP 부분 지원CDP / 자체 (WebSocket)
자동 대기수동 (WebDriverWait)자동
성능보통대체로 더 빠름
Network 가로채기제한적강력
병렬 실행세팅 복잡BrowserContext로 자연스러움
브라우저 지원Chrome/FF/Safari/EdgeChromium/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, 별도 격리가 핵심.

브라우저 자동화는 강력하지만 비싼 도구다. 필요할 때만, 최소한으로 쓰는 게 실무의 정석이다.

다음 글에서는 크롤링을 오래 지속하기 위해 반드시 짚어야 할 법·윤리·안티봇 대응을 정리한다.