2 / 2

02: CSR과 SSR의 차이 — 브라우저가 그리냐, 서버가 그리냐

예상 시간: 5분

02: CSR과 SSR의 차이 — 브라우저가 그리냐, 서버가 그리냐

이 문서에서 배우는 것

  • CSR(Client-Side Rendering)의 동작 원리: 빈 HTML + JS 번들
  • SSR(Server-Side Rendering)의 동작 원리: 완성된 HTML + Hydration
  • 초기 로딩 속도 비교
  • 검색엔진 봇이 각각에서 보는 것
  • SSR이 필요한 경우와 불필요한 경우

PM 요청 (김도연)

김도연 PM: 준혁님, 01편에서 Flask로 React 빌드 파일을 서빙하는 법을 배웠는데요. 그런데 다른 팀에서 "SSR을 해야 한다"고 하더라고요. CSR이니 SSR이니 하는 게 정확히 뭔가요?

이준혁 시니어: 오, 드디어 핵심 질문이 나왔네! 01편에서 배운 "빌드 파일을 서빙하는 방식"이 바로 CSR이야. SSR은 완전히 다른 접근이지. 둘을 비교해보자.


시니어 멘토링 (이준혁)

질문 1: 01편에서 Flask가 서빙한 HTML, 열어본 적 있어?

이준혁: 01편에서 Flask가 서빙한 React의 index.html 파일을 직접 열어본 적 있어?

김도연: 아니요, 그냥 브라우저에서 잘 보이길래...

이준혁: 한번 열어봐.

<!-- React 빌드 결과물: dist/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <script type="module" crossorigin src="/assets/index-CdTgQbWo.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-DiwrgTda.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

김도연: ...? <div id="root"></div> 밖에 없어요? 내용이 하나도 없는데요?

이준혁: 맞아! 이게 CSR의 본질이야. HTML에는 내용이 없어. 전부 JavaScript가 그려.


CSR: 브라우저가 화면을 그리는 방식

이준혁: CSR의 전체 흐름을 보자.

┌──────────────────────────────────────────────────────────────┐
│                    CSR (Client-Side Rendering)                │
│                                                                │
│  1. 브라우저 → 서버: "/ 페이지 주세요"                        │
│                                                                │
│  2. 서버 → 브라우저: 빈 HTML 전송                             │
│     ┌─────────────────────────────────┐                       │
│     │  <div id="root"></div>          │  ← 내용 없음!        │
│     │  <script src="bundle.js"/>      │  ← JS 번들 링크만    │
│     └─────────────────────────────────┘                       │
│                                                                │
│  3. 브라우저: "HTML 받았다... 근데 아무것도 없네?"            │
│     → 사용자에게 빈 화면 또는 로딩 스피너 표시                │
│                                                                │
│  4. 브라우저: bundle.js 다운로드 시작 (500KB ~ 2MB)           │
│     → 네트워크 속도에 따라 1~5초 소요                         │
│                                                                │
│  5. 브라우저: JS 실행 → React가 DOM 생성                      │
│     → document.getElementById('root') 안에 HTML 삽입          │
│     → 사용자에게 완성된 화면 표시!                             │
│                                                                │
│  6. 브라우저: API 호출 → 데이터 가져오기                       │
│     → fetch('/api/users') → 결과 받아서 화면 업데이트          │
└──────────────────────────────────────────────────────────────┘

김도연: 그러니까... 처음에 빈 화면이 나오고, JS가 다 다운로드되어야 뭔가 보이는 거예요?

이준혁: 정확해! CSR에서 사용자가 보는 순서는:

시간 →
 
0초          1초          2초          3초
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│         │  │         │  │ Loading │  │ Hello!  │
│  빈     │  │  빈     │  │   ...   │  │ 사용자  │
│  화면   │  │  화면   │  │         │  │ 목록    │
│         │  │         │  │         │  │ - Alice │
└─────────┘  └─────────┘  └─────────┘  └─────────┘
 HTML 수신    JS 다운로드   JS 실행       API 완료
              중...        React 렌더링   데이터 표시

