데이터 크롤링

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이었다면, browserCrawlStep2~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, 별도 격리가 핵심.

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

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