Computer Science/Web Development

웹 렌더링 전략: CSR, SSR, Streaming SSR, SSG, ISR, RSC & 하이브리드

JTB 2026. 6. 22. 17:51

React, Next.js, Vue, Nuxt 같은 모던 웹 프레임워크는 서로 다른 렌더링 전략 위에 만들어져 있다. 각 전략은 하나의 질문에 다르게 답한다: HTML을 어디서, 언제 생성하는가? 이 문서는 역사적 배경부터 현대의 하이브리드 접근까지 모든 렌더링 전략을 다룬다.


배경: 어떻게 여기까지 왔나

전통적인 MPA (Multi-Page Application)

초기 웹: 모든 페이지 이동 = 서버로부터 받는 완전한 HTML 문서.

사용자가 링크 클릭 → 서버가 PHP/JSP/Rails 실행 → 완성된 HTML 반환 → 브라우저 전체 새로고침

단순하지만, 페이지 전환마다 화면이 깜빡였고 문서 전체를 다시 받느라 대역폭을 낭비했다.

모던 SSR(React/Next.js의 서버 렌더링)은 전통적인 MPA와 다르다. 모던 SSR은 첫 로드 이후 클라이언트 하이드레이션과 SPA식 네비게이션을 사용한다.

  전통적 MPA Ajax 기반 SPA
요청 주체 브라우저 (전체 페이지 요청) JavaScript (XHR/fetch)
응답 단위 전체 HTML 페이지 JSON 또는 부분 HTML
페이지 갱신 전체 새로고침 부분 DOM 갱신
UX 깜빡임, 대기 매끄러움, 앱 같은 경험

SPA의 등장

JavaScript + Ajax 덕분에 전체 새로고침 없이 페이지의 일부만 갱신할 수 있게 됐다 → SPA (Single Page Application): 실제로 페이지를 다시 로드하지 않고 뷰 사이를 이동하는 앱.

 

Ajax란?

Ajax — Asynchronous JavaScript And XML

정의

페이지 전체를 새로고침하지 않고, JavaScript로 서버와 비동기 통신해서 필요한 데이터만 받아와 화면 일부만 갱신하는 기법이다.

A  — Asynchronous  (비동기)  ← async가 아님
ja — JavaScript
x  — And XML

⚠️ async JavaScript가 아니라 Asynchronous JavaScript and XML의 약자다.

 

동작 개념

기존: 링크 클릭 → 페이지 전체 새로고침 → 깜빡임
Ajax: JS가 백그라운드에서 데이터만 요청 → 화면 일부만 갱신 → 매끄러움

 

구현 수단:

  • XMLHttpRequest (옛날)
  • fetch API (현대)
  • axios (라이브러리)

이름 속 XML

XML(eXtensible Markup Language)은 데이터를 태그로 감싸 구조화하는 마크업 언어. Ajax 초창기의 데이터 교환 표준이었다.

<!-- XML -->
<user>
  <name>Jay</name>
  <age>30</age>
  <skills>
    <skill>React</skill>
    <skill>TypeScript</skill>
  </skills>
</user>

// JSON (같은 데이터)
{
  "name": "Jay",
  "age": 30,
  "skills": ["React", "TypeScript"]
}

HTML vs XML

  • HTML → 화면에 "보여주기" 위한 정해진 태그 (<div>, <p>, <h1>...)
  • XML → 데이터를 "저장/전송"하기 위한 자유로운 태그 (직접 정의 = eXtensible)

XML이 JSON에 밀린 이유

항목 XML JSON
용량 무거움 (여는/닫는 태그 중복) 경량 (light)
가독성 장황함 간결함
JS 호환 파싱 후 DOM 순회 필요 (번거로움) JSON.parse() 한 줄

 

지금도 XML이 쓰이는 곳

  • RSS 피드 (블로그 구독)
  • SOAP API (구형 엔터프라이즈 시스템)
  • 설정 파일 (Android 레이아웃, Maven pom.xml)
  • SVG (SVG도 XML 기반!)