SSR: 서버가 화면을 그리는 방식

이준혁: 이번엔 SSR을 보자. SSR은 서버가 완성된 HTML을 만들어서 보내는 것이야.

┌──────────────────────────────────────────────────────────────┐
│                    SSR (Server-Side Rendering)                 │
│                                                                │
│  1. 브라우저 → 서버: "/ 페이지 주세요"                        │
│                                                                │
│  2. 서버: React 컴포넌트를 실행해서 HTML 문자열 생성          │
│     const html = renderToString(<HomePage />)                  │
│                                                                │
│  3. 서버 → 브라우저: 완성된 HTML 전송                         │
│     ┌─────────────────────────────────┐                       │
│     │  <h1>Hello!</h1>               │  ← 내용이 있음!       │
│     │  <ul>                          │                        │
│     │    <li>Alice</li>              │  ← 데이터도 포함      │
│     │    <li>Bob</li>                │                        │
│     │  </ul>                         │                        │
│     │  <script src="bundle.js"/>     │  ← JS 번들도 포함     │
│     └─────────────────────────────────┘                       │
│                                                                │
│  4. 브라우저: "HTML 받았다! 바로 보여주자!"                   │
│     → 사용자에게 즉시 완성된 화면 표시                        │
│     → 단, 버튼 클릭 등은 아직 안 됨 (JS 로드 전)             │
│                                                                │
│  5. 브라우저: bundle.js 다운로드 + 실행 (Hydration)           │
│     → 정적 HTML에 이벤트 핸들러 연결                          │
│     → 이제 버튼 클릭도 됨!                                     │
└──────────────────────────────────────────────────────────────┘

이준혁: SSR에서 사용자가 보는 순서는:

시간 →
 
0초          0.5초        1초          2초
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│ Hello!  │  │ Hello!  │  │ Hello!  │  │ Hello!  │
│ 사용자  │  │ 사용자  │  │ 사용자  │  │ 사용자  │
│ 목록    │  │ 목록    │  │ 목록    │  │ 목록    │
│ - Alice │  │ - Alice │  │ - Alice │  │ - Alice │
└─────────┘  └─────────┘  └─────────┘  └─────────┘
 HTML 수신    화면 보임     JS 다운로드   Hydration
 즉시 표시!   (클릭 안됨)   완료          완료(인터랙티브)

김도연: 와! SSR은 처음부터 화면이 보이는 거예요?

이준혁: 맞아! 서버가 이미 완성된 HTML을 보내니까, 브라우저는 JS를 기다리지 않고 바로 표시할 수 있어.


질문 2: Hydration이 뭐야?

김도연: 근데 "Hydration"이라는 게 뭐예요? SSR에서 JS가 나중에 하는 거라고 했는데...

이준혁: Hydration은 **"정적 HTML에 생명을 불어넣는 것"**이야.

Hydration 전:  HTML은 보이지만 "그림"일 뿐 — 클릭해도 반응 없음
Hydration 후:  HTML에 이벤트 핸들러가 연결됨 — 클릭하면 반응함

구체적으로 보면:

// 서버에서 만든 HTML (정적)
<button>좋아요 0</button>   // ← 그냥 텍스트. 클릭해도 아무 일 없음
 
// Hydration 후 (인터랙티브)
<button onclick="...">좋아요 0</button>  // ← 이벤트 핸들러 연결됨!
// React에서의 Hydration 코드
import { hydrateRoot } from 'react-dom/client';
import App from './App';
 
// 서버가 만든 HTML에 React를 "주입"
hydrateRoot(document.getElementById('root'), <App />);
// → 기존 HTML은 그대로 두고, 이벤트 핸들러만 연결

이준혁: 비유하면:

SSR = 집을 지어서 완성된 상태로 배달 Hydration = 배달된 집에 전기와 수도를 연결하는 것

