Web crawling(웹 크롤링)과 web scraping(웹 스크레이핑)을 구분하고, 웹페이지의 데이터 제공 방식에 맞춰 가장 효율적인 수집 방법을 제시한다. 정적 페이지는 SSR(Server-Side Rendering, 서버사이드 렌더링)로 HTML에 데이터가 내장되어 있고, 동적 페이지는 CSR(Client-Side Rendering, 클라이언트사이드 렌더링) 또는 하이브리드로 JSON format의 API (Application Programming Interface) 응답을 받아 렌더링한다. DevTools(Network/Elements)를 통해 구조를 판별한 뒤, 정적은 requests + BeautifulSoup로, 동적은 Playwright·Puppeteer·Selenium 또는 직접 API 호출로 처리한다. 대규모 수집은 Scrapy로 파이프라인화하고, 배포는 Docker 컨테이너로 FaaS(Function as a Service, 함수형 서비스)나 PaaS(Platform as a Service, 플랫폼형 서비스)에 올리는 것이 효율적이다.

서론

핵심 질문은 다음과 같다.

1) 웹페이지가 데이터를 어디서 어떻게 공급하는가?

2) 그 구조에 가장 맞는 최소 비용·최대 안정성 도구 조합은 무엇인가?

답은 DevTools로 구조를 먼저 식별한 뒤, 구조에 맞춘 최소한의 스택으로 구현·확장하는 것이다.

본론 1: 분류 체계와 판별 절차

  • 기준 용어 정의
  • DevTools 판별 3단계
  • 보조 시그널

본론 2: 구조별 추천 도구·방법 매핑

  • 정적(SSR/SSG)
  • 동적(CSR/하이브리드)
  • 대체 경로
  • 상용/클라우드

본론 3: 의사결정 트리

  1. Elements에서 데이터가 보이는가? 보이면 requests + BeautifulSoup.
  2. Network에 JSON/XHR가 보이는가? 보이면 API 재현을 시도.
  3. 1·2가 막히면 Playwright로 렌더 후 DOM 또는 response interception으로 추출.
  4. 전역 크롤링이면 Scrapy로 링크 추출·큐·파이프라인 구현. 동적은 scrapy-playwright 병행.
  5. 반봇·레이트리밋 대응이 필요하면 세션 유지, 지수 백오프, 캐시, 존중 가능한 속도 설정. 합법성·robots.txt·약관 준수.

본론 4: 최소 구현 레시피 예시

  • 정적 페이지 파싱
python
import requests
from bs4 import BeautifulSoup

url = "https://example.com/page"
res = requests.get(url, timeout=20)
res.raise_for_status()
soup = BeautifulSoup(res.text, "lxml")

data = [e.get_text(strip=True) for e in soup.select("table#prices tr td:nth-child(2)")]
print(data)
  • Network에서 본 JSON API 재현
python
import httpx

api = "https://example.com/api/items"
headers = {
    "User-Agent": "Mozilla/5.0",
    "Accept": "application/json",
    "X-Requested-With": "XMLHttpRequest",
}
params = {"page": 1, "q": "keyword"}
with httpx.Client(timeout=20, headers=headers) as client:
    items = []
    while True:
        r = client.get(api, params=params)
        r.raise_for_status()
        payload = r.json()
        items.extend(payload["data"])
        if not payload.get("next_page"):
            break
        params["page"] += 1
print(len(items))
  • 스크립트 태그에 직렬화된 JSON 추출
python
import re, json
from bs4 import BeautifulSoup
import requests

html = requests.get("https://example.com").text
soup = BeautifulSoup(html, "lxml")
script = soup.find("script", string=re.compile("__NEXT_DATA__|__INITIAL_STATE__"))
obj = json.loads(script.string)
print(obj.keys())
  • Playwright로 동적 렌더링 및 XHR 가로채기
python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context()
    page = ctx.new_page()
    captured = []
    page.on("response", lambda resp: captured.append(resp) if "api" in resp.url else None)
    page.goto("https://example.com/list", wait_until="networkidle")
    html = page.content()
    data = [r.json() for r in captured if r.request.resource_type == "xhr"]
    browser.close()
    print(len(html), len(data))
  • Scrapy 스켈레톤
python
import scrapy

class ItemsSpider(scrapy.Spider):
    name = "items"
    start_urls = ["https://example.com/list"]

    def parse(self, response):
        for href in response.css("a.item::attr(href)").getall():
            yield response.follow(href, self.parse_item)

    def parse_item(self, response):
        yield {
            "title": response.css("h1::text").get(),
            "price": response.css(".price::text").get(),
        }
  • scrapy-playwright 설정 요지
python
# settings.py
DOWNLOADER_MIDDLEWARES = {"scrapy_playwright.middleware.ScrapyPlaywrightDownloaderMiddleware": 543}
PLAYWRIGHT_BROWSER_TYPE = "chromium"
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000

본론 5: 신뢰성·확장성 체크리스트

  • 정확성
  • 성능
  • 유지보수
  • 합법·윤리

본론 6: 상용 서비스와 자체 호스팅 비교 요약

  • Apify: 빠른 시작, 4,000+ Actor. 비용 증가와 커스터마이징 제약이 단점.
  • ThunderBit: 유사 FaaS. 비용·기능 비교 필요.
  • 자체 호스팅 권장 패턴

결론

데이터 전송 방식 판별이 먼저이고 그에 따른 도구 선택은 그 다음에 결정한다. SSR/SSG는 requests + BeautifulSoup, CSR/하이브리드는 API 재현 우선, 필요 시 Playwright. 전역·대량은 Scrapy로 파이프라인화하고, 배포는 컨테이너 기반 FaaS/PaaS로 표준화한다. DevTools 중심의 구조 판별과 최소 스택 원칙이 비용·안정성을 동시에 달성하는 최적 경로다.