Ajax는 Asynchronous JavaScript And XML의 약자로, 페이지 새로고침 없이 비동기로 데이터만 받아 화면 일부를 갱신하는 기법이다. 이름엔 XML이 있지만 현대에는 가볍고 JS 친화적인 JSON을 주로 쓰며, 구현은 fetch/axios로 한다.

 

SPA ≠ CSR

흔히 혼동하는 지점:

  • SPA — 아키텍처 패턴: 하나의 HTML 페이지 안에서 콘텐츠를 동적으로 교체
  • CSR — 렌더링 전략: 브라우저가 렌더링을 수행

대부분의 SPA가 CSR을 쓰지만, 두 개념은 독립적이다. SPA가 SSR이나 SSG를 쓸 수도 있다.

SPA + CSR → React create-react-app (전통적)
SPA + SSR → Next.js (서버가 첫 로드 렌더, 이후 클라이언트가 네비게이션)
SPA + SSG → Next.js static export

개요

전략 HTML 생성 위치 생성 시점 클라이언트 JS
CSR 브라우저 JS 로드 후 전체 번들
SSR 서버 요청마다 전체 번들 + 하이드레이션
Streaming SSR 서버 (청크 단위) 요청마다, 점진적으로 전체 번들 + 하이드레이션
SSG 빌드 서버 빌드 시점 전체 번들 + 하이드레이션
ISR 빌드 서버 빌드 + 백그라운드 재검증 전체 번들 + 하이드레이션
RSC 서버 컴포넌트 단위, 빌드 또는 온디맨드 Zero JS (서버 파트)

ℹ️ 하이브리드는 별도 모드가 아니다 — 위 전략들을 라우트 또는 컴포넌트 단위로 조합한 것이다. §7 참고.


1. CSR (Client-Side Rendering)

정의

서버는 거의 빈 HTML 껍데기를 보낸다. 브라우저가 JS 번들을 내려받아 실행하고, UI 전체를 클라이언트에서 렌더링한다.

동작 방식

1. 브라우저가 / 요청 → 서버가 빈 HTML 반환 (<div id="root"></div> + JS 번들)
2. 브라우저가 JS 번들 다운로드 및 실행
3. React가 DOM 렌더 — 페이지가 보이기 시작
4. 앱이 인터랙티브해짐

사용자는 JS가 실행될 때까지 빈 화면을 본다. JS 실행 완료 시점에 한 번에 콘텐츠가 나타난다.

특징

  • 렌더링 단위: 브라우저 (앱 전체)
  • 데이터 패칭: 클라이언트 (useEffect, React Query, SWR)
  • 서버 부하: 매우 낮음 — 정적 파일만 서빙
  • SEO: 약함 — JS를 실행하지 않는 크롤러는 빈 페이지를 본다

장점

  • 빠른 페이지 전환 (첫 로드 이후 서버 왕복 없음)
  • 앱 같은 매끄러운 UX
  • 단순한 인프라 — 정적 CDN 어디에나 호스팅 가능
    • 정적 CDN- CSR은 서버에서 렌더링을 안 하기 때문에, 결과물이 그냥 정적 파일 덩어리다.
      CSR 빌드 결과물:
        index.html   (거의 빈 껍데기)
        app.js       (JS 번들)
        style.css
        → 이 파일들만 어딘가 올려두면 끝
      

단점

  • 느린 FCP/LCP — JS 실행 전까지 빈 화면
  • 약한 SEO — 첫 로드 시 빈 HTML
  • 첫 방문 시 더 큰 JS 번들 전송

언제 쓰나

  • 대시보드, 인증 뒤의 앱 (SEO 무관)
  • 인터랙션이 많은 도구 (에디터, 실시간 앱)
  • 초기 로드 속도가 허용되는 내부 도구