집(HTML)은 이미 있지만, 전기(이벤트)와 수도(상태)가 연결되어야 "살 수 있는 집"이 된다.


질문 3: 둘의 초기 로딩 속도는 얼마나 차이나?

김도연: 실제로 CSR이 얼마나 느린 거예요?

이준혁: 직접 비교해보자.

CSR vs SSR 초기 로딩 타임라인

CSR (빈 HTML + 500KB JS 번들):
─────────────────────────────────────────────
0ms   HTML 수신 (1KB)

100ms ┤ 빈 화면 표시 (또는 로딩 스피너)

      │ ← JS 번들 다운로드 중 (500KB) ──→

800ms ┤ JS 다운로드 완료

      │ ← JS 파싱 + 실행 중 ──→

1200ms┤ React 렌더링 완료 → 화면 표시!

      │ ← API 호출 + 응답 대기 ──→

1800ms┤ 데이터 수신 → 최종 화면 완성
─────────────────────────────────────────────
FCP (First Contentful Paint): ~1200ms
TTI (Time to Interactive):    ~1200ms
 
 
SSR (완성된 HTML + 500KB JS 번들):
─────────────────────────────────────────────
0ms   서버에서 렌더링 중 (50~200ms)

200ms ┤ 완성된 HTML 수신 (15KB)

250ms ┤ 화면 즉시 표시! (데이터 포함)

      │ ← JS 번들 다운로드 중 (500KB) ──→

1050ms┤ JS 다운로드 완료

      │ ← Hydration 중 ──→

1300ms┤ Hydration 완료 → 인터랙티브!
─────────────────────────────────────────────
FCP (First Contentful Paint): ~250ms
TTI (Time to Interactive):    ~1300ms

이준혁: 핵심 차이를 보자:

지표CSRSSR
FCP (첫 콘텐츠 표시)~1200ms (JS 실행 후)~250ms (HTML 수신 즉시)
TTI (인터랙션 가능)~1200ms (같은 시점)~1300ms (Hydration 후)
빈 화면 시간~1200ms~0ms
데이터 표시1800ms (API 호출 후)250ms (HTML에 포함)

김도연: SSR은 화면이 빨리 보이지만 클릭이 늦게 되고, CSR은 화면이 늦게 보이지만 바로 클릭할 수 있는 거네요?

이준혁: 정확해! 그게 바로 트레이드오프야.

CSR:  화면 늦게 보임 → 바로 인터랙티브
      ────────────────X══════════════════
 
SSR:  화면 빨리 보임 → 잠시 비인터랙티브 → 인터랙티브
      ──X──────────────────────X═════════
 
X = 화면 표시 시점
─ = 비인터랙티브
═ = 인터랙티브

질문 4: 검색엔진 봇이 보는 것은?

김도연: SEO 때문에 SSR을 해야 한다는 얘기를 들었는데, 왜 그런 거예요?

이준혁: 검색엔진 봇(Google, Naver 등)이 페이지를 수집할 때 무엇을 보는지 비교해보면 이해가 돼.

CSR에서 검색엔진 봇이 보는 것

<!-- Google 봇이 받는 HTML -->
<!DOCTYPE html>
<html>
  <head><title>My App</title></head>
  <body>
    <div id="root"></div>
    <script src="/assets/bundle.js"></script>
  </body>
</html>
 
<!-- 봇이 인식하는 내용: "이 페이지는 비어있다" -->

이준혁: Google 봇은 이제 JavaScript를 실행할 수 있지만, 몇 가지 문제가 있어:

┌─────────────────────────────────────────────────────────────┐
│  검색엔진 봇의 한계                                          │
│                                                               │
│  1. JS 실행에 추가 리소스 필요 → 크롤링 예산(crawl budget) ↑  │
│  2. 모든 봇이 JS를 실행하지는 않음 (Naver, Bing 일부)       │
│  3. JS 실행 결과를 인덱싱하기까지 시간 지연 (최대 며칠)      │
│  4. JS 에러 시 빈 페이지로 인덱싱                            │
└─────────────────────────────────────────────────────────────┘

