크롤러 / 스크래퍼

크롤러 / 스크래퍼 아키텍처 및 도구 선택 가이드

동적 렌더링 vs 정적 수집 도구 매트릭스, DashCloud 헤드리스 Chromium 런타임 특장점과 장기 가동 파이프라인 설계.

업데이트: 2026-10-02

최신 웹 환경(React, Vue, Next.js 기반의 싱글 페이지 애플리케이션)에서 데이터를 주기적으로 수집하는 작업은 과거의 단순 HTML 파싱보다 훨씬 복잡한 인프라 요구조건을 수반합니다.

자바스크립트 동적 실행, 무한 스크롤, WAF 웹 방화벽의 봇 식별 장치, 그리고 브라우저를 장시간 켜두었을 때 발생하는 Chromium 메모리 누수를 극복하지 못하면, 크롤러는 며칠을 버티지 못하고 크래시됩니다.

DashCloud가 제공하는 크롤러 전용 런타임의 특징과, 대상 사이트의 특성에 맞춰 최적의 라이브러리를 선택하는 가이드를 상세히 다룹니다.


데이터 수집 도구별 기술 스택 매트릭스

모든 수집 작업에 무거운 헤드리스 브라우저를 띄울 필요는 없습니다. 대상 웹사이트의 데이터 렌더링 방식에 따라 적절한 도구를 조합하는 것이 서버 자원과 수집 속도를 극대화하는 비결입니다.

프레임워크 도구렌더링 엔진1회 수집 속도메모리 점유율자바스크립트 실행추천 수집 대상
PlaywrightChromium (헤드리스)보통 (0.8~2.5초)250~450MB완벽 실행 (동적 SPA)쇼핑몰 옵션가, 로그인 필요 사이트, 무한 스크롤
curl_cffi없음 (L7 C-클라이언트)초고속 (0.05~0.2초)20~40MB불가 (원시 HTML/JSON)일반 REST API, 차단 강도가 높은 정적 쇼핑몰
httpx / requests없음 (순수 파이썬)초고속 (0.05~0.2초)15~30MB불가 (원시 HTML/JSON)공공 데이터 포털, 단순 뉴스/게시판, 피드
Selenium웹드라이버 기반느림 (1.5~4.0초)400~700MB실행 가능레거시 파이프라인 유지보수 (신규 권장 안 함)
Playwright vs Selenium 선택 기준

신규 프로젝트라면 무조건 Playwright를 권장합니다. Selenium에 비해 실행 속도가 2~3배 빠르고, 네트워크 패킷 가로채기와 이미지/폰트 차단, 로그인 세션 저장이 파이썬 네이티브 비동기(async/await)로 훨씬 우아하게 지원됩니다.


DashCloud Chromium + Python 전용 런타임의 강점

DashCloud는 크롤러 개발자가 리눅스 가상머신을 세팅할 때 겪는 번거로운 종속성 문제를 해결한 crawler-chromium 런타임 프리셋을 기본 지원합니다.

  1. Chromium 브라우저 바이너리 기본 내장: 일반 도커나 VPS에서 playwright install을 실행할 때 수백 메가바이트의 브라우저를 다운로드하느라 빌드가 5분씩 걸리던 문제를 없앴습니다. 런타임 이미지에 최신 Chromium이 사전 빌드되어 10초 만에 즉시 배포됩니다.
  2. 리눅스 필수 C 라이브러리 완비: 우분투 환경에서 Chromium 구동에 필요한 20여 종의 핵심 공유 라이브러리(libx11-xcb, libasound2, libnss3 등)가 시스템 레벨에 완벽히 설치되어 있어 의존성 오류가 발생하지 않습니다.
  3. Noto Sans CJK 한글 폰트 기본 내장: 웹페이지 스크린샷이나 PDF 캡처 시 한글 텍스트가 네모 박스(□□□, 뛟뛟)로 깨지는 고질적인 리눅스 폰트 누락 문제를 원천 차단했습니다.
  4. 좀비 프로세스 자동 청소: 크롤러 스크립트가 비정상 종료되어도 백그라운드에 고립된 자식 Chromium 프로세스를 호스트 데몬이 감지하여 메모리를 즉시 회수합니다.

완성형 스타터 보일러플레이트 코드

crawler_playwright.py
import os
import sys
import logging
from pathlib import Path
from playwright.sync_api import sync_playwright
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("PlaywrightCrawler")
 
# 영구 저장소 디렉터리
OUTPUT_DIR = Path("/app/data/screenshots")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 
def crawl_dynamic_page(url: str):
    logger.info(f"Playwright 브라우저 가동 시작: {url}")
    with sync_playwright() as p:
        # 1. 헤드리스 Chromium 런칭
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-dev-shm-usage",  # 메모리 부족 방지
                "--disable-gpu"
            ]
        )
        
        # 2. 독립된 브라우저 컨텍스트 생성 (캐시/쿠키 격리)
        context = browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
        )
        
        page = context.new_page()
        try:
            # 3. 네트워크 레벨 이미지/폰트 차단 (메모리와 대역폭 80% 절감)
            # 스크린샷이 필요한 경우 route 차단을 해제하세요
            # page.route("**/*", lambda route: route.abort() if route.request.resource_type in ["image", "media"] else route.continue_())
 
            # 4. 페이지 이동 (DOM 렌더링 대기)
            page.goto(url, wait_until="domcontentloaded", timeout=30000)
            
            # 특정 데이터 셀렉터가 뜰 때까지 대기
            page.wait_for_selector("body", timeout=10000)
            
            # 페이지 타이틀 및 본문 텍스트 추출
            title = page.title()
            logger.info(f"수집 성공: 페이지 제목 = {title}")
            
            # 한글 스크린샷 캡처 (Noto Sans CJK 폰트 적용)
            screenshot_path = OUTPUT_DIR / "sample_capture.png"
            page.screenshot(path=str(screenshot_path), full_page=False)
            logger.info(f"스크린샷 저장 완료: {screenshot_path}")
 
        except Exception as e:
            logger.error(f"수집 실패: {e}")
            raise
        finally:
            # 5. [필수] 컨텍스트 및 브라우저 명시적 해제 (메모리 누수 방지)
            context.close()
            browser.close()
            logger.info("Chromium 프로세스가 안전하게 종료되었습니다.")
 
if __name__ == "__main__":
    crawl_dynamic_page("https://news.naver.com")

장기 무중단 크롤러 설계의 4대 원칙

  1. 컨텍스트 수명주기 격리: 단일 브라우저 인스턴스를 유지하더라도, 루프를 돌 때마다 context = browser.new_context()를 새로 생성하고 finally: context.close()로 닫아야 DOM 가비지가 디스크 힙에 쌓이지 않습니다.
  2. 불필요한 리소스 차단: CSS, 이미지, 폰트, 구글 애널리틱스 스크립트 전송을 가로채서 중단시키면 대역폭과 CPU 연산량이 70% 이상 절감됩니다.
  3. 가변 주기 스케줄링: 정확히 5.0초마다 요청을 보내는 기계적 패턴은 웹 방화벽의 봇 탐지기에 즉시 포착됩니다. 불규칙한 지연(랜덤 지터)을 두어 탐지를 회피하세요.
  4. 차분 감지 기반 저장: 모든 페이지 내용을 매번 디스크에 쓰지 말고, 직전 수집 결과와 가격이나 재고 상태가 달라졌을 때만 SQLite에 기록하여 스토리지 수명을 보존하세요.

관련 블로그


다음 추천 문서