코드 예시

<!-- index.html -->
<body>
  <div id="root"></div>
  <script defer src="app.js"></script>
</body>
// app.js
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);

2. SSR (Server-Side Rendering)

정의

서버가 매 요청마다 완성된 HTML 문서를 생성해 브라우저로 보낸다. 이후 React가 하이드레이션(hydration) 으로 인터랙티비티를 붙인다.

동작 방식

1. 브라우저가 / 요청 → 서버가 React 실행 → 완전히 렌더된 HTML 반환
2. 브라우저가 HTML 즉시 표시 — JS 기다리지 않고 콘텐츠 보임
3. JS 번들이 병렬로 다운로드
4. 하이드레이션: React가 Fiber tree 생성, 서버 렌더 DOM과 비교, 이벤트 핸들러 부착
5. 앱이 인터랙티브해짐

하이드레이션이란: 서버가 보낸 HTML(이미 DOM에 그려진 상태)과 클라이언트에서 생성한 Fiber tree를 비교해 이벤트 핸들러를 부착하는 과정이다. 브라우저 렌더링 파이프라인(DOM → CSSOM → Render Tree → Layout → Paint) 완료 이후에 발생한다.

 

DOM에 직접 적용 — 이벤트 핸들러 붙이기 / 상태값 변경

## 이벤트 핸들러 붙이기
1. DOM 생성 (서버 HTML 기반)
2. DOM + CSSOM → Render Tree → 화면 그림 (Render Tree 임무 끝)
3. React가 Fiber Tree 생성
4. Fiber Tree ↔ DOM 비교
5. DOM에 이벤트 핸들러 부착  ← Render Tree와 무관, DOM에 직접

## 상태값 변경
setState
  ↓
[React]  새 Fiber Tree 생성 → 이전과 diff → 변경된 부분만 추출
  ↓
[DOM]    변경된 부분만 commit (실제 DOM 수정)
  ↓
[브라우저] DOM + CSSOM → Render Tree 재생성 → Layout → Paint
  ↓
화면 갱신 ✅

 

⚠️ Hydration Mismatch: 서버가 만든 HTML과 클라이언트의 첫 렌더 결과가 다를 때 발생하는 에러다. Date.now(), Math.random(), new Date() 처럼 서버와 클라이언트에서 값이 달라지는 코드를 렌더 함수에 직접 쓰면 발생한다. 동작이 멈추진 않지만 — React가 해당 부분을 클라이언트에서 다시 렌더 — 화면 깜빡임, SSR 이점 상실, 콘솔 에러가 따라오는 버그다. 해결: 시간·랜덤 등 클라이언트 전용 값은 useEffect 안에서 설정해 hydration 이후에만 실행되게 한다.

 

💡 별개 개념 — Hydration 지연: mismatch가 없어도, HTML이 보이는 시점인터랙티브해지는 시점 사이에는 간격이 있다. JS 번들이 로드·실행되어 hydration이 끝나기 전까지는 화면은 보이지만 클릭이 동작하지 않는다. 이건 버그가 아니라 모든 SSR의 정상 특성이다 (TTI 2단계).

특징

  • 렌더링 단위: 페이지 단위
  • 시점: 매 요청마다
  • 데이터 패칭: 서버
  • 클라이언트 JS: 전체 번들 전송 (하이드레이션용)

장점

  • 빠른 FCP/LCP — JS 실행 전에 콘텐츠가 보임
  • SEO 친화적 — 크롤러가 완성된 HTML을 받음
  • JS가 비활성화돼도 정적으로 동작 (콘텐츠는 보이되 인터랙션은 안 됨)

단점

  • 느린 TTFB(Time To First Byte) — 응답 전 매 요청마다 React 렌더 필요
  • 높은 서버 부하 — 매 요청마다 CPU 작업
  • 하이드레이션 비용 — 매 페이지 로드마다 인터랙티비티 부착을 위한 JS 작업

