architecture
CSR vs SSR vs SSG vs ISR vs Streaming SSR 완전 비교
렌더링 전략은 서비스의 성능, SEO, UX, 인프라 비용을 직접 결정합니다. 7년차라면 "Next.js 쓰면 SSR 되는 것 아닌가요?"가 아니라, 각 전략의 트레이드오프를 이해하고 서비스 특성에 맞게 선택할 수 있어야 합니다. 면접에서는 단순 정의가 아닌 "왜 이 서비스에 이 전략을 선택했는가"를 물어봅니다.
학습 개요
탄생 배경
해결하려 했던 문제
웹 초기에는 서버가 완성된 HTML을 내려주는 방식(MPAs)이 기본이었습니다. JavaScript가 점점 많아지면서 SPA(Single Page Application)가 등장했고, 클라이언트가 모든 렌더링을 담당하는 CSR이 주류가 되었습니다. 그러나 CSR은 초기 빈 HTML로 인한 SEO 불이익과 First Contentful Paint 지연 문제가 있었습니다. 이를 해결하기 위해 SSR, SSG, ISR이 차례로 등장했습니다.
역사적 맥락
2013년 React 등장 → SPA/CSR 전성기 → 2016년 Next.js 등장(SSR 대중화) → 2019년 Next.js 9.0에서 SSG 지원 → 2020년 Next.js 9.5에서 ISR 도입 → 2022년 React 18과 함께 Streaming SSR(Suspense 기반) 등장 → 2023년 Next.js App Router에서 React Server Components 도입으로 패러다임 전환
이전에는 어떻게 했나
렌더링 전략이 다양화되기 전에는 PHP, JSP, Ruby on Rails 같은 서버 사이드 템플릿 엔진이 모든 것을 해결했습니다. 서버가 DB 조회 → 데이터 바인딩 → HTML 생성을 한 번에 처리했고, JavaScript는 단순한 인터랙션 보조 역할이었습니다.
멘탈 모델
동작 원리
CSR: 브라우저가 빈 HTML(div#root)을 받고, JavaScript 번들을 다운로드/파싱/실행한 뒤 React가 DOM을 생성합니다. 사용자는 JS가 실행되기 전까지 빈 화면을 봅니다. SSR: 요청마다 서버에서 React 컴포넌트를 실행하여 완성된 HTML 문자열을 생성 후 전송합니다. 브라우저는 즉시 콘텐츠를 렌더링하고, 이후 JS 번들이 로드되면 Hydration을 통해 인터랙티브해집니다. SSG: 빌드 타임에 모든 페이지의 HTML을 미리 생성합니다. 요청 시 CDN에서 정적 파일을 즉시 서빙합니다. 런타임에 서버 연산이 없으므로 가장 빠르지만, 데이터가 빌드 시점에 고정됩니다. ISR: SSG의 변형으로, 빌드 후에도 특정 주기로 페이지를 백그라운드에서 재생성합니다. revalidate 시간이 지나면 첫 요청 시 stale 페이지를 제공하면서 백그라운드에서 새 페이지를 생성합니다(Stale-While-Revalidate). Streaming SSR: React 18의 Suspense와 결합하여 HTML을 청크 단위로 스트리밍합니다. 준비된 부분부터 먼저 전송하고, 느린 데이터가 준비되면 해당 청크를 추가로 전송합니다. TTFB(Time To First Byte)를 줄이고 LCP를 개선합니다. Hydration: SSR/SSG로 생성된 HTML에 이벤트 리스너와 React 상태를 연결하는 과정입니다. 서버가 만든 DOM과 클라이언트 React가 만들 DOM이 일치해야 하며, 불일치 시 Hydration Mismatch가 발생합니다.
핵심 구성 요소
TTFB (Time To First Byte)
서버가 첫 바이트를 응답하는 시간. SSG > SSR > CSR 순으로 빠름
FCP (First Contentful Paint)
첫 콘텐츠가 화면에 그려지는 시간. CSR에서 가장 느림
TTI (Time To Interactive)
사용자가 실제로 인터랙션 가능한 시간. Hydration 완료 후
Hydration
서버 HTML에 클라이언트 React를 연결하는 과정. SSR/SSG에서 필수
revalidate
ISR에서 페이지 재생성 주기를 결정하는 설정값(초 단위)
Suspense Boundary
Streaming SSR에서 청크 단위 스트리밍 경계를 정의
흐름 설명
[CSR 흐름]
1. 브라우저 → 서버: GET /page
2. 서버 → 브라우저: <html><body><div id="root"></div><script src="bundle.js"></script></body></html>
3. 브라우저: bundle.js 다운로드 (수백KB~수MB)
4. 브라우저: JS 파싱 및 실행
5. React: API 요청 → 데이터 수신 → DOM 생성 → 화면 표시
[SSR 흐름]
1. 브라우저 → 서버: GET /page
2. 서버: React 컴포넌트 실행 → API 호출 → 데이터 바인딩 → HTML 생성
3. 서버 → 브라우저: 완성된 HTML (콘텐츠가 포함된 상태)
4. 브라우저: HTML 즉시 렌더링 (FCP 빠름)
5. 브라우저: JS 번들 로드 → Hydration → TTI 달성
[SSG 흐름]
1. 빌드 시: getStaticProps 실행 → HTML 파일 생성 → CDN 업로드
2. 브라우저 → CDN: GET /page
3. CDN → 브라우저: 미리 생성된 HTML (캐시 히트, 매우 빠름)
4. 브라우저: HTML 즉시 렌더링 → JS 로드 → Hydration
[ISR 흐름]
1. 첫 빌드: SSG처럼 HTML 생성
2. revalidate 시간 경과 후 첫 요청: stale HTML 즉시 제공 + 백그라운드 재생성 트리거
3. 다음 요청부터: 새로 생성된 HTML 제공 (Stale-While-Revalidate)
[Streaming SSR 흐름]
1. 브라우저 → 서버: GET /page
2. 서버: Shell HTML 즉시 전송 (Suspense fallback 포함)
3. 서버: 느린 컴포넌트 데이터 준비 완료 시 해당 HTML 청크 스트리밍
4. 브라우저: 청크 수신 즉시 DOM 업데이트 (Selective Hydration)
코드 예제
// SSG + ISR 예시 (Next.js Pages Router)
export async function getStaticProps() {
const data = await fetchProductData();
return {
props: { data },
revalidate: 60, // 60초마다 백그라운드 재생성 (ISR)
};
}
// Streaming SSR 예시 (Next.js App Router + React Suspense)
// app/page.tsx
import { Suspense } from 'react';
async function SlowComponent() {
const data = await fetch('https://slow-api.com/data'); // 3초 걸림
return <div>{data}</div>;
}
export default function Page() {
return (
<div>
<h1>즉시 스트리밍되는 헤더</h1>
<Suspense fallback={<div>로딩 중...</div>}>
{/* SlowComponent 준비될 때까지 fallback 표시 후 스트리밍 */}
<SlowComponent />
</Suspense>
</div>
);
}
// Hydration Mismatch 예시와 원인
function ProblematicComponent() {
// 서버: typeof window === 'undefined' → '서버'
// 클라이언트: typeof window === 'object' → '클라이언트'
// → Hydration Mismatch 발생!
const env = typeof window === 'undefined' ? '서버' : '클라이언트';
return <div>{env}</div>;
}
// 해결책: useEffect로 클라이언트 전용 렌더링
function FixedComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <div>서버</div>; // SSR에서는 항상 이 값
return <div>클라이언트</div>;
}
비교 분석
CSR
vsCSR
CSR
vsSSG
CSR
vsISR
트레이드오프
이상적인 사용 사례