크롤러가 CSR 페이지를 못 읽는 이유

CSR·SSR·SSG 렌더링 방식과 브라우저 파이프라인

BrowserWebStandardRendering

난이도: ★★☆☆☆
연관 노트: 왜 서버는 이전 요청을 기억하지 않는가


핵심 요약

  1. WebFetch(HTTP GET)는 서버의 HTML 응답을 그대로 읽는다.
  2. CSR 방식 페이지는 초기 HTML이 빈 껍데기(<div id="root">)이고, JS 번들이 실행된 뒤 콘텐츠가 DOM에 채워진다.
  3. 크롤러는 JS를 실행하지 않으므로 콘텐츠를 볼 수 없다.
  4. 브라우저는 JS를 실행할 수 있기 때문에 최종 화면을 볼 수 있다.

왜 헷갈렸나

Claude Code의 WebFetch로 toss.im 아티클을 읽으려 했는데, 본문 대신 네비게이션만 반환됐다. 이유를 설명하다가 정리하게 된 개념.

크롤러와 브라우저가 “둘 다 URL로 페이지를 가져온다”는 점에서 동일하다고 생각했음. HTTP GET을 보내고 응답을 받는 것까지는 같지만, JS 실행 여부에서 완전히 갈린다.


메커니즘

HTTP GET → 서버 응답

┌─────────────────────────────┐
│ HTML (CSR 방식)              │
│ <body>                      │
│   <div id="root"></div>     │  ← 본문 없음
│   <script src="bundle.js"/> │
│ </body>                     │
└─────────────────────────────┘
          ↓                      ↓
  [크롤러 / WebFetch]       [브라우저]
  여기서 종료 (본문 없음)    bundle.js 다운로드

                           JS 실행 (React/Vue)

                           API 요청 → 응답

                           DOM에 콘텐츠 주입

                           사용자에게 보이는 화면

렌더링 방식별 차이

방식서버 응답 HTML크롤러/WebFetch
SSR완성된 HTML읽을 수 있다
SSG완성된 HTML읽을 수 있다
CSR빈 껍데기못 읽는다

SSR/SSG: 서버가 완성된 HTML을 응답한다. 크롤러가 GET만 해도 콘텐츠가 있다.
CSR: 서버는 빈 HTML + JS 번들만 내려준다. 콘텐츠는 클라이언트 JS 실행 후 생성된다.


해결 패턴

// CSR 페이지를 읽어야 할 때

// ❌ HTTP 클라이언트 — JS 실행 없음, 본문 못 읽음
const res = await fetch(url);
const html = await res.text(); // <div id="root"></div> 만 나옴

// ✅ Headless Browser — 실제 브라우저처럼 JS 실행
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
await page.waitForSelector('#root > *'); // JS가 DOM 채울 때까지 대기
const content = await page.content();   // 완성된 HTML
await browser.close();

Puppeteer/Playwright는 실제 Chromium을 띄워서 JS까지 실행한다. 단순 HTTP 클라이언트와 레벨이 다르다.


다음에 이 상황을 만나면

“크롤러/자동화 도구로 페이지를 읽어야 하는데 본문이 안 보인다” → 해당 페이지가 CSR인지 확인

  • 브라우저 개발자 도구 → Network 탭 → 첫 번째 HTML 응답 확인
  • 응답 본문에 콘텐츠가 있으면 SSR/SSG, <div id="root"> 만 있으면 CSR
  • CSR이면 Puppeteer/Playwright 사용

커넥팅 닷

← 선행 개념 (이걸 알아야 이해된다)

  • HTTP 요청·응답 사이클 — GET → 서버 응답 → HTML 파싱. 이걸 알아야 “왜 GET만으론 안 되는지” 이해됨
  • DOM이 무엇인가 — JS가 DOM을 동적으로 채운다는 게 이해되려면 DOM이 무엇인지 선행 필요
  • 왜 서버는 이전 요청을 기억하지 않는가 — HTTP가 stateless하고 요청마다 독립적이라는 기반 원리가 같음

→ 확장 개념 (여기서 더 나아가면)

  • Critical Rendering Path — 브라우저가 HTML → CSSOM → DOM → Render Tree → Paint 하는 전체 과정. CSR이 FCP가 느린 구조적 이유가 여기서 나옴
  • Core Web Vitals (FCP, LCP) — CSR 방식은 JS 실행 전까지 화면이 비어있어 FCP가 불리함. 렌더링 방식 선택 기준이 됨
  • SEO와 렌더링 전략 — 구글 봇도 JS 실행을 완전히 보장하지 않음. CSR 사이트의 SEO 한계와 Next.js가 SSR/SSG를 지원하는 이유
  • 대규모 트래픽에서 프론트엔드 설계는 진짜 달라지나? — 렌더링 방식 선택(CSR/SSR/ISR)이 스케일 환경에서 서버 비용과 사용자 경험에 미치는 실질적 영향
  • SSG에서 ?page=2가 동작하지 않는 이유 — SSG의 “서버 없음” 특성이 페이지네이션 설계 방식을 완전히 바꾼다는 실제 사례

↔ 같은 원리가 적용되는 곳

  • 구글 봇 크롤링 — WebFetch와 동일한 문제. CSR 페이지는 크롤러가 JS 실행 안 하면 인덱싱이 안 됨
  • React Hydration — SSR로 완성된 HTML을 내려보낸 뒤, 클라이언트에서 JS가 이벤트 핸들러를 붙이는 과정. “서버가 HTML 주고 클라이언트가 JS로 보강”이라는 방향이 같음

참고