언제 쓰나

  • 동적·개인화 콘텐츠의 SEO 중요 페이지 (이커머스, 뉴스 기사)
  • 매 요청마다 데이터가 바뀌는 페이지 (사용자별 피드)

코드 예시 (Next.js App Router)

// app/page.tsx
export default async function Page() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store', // 매 요청마다 재실행 (SSR 동작)
  });
  const posts = await res.json();

  return (
    <main>
      <h1>Latest Posts</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

 


3. Streaming SSR

정의

SSR의 확장으로, 전체 페이지 렌더가 끝나길 기다리는 대신 HTML을 준비되는 대로 청크 단위로 스트리밍한다. React 18 + <Suspense>가 이를 가능케 한다.

동작 방식

표준 SSR:
  서버가 모든 데이터를 기다림 → 완성된 HTML 렌더 → 한 번에 전송
  (느린 API = 전부 대기)

Streaming SSR:
  서버가 HTML 껍데기를 즉시 전송
  → <Suspense> 경계가 "나중에 렌더" 섹션을 표시
  → 느린 부분은 데이터가 준비되면 스트리밍됨

<Suspense>는 청크 경계 역할을 한다 — React에게 HTML 스트림을 어디서 나눠 껍데기를 먼저 흘려보내고 느린 섹션을 나중에 스트리밍할지 알려준다.

특징

  • 렌더링 단위: 페이지 단위 (청크로 스트리밍)
  • 시점: 요청마다, 점진적으로
  • 데이터 패칭: 서버, Suspense 경계 단위
  • 클라이언트 JS: 전체 번들 전송 (하이드레이션용)

장점

  • TTFB 극적 개선 — 데이터가 느려도 껍데기가 즉시 도착
  • 모든 걸 기다리지 않고 콘텐츠를 점진적으로 봄
  • 각 <Suspense> 경계가 독립적으로 해결됨

단점

  • 표준 SSR보다 복잡한 멘탈 모델
  • 경계 설계가 나쁘면 폴백 UI 깜빡임
  • React 18+ 와 스트리밍 가능한 서버 런타임 필요

언제 쓰나

  • 빠른/느린 데이터 의존성이 섞인 SSR 페이지
  • 독립적인 데이터 섹션이 여러 개인 페이지

코드 예시 (Next.js App Router)

// app/page.tsx
import { Suspense } from 'react';
import FastSection from './FastSection';
import SlowSection from './SlowSection'; // 느린 API를 패칭

export default function Page() {
  return (
    <main>
      <h1>Shell — 즉시 전송</h1>
      <FastSection />
      <Suspense fallback={<p>Loading...</p>}>
        <SlowSection />  {/* 데이터 준비되면 나중에 스트리밍 */}
      </Suspense>
    </main>
  );
}

4. SSG (Static Site Generation)

정의

HTML을 빌드 시점에 생성해 CDN에 저장한다. 모든 요청은 동일한 사전 빌드 파일을 받는다 — 요청 시점의 서버 렌더링이 없다.

동작 방식

1. npm run build → 프레임워크가 데이터를 패칭하고 모든 페이지를 HTML 파일로 렌더
2. HTML 파일을 CDN에 배포
3. 브라우저가 / 요청 → CDN이 사전 빌드 HTML 즉시 반환
4. JS 번들 다운로드 → 하이드레이션 (SSR과 동일)

특징

  • 렌더링 단위: 페이지 단위
  • 시점: 빌드 시점, 한 번
  • 데이터 패칭: 빌드 시점
  • 클라이언트 JS: 전체 번들 전송 (하이드레이션용)

장점

  • 가능한 최고의 TTFB — CDN 파일 서빙만, 연산 없음
  • 가장 강력한 SEO — 완성된 HTML이 항상 존재
  • 가장 낮은 비용 — 서버 런타임 불필요

단점

  • 오래된 데이터 — 변경 반영하려면 사이트 재빌드 필요
  • 빌드 시간이 페이지 수에 비례해 증가
  • 개인화·실시간 콘텐츠에 부적합

언제 쓰나

  • 문서, 블로그, 마케팅·랜딩 페이지
  • 거의 또는 전혀 바뀌지 않는 콘텐츠

코드 예시 (Next.js App Router)

// app/page.tsx
export default async function Page() {
  // cache 옵션 없음 = 기본값 = 정적 (SSG 동작)
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

5. ISR (Incremental Static Regeneration)

정의

SSG를 확장해, 페이지가 오래되면 개별 페이지를 백그라운드에서 재생성하면서 그동안 캐시된 버전을 계속 서빙한다. 전체 재빌드가 필요 없다.

  • Incremental? 두 가지 의미의 "incremental"
    1. 페이지 단위 부분 갱신: 전체(whole)가 아니라 조각(increment) 단위로, 한 페이지씩 독립적으로 재생성.
    2. 빌드 후에도 계속 추가/갱신 가능: SSG는 빌드 시점에 모든 게 확정되지만, ISR은 빌드 후에도 런타임에 페이지를 점진적으로 추가/갱신한다 (generateStaticParams에 없던 경로도 요청 시 생성).
    Incremental  → 한 페이지씩 "점진적"으로
    Static       → 정적 HTML을 (SSG 기반)
    Regeneration → "재생성"한다
    
     

동작 방식

1. 빌드 시점 → 페이지를 HTML로 사전 렌더 (SSG와 동일)
2. 사용자가 페이지 요청 → CDN이 캐시 HTML 즉시 서빙
3. revalidate 기간 만료 후, 다음 요청이 백그라운드 재생성 트리거
4. 새 HTML 생성 → 기존 캐시 버전 교체
5. 이후 요청은 새 HTML을 받음

첫 번째 요청은 항상 캐시된 HTML을 받는다. 만료 후 첫 요청이 재생성을 트리거하지만, 그 사람도 이전 캐시를 받는다. 재생성 완료 후 다음 요청부터 새 HTML이 제공된다.

특징

  • 렌더링 단위: 페이지 단위
  • 시점: 빌드 시점 + 백그라운드 재검증
  • 데이터 패칭: 빌드 시점 + 재검증 시
  • 클라이언트 JS: 전체 번들 전송 (하이드레이션용)

장점

  • 요청 시점 속도는 SSG와 동일 (CDN 서빙)
  • 전체 재빌드 없이 데이터 갱신 가능
  • 페이지별 재검증 주기 제어

단점

  • stale 기간 후 첫 요청은 오래된 데이터를 볼 수 있음
  • 캐시 무효화 전략이 까다로움
  • SSG·SSR보다 디버깅이 어려움

언제 쓰나

  • 상품 카탈로그, 뉴스, 이커머스 — 콘텐츠가 바뀌지만 매 요청마다는 아닌 경우
  • 주기적 신선도가 필요한 대체로 정적인 콘텐츠

코드 예시 (Next.js App Router)

// app/page.tsx
export default async function Page() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // 60초마다 재생성
  });
  const posts = await res.json();

  return (
    <main>
      <h1>Product Catalog</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

6. RSC (React Server Components)

정의

React 18+ / Next.js App Router에서 도입된 Server Components. 컴포넌트 단위서버에서만 렌더링되며 클라이언트에 Zero JS를 보낸다.

디렉티브 정리 — 'use client'와 'use server'는 이름이 대칭처럼 보여 짝으로 오해하기 쉽지만, 서로 다른 층위를 가리킨다. ('use client'는 컴포넌트, 'use server'는 함수)

  

디렉티브 만드는 것 비고
(없음) Server Component App Router 기본값 — 디렉티브 불필요. 서버 렌더링 + DB/API 직접 접근
'use client' Client Component 브라우저에서 실행, JS 번들 전송, hooks·이벤트·브라우저 API 사용 가능
'use server' Server Action (컴포넌트 ❌) 클라이언트가 호출할 수 있는 서버 함수. 폼 제출·뮤테이션용
// (없음) — Server Component: 서버에서 렌더, DB 직접 접근
export default async function Page() {
  const posts = await db.post.findMany();
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

// 'use client' — Client Component: 상태·이벤트 사용
'use client';
import { useState } from 'react';
export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

// 'use server' — Server Action: 클라이언트가 호출하는 서버 함수
'use server';
export async function createPost(formData: FormData) {
  await db.post.create({ data: { title: formData.get('title') } });
}
  • 클라이언트가 호출하는 서버 함수 — 어떻게 동작하나? 클라이언트 컴포넌트에서 import해서 호출한다. 단, import하지만 코드는 클라이언트로 가지 않는다.
    일반 함수 import → 그 함수 코드가 번들에 포함 → 클라에서 실행
    Server Action import → 함수 코드는 서버에 남음 → 클라엔 "호출 방법"만 감
    
    클라이언트가 createPost() 호출
      ↓
    실제로는 네트워크 요청이 자동 생성됨 (내부적으로 fetch 같은 것)
      ↓
    서버에서 진짜 createPost 실행
      ↓
    결과만 클라로 반환
    
    비유: import한 createPost는 "서버로 연결되는 리모컨". 버튼을 누르면(호출) 실제 동작은 서버에서 일어나고, 함수 본체(DB 접근 코드)는 클라가 절대 못 본다. 왜 좋은가: 기존 방식은 API 라우트(/api/posts)를 따로 만들고 클라에서 fetch하고 직렬화/역직렬화를 직접 처리해야 했다. Server Action은 함수에 'use server'를 붙이고 import해서 그냥 호출하면 된다 — API 라우트 + fetch 보일러플레이트가 사라진다.

⚠️ 흔한 오해: 'use server'는 Server Component를 만드는 게 아니다. Server Component는 디렉티브 없이 기본으로 만들어지고, 'use server'는 함수(Server Action)를 표시한다.

동작 방식

1. React Server Components가 서버에서 실행
2. HTML(과 자식 Client Component를 위한 직렬화된 props)만 클라로 전송
3. Server Component를 위한 JS 번들 없음 — 클라이언트 풋프린트 0
4. 인터랙티브한 부분은 'use client' 컴포넌트로 임베드:
   → 해당 JS 번들은 전송되고 하이드레이션됨

SSR과의 차이: SSR은 페이지 단위로 서버에서 렌더링하고 JS 번들도 함께 보낸다. RSC는 컴포넌트 단위로 서버에서 렌더링하고 해당 컴포넌트의 JS를 아예 보내지 않는다.

특징

  • 렌더링 단위: 컴포넌트 단위
  • 시점: 빌드 시점 또는 서버에서 온디맨드
  • 데이터 패칭: 서버에서 직접 (DB, API 호출)
  • 클라이언트 JS: Server Component는 Zero JS ('use client' 파트만 JS 전송)

장점

  • 클라이언트 JS 번들 최소화 — Server Component는 클라 풋프린트 0
  • 자격 증명 노출 없이 DB·API 직접 접근
  • 세밀한 제어: 컴포넌트별로 서버/클라이언트 렌더링 선택

단점

  • Server Component에서 useState, useEffect, 브라우저 API 사용 불가
  • 복잡한 멘탈 모델 — 서버/클라이언트 경계 관리
  • 하이드레이션 mismatch 버그 유발 쉬움
  • Node.js 또는 엣지 런타임 필요 (순수 정적 호스팅 불가)

언제 쓰나

  • 인터랙션 없는 데이터 중심 컴포넌트 (기사 본문, 상품 설명)
  • DB나 시크릿에 직접 접근하는 컴포넌트
  • JS 번들 크기가 우려되는 대형 앱

코드 예시 (Next.js App Router)

// app/components/PostList.tsx — Server Component (기본값, 디렉티브 불필요)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', { cache: 'no-store' });
  return res.json();
}

export default async function PostList() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
  // 이 컴포넌트는 클라로 Zero JS 전송
}

 