SSR에서 검색엔진 봇이 보는 것

<!-- Google 봇이 받는 HTML -->
<!DOCTYPE html>
<html>
  <head>
    <title>My App - 사용자 목록</title>
    <meta name="description" content="Alice, Bob, Charlie의 프로필을 확인하세요">
  </head>
  <body>
    <div id="root">
      <h1>사용자 목록</h1>
      <ul>
        <li>Alice - 개발자</li>
        <li>Bob - 디자이너</li>
        <li>Charlie - PM</li>
      </ul>
    </div>
    <script src="/assets/bundle.js"></script>
  </body>
</html>
 
<!-- 봇이 인식하는 내용: "사용자 목록 페이지. Alice, Bob, Charlie 프로필." -->

김도연: CSR은 봇이 빈 페이지를 보는 거고, SSR은 완성된 페이지를 보는 거네요!

이준혁: 맞아! 그래서 SEO가 중요한 페이지는 SSR을 쓰는 거야.


질문 5: SSR이 필요한 경우 vs 불필요한 경우

김도연: 그럼 모든 페이지를 SSR로 해야 하나요?

이준혁: 아니! 상황에 따라 다르지. 이걸 보자.

SSR이 필요한 경우

상황이유
공개 블로그/뉴스검색엔진에 노출되어야 함
제품 랜딩 페이지첫 인상이 중요, 빠른 로딩 필요
E-commerce 상품 페이지상품이 검색에 잡혀야 매출
마케팅 페이지SNS 공유 시 미리보기(OG 태그) 필요
콘텐츠 중심 사이트SEO + 빠른 초기 로딩

SSR이 불필요한 경우

상황이유
관리자 대시보드로그인해야 접근, 검색 노출 불필요
내부 툴직원만 사용, SEO 무관
SaaS 앱 (로그인 후)개인 데이터, 검색 노출 안 됨
실시간 채팅/게임초기 HTML보다 실시간 인터랙션이 중요
프로토타입/MVP빠른 개발이 우선, SEO는 나중에

이준혁: 정리하면:

┌─────────────────────────────────────────────────────────┐
│                                                           │
│   "누구나 볼 수 있는 공개 페이지"  →  SSR 고려           │
│   "로그인해야 보는 비공개 페이지"  →  CSR로 충분         │
│                                                           │
└─────────────────────────────────────────────────────────┘

김도연: 아! 그러면 하나의 사이트 안에서도 페이지마다 다를 수 있는 거예요?

이준혁: 완전 맞아! 실제로 많은 사이트가 이렇게 해:

내 쇼핑몰 사이트:
─────────────────────────────────────────
/                     → SSR (랜딩 페이지, SEO 중요)
/products/:id         → SSR (상품 페이지, 검색 노출 필요)
/blog/:slug           → SSR (블로그, SEO 중요)
/login                → CSR (로그인 폼, SEO 불필요)
/dashboard            → CSR (개인 대시보드, 비공개)
/admin                → CSR (관리자 페이지, 비공개)
/checkout             → CSR (결제 페이지, 비공개)

Next.js 같은 프레임워크는 이렇게 페이지별로 CSR/SSR을 선택할 수 있게 해줘.


CSR vs SSR 코드 비교

이준혁: 실제 코드가 어떻게 다른지 보여줄게.

CSR: React + Vite (빌드 → 정적 파일)

// src/App.jsx — CSR
import { useState, useEffect } from 'react';
 
