02: CSR과 SSR의 차이 — 브라우저가 그리냐, 서버가 그리냐
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이준혁: 핵심 차이를 보자:
| 지표 | CSR | SSR |
|---|---|---|
| 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
| 항목 | CSR | SSR |
|---|---|---|
| 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분