// app/components/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';

export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️ Liked' : '🤍 Like'}
    </button>
  );
}
// app/page.tsx — Server와 Client Component 조합
import PostList from './components/PostList';
import LikeButton from './components/LikeButton';

export default function Page() {
  return (
    <main>
      <PostList />    {/* Server Component — Zero JS */}
      <LikeButton />  {/* Client Component — JS 전송 */}
    </main>
  );
}

7. 하이브리드 렌더링

모던 프레임워크는 여러 전략을 라우트 단위, 심지어 컴포넌트 단위로 조합한다. Next.js App Router의 기본 모델이다.

Next.js App Router — 라우트별 조합:
  /                → SSG  (정적 마케팅 페이지)
  /blog/[slug]     → ISR  (revalidate: 3600)
  /dashboard       → CSR  (인증 뒤, SEO 무관)
  /feed            → SSR  (개인화, cache: 'no-store')
  /product/[id]    → ISR + RSC + Streaming

한 페이지 안에서 모든 전략을 섞을 수 있다:

export default function ProductPage() {
  return (
    <>
      <ProductDetails />           {/* RSC — Server Component, Zero JS */}
      <AddToCart />                {/* 'use client' — JS 전송, 인터랙션 처리 */}
      <Suspense fallback={<p>Loading reviews...</p>}>
        <Reviews />                {/* RSC 스트리밍 — 느린 데이터, JS 없음 */}
      </Suspense>
    </>
  );
}