export default function App() {
  const [users, setUsers] = useState([]);  // 초기값: 빈 배열
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    // 브라우저에서 API 호출 (CSR이니까 클라이언트에서 데이터 가져옴)
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data.users);
        setLoading(false);
      });
  }, []);
 
  if (loading) return <p>Loading...</p>;  // JS 실행 후에야 로딩 표시
 
  return (
    <div>
      <h1>사용자 목록</h1>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

브라우저가 보는 HTML (JS 실행 전):

<div id="root"></div>
<!-- 아무것도 없음 -->

SSR: Express + React (서버에서 렌더링)

// server.js — SSR
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './src/App.jsx';
 
const app = express();
 
app.get('/', async (req, res) => {
  // 서버에서 데이터 가져오기
  const users = await db.query('SELECT * FROM users');
 
  // 서버에서 React 컴포넌트를 HTML 문자열로 변환
  const html = renderToString(
    React.createElement(App, { users })  // props로 데이터 전달
  );
 
  res.send(`
    <!DOCTYPE html>
    <html>
    <head><title>사용자 목록</title></head>
    <body>
      <div id="root">${html}</div>
      <script>
        // 서버 데이터를 클라이언트에 전달 (Hydration용)
        window.__INITIAL_DATA__ = ${JSON.stringify({ users })};
      </script>
      <script src="/bundle.js"></script>
    </body>
    </html>
  `);
});

브라우저가 보는 HTML (JS 실행 전에도 내용 있음!):

<div id="root">
  <div>
    <h1>사용자 목록</h1>
    <ul>
      <li>Alice</li>
      <li>Bob</li>
      <li>Charlie</li>
    </ul>
  </div>
</div>

한눈에 비교: CSR vs SSR

항목CSRSSR
HTML 내용<div id="root"></div> (비어있음)완성된 HTML (내용 포함)
화면 그리는 주체브라우저 (JavaScript)서버 (Node.js 등)
첫 화면 표시JS 다운로드 + 실행 후 (느림)HTML 수신 즉시 (빠름)
인터랙션 가능화면 표시와 동시에Hydration 완료 후
SEO불리 (빈 HTML)유리 (완성된 HTML)
서버 부하낮음 (파일만 전송)높음 (매 요청마다 렌더링)
서버 요구사항아무 서버 (Nginx, Flask...)JS 엔진 필요 (Node.js)
데이터 가져오기브라우저에서 API 호출서버에서 DB 직접 조회
빌드 결과물정적 파일 (HTML + JS + CSS)서버 애플리케이션
배포 난이도쉬움 (CDN 가능)복잡 (서버 필요)

핵심 정리

CSR (Client-Side Rendering)

서버: "여기 빈 HTML이랑 JS 파일이야. 나머지는 니가 알아서 그려."
브라우저: "OK, JS 다운로드하고 실행해서 화면 그릴게... 좀 기다려."
  • 빌드 결과 = 정적 파일 (어떤 서버든 서빙 가능)
  • 화면은 브라우저가 JS를 실행해서 그림
  • 초기 로딩 느림, SEO 불리
  • 서버 부하 적음, 배포 간단

SSR (Server-Side Rendering)

서버: "내가 이미 완성된 HTML을 만들어서 보내줄게. 바로 보여줘."
브라우저: "와, 바로 보인다! JS 다운로드해서 이벤트만 연결할게."
  • 빌드 결과 = 서버 애플리케이션 (Node.js 서버 필요)
  • 화면은 서버가 HTML을 만들어서 보냄
  • 초기 로딩 빠름, SEO 유리
  • 서버 부하 높음, 배포 복잡

핵심 한 줄

CSR은 "빈 그릇 + 레시피(JS)"를 보내는 것이고, SSR은 "완성된 요리"를 보내는 것이다.

선택 기준

SEO가 중요하고 공개 페이지인가?
├── Yes → SSR 고려 (Next.js, SvelteKit)
└── No  → CSR로 충분 (React + Vite, Svelte + Vite)
         → Flask/Express로 정적 파일 서빙

다음 단계

03-nextjs-is-different.md에서 "그렇다면 SSR을 하려면 Next.js를 써야 하는 건가?"에 대해 알아봅니다. Next.js의 빌드 결과물이 일반 React 빌드와 어떻게 근본적으로 다른지 비교합니다.


작성: 2026-02-09 버전: 1.0 예상 독서 시간: 12분