장점

  • 빠른 FCP/LCP (서버 렌더 HTML)
  • 강력한 SEO
  • 하이드레이션 후 CSR 같은 매끄러운 UX
  • 컴포넌트별 서버/클라이언트 렌더링 선택 — 페이지 단위를 넘는 제어
  • 스트리밍으로 백엔드 지연 은폐

단점

  • 복잡한 멘탈 모델 (렌더링 경계, 서버 vs 클라이언트 컴포넌트)
  • 하이드레이션 mismatch 버그 유발 쉬움
  • 서버/클라이언트 상태 동기화 필요 (예: 인증된 사용자 상태)
  • 배포 복잡성 (Node.js 또는 엣지 런타임 필요, 정적 호스팅만으로는 불가)

하이브리드 = CSR + SSR + SSG + ISR + RSC + Streaming, 라우트·컴포넌트 단위로 조합.


8. 전체 비교

  CSR SSR Streaming SSR SSG ISR RSC
HTML 생성 브라우저 서버 서버 (청크) 빌드 빌드 + BG 서버
렌더링 단위 앱 전체 페이지 페이지 (청크) 페이지 페이지 컴포넌트
시점 JS 로드 후 요청마다 요청마다, 점진적 빌드 시점 빌드 + 재검증 컴포넌트마다
클라이언트 JS 전체 번들 전체 번들 전체 번들 전체 번들 전체 번들 Zero (서버 파트)
FCP/LCP 느림 빠름 빠름 가장 빠름 가장 빠름 빠름
TTFB 빠름 (정적) 느림 빠름 (껍데기 먼저) 가장 빠름 가장 빠름 빠름
TTI 단일 단계: 보임 = 인터랙티브 2단계: 보임 → 인터랙티브 (하이드레이션 후) 2단계: 보임 → 인터랙티브 (하이드레이션 후) 2단계: 보임 → 인터랙티브 (하이드레이션 후) 2단계: 보임 → 인터랙티브 (하이드레이션 후) 최소 (클라 파트만)
SEO 약함 강함 강함 가장 강함 강함 강함
데이터 신선도 항상 최신 항상 최신 항상 최신 재빌드까지 stale 주기적 갱신 설정 가능
서버 부하 매우 낮음 높음 높음 없음 매우 낮음 중간
하이드레이션 오버헤드 없음 페이지 전체 페이지 전체 페이지 전체 페이지 전체 클라 파트만
페이지 전환 빠름 (JS 처리) 서버 왕복 서버 왕복 빠름 (CDN) 빠름 (CDN) 설정에 따라
구조 복잡도 단순 복잡 매우 복잡 단순 중간 복잡 (경계 관리)
인터랙티비티 전체 하이드레이션 경유 하이드레이션 경유 하이드레이션 경유 하이드레이션 경유 'use client'만
핵심 패턴 createRoot fetch(..., { cache: 'no-store' }) <Suspense> 경계 fetch(url) (기본) fetch(..., { next: { revalidate: N } }) 기본 Server Components

⚠️ RSC는 SSR/SSG/ISR과 직교(orthogonal) 하며, 상호 배타적 대안이 아니다. SSR/SSG/ISR은 HMTL을 언제 생성할지 결정하고, RSC는 어떤 컴포넌트가 JS를 보낼지 결정한다. Server Component도 여전히 SSR/SSG/ISR을 통해 HTML로 렌더되며 — RSC는 그 클라이언트 JS만 제거한다. 위 표는 비교를 위한 단순화다.


9. 전략별 선택 가이드

콘텐츠 중심 + 항상 변함            → SSR
느린 데이터 의존성 있는 콘텐츠       → Streaming SSR
인터랙션 중심, 인증 뒤             → CSR
대체로 정적, 드물게 변함           → SSG
대체로 정적 + 가끔 갱신            → ISR
대형 앱, 번들 크기 중요           → RSC (SSR/SSG와 혼합)
위의 모든 것의 조합               → Hybrid (Next.js App Router)

실제 사례 

앱 유형 권장 전략
이커머스 상품 페이지 SSR 또는 ISR
블로그 / 문서 SSG
뉴스 사이트 ISR (revalidate: 60)
사용자 대시보드 CSR
마케팅 랜딩 페이지 SSG
소셜 피드 (개인화) SSR
느린/빠른 섹션 혼재 페이지 Streaming SSR
데이터 많은 페이지 (인터랙션 없음) RSC

10. 핵심 정리

  • CSR: 브라우저가 모든 일을 한다. 빠른 전환, 약한 SEO, 느린 첫 페인트.
  • SSR: 서버가 요청마다 렌더. 빠른 첫 페인트, 강한 SEO, 높은 서버 비용. 하이드레이션 필요.
  • Streaming SSR: 청크 단위 전송 SSR. 데이터가 느려도 껍데기가 빨리 도착. <Suspense>가 청크 경계를 표시.
  • SSG: 빌드 시점에 HTML 사전 생성. 가장 빠른 전송, 단 데이터는 stale.
  • ISR: 주기적 백그라운드 재생성이 붙은 SSG. SSG의 속도 + 가끔의 신선도.
  • RSC: 컴포넌트 단위 서버 렌더링. 서버 파트는 Zero JS. 'use server'는 Server Component가 아니라 Server Action용.
  • Hybrid: 모든 전략을 라우트·컴포넌트 단위로 조합 — Next.js App Router의 기본 모델.

면접 한 줄: 렌더링 전략은 "HTML을 누가, 언제 만드는가"의 차이다. CSR은 브라우저, SSR은 요청마다 서버, Streaming SSR은 청크 단위 스트리밍, SSG는 빌드 시, ISR은 빌드 후 주기적으로, RSC는 컴포넌트 단위로 서버에서 처리하며 JS를 보내지 않는다. 현대 프레임워크는 이 모든 전략을 라우트와 컴포넌트 단위로 조합한다.