프론트엔드 개발하면서 CSR, SSR은 한번쯤은 들어봤을것이다.
하지만 까놓고 보면 렌더링 전략이 이것만 있는 게 아니다.
최근 Next.js App Router가 기본이 되면서 Streaming SSR, RSC 같은 개념도 알아야 하는 시대가 됐다.
이번 글에서는 현재 존재하는 주요 렌더링 전략 8가지를 정리해봤다.
(이미지는 AI를 통해 생성했습니다.)
빈 HTML을 받고, 브라우저에서 JavaScript가 다 그린다.

우리가 Vite + React로 프로젝트 만들면 기본적으로 이 방식이다.
서버는 그냥 빈 <div id="root"> 하나 던져주고, 브라우저가 JS 번들을 다운로드해서 React를 실행하고, DOM을 만들어서 화면을 그린다.
서버 → 빈 HTML 전송 → JS 번들 다운로드 → React 실행 → DOM 생성 → 화면 표시
문제는 JS 번들을 다 받고 실행할 때까지 사용자는 빈 화면을 본다는 것이다.
그리고 크롤러도 빈 HTML을 보니까 SEO도 안 된다.
대신 한 번 로딩되면 페이지 전환이 엄청 빠르고, 서버 부담이 없다.
주로 로그인 후 사용하는 관리자 페이지나 SEO가 필요 없는 곳에 쓴다
요청이 올 때마다 서버에서 HTML을 만들어서 보낸다.

사용자가 페이지를 요청하면 서버가 데이터를 fetch하고, HTML을 만들어서 보내준다.
브라우저는 받은 HTML을 바로 보여주고, 이후에 JS를 붙여서(hydration) 인터랙션을 활성화한다.
사용자 요청 → 서버에서 데이터 fetch → HTML 생성 → 전송 → hydration → 인터랙션 가능
항상 최신 데이터를 보여줄 수 있고, SEO도 좋다.
근데 매 요청마다 서버가 굴러야 해서 서버 부담이 크고, TTFB(첫 바이트까지 시간)가 느릴 수 있다.
사용자별 커스텀 페이지나 실시간 데이터가 중요한 곳에 자주 쓰인다
빌드할 때 HTML을 미리 다 만들어놓는다.

next build 하면 모든 페이지의 HTML이 미리 생성된다.
사용자가 요청하면 이미 만들어진 HTML을 CDN에서 바로 던져주기만 하면 끝이다.
빌드 타임 → 모든 페이지의 HTML 미리 생성
요청 시 → CDN에서 즉시 전송 → hydration → 인터랙션 가능
당연히 가장 빠르고, SEO도 완벽하다.
근데 데이터가 바뀌면 전체를 다시 빌드해야 한다. 페이지가 1만 개면 빌드 시간이 어마어마해진다.
블로그, 기술 문서, 마케팅 랜딩 페이지 같이 데이터가 많이 안바뀌는곳에 사용한다.
SSG인데, 일정 시간마다 백그라운드에서 페이지를 다시 만든다.

SSG의 "데이터 바뀌면 전체 재빌드" 문제를 해결한 버전이다.
처음에는 SSG처럼 빌드 시점에 HTML을 만들어놓는다. 이후 요청이 오면 캐시된 HTML을 즉시 보내주고,
revalidate로 설정한 시간이 지나면 백그라운드에서 새 HTML을 만들어둔다.
다음 요청부터 갱신된 HTML이 나간다.
빌드 타임 → HTML 미리 생성
요청 시 → 캐시된 HTML 즉시 전송
revalidate 후 → 백그라운드에서 새 HTML 생성
다음 요청 → 갱신된 HTML 전송
Next.js에서 이렇게 쓴다:
// Pages Router
export async function getStaticProps() {
const data = await fetchProducts();
return {
props: { data },
revalidate: 60, // 60초마다 재생성
};
}
// App Router
async function ProductPage() {
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
});
return <ProductList data={data} />;
}
SSG의 속도를 유지하면서 데이터 최신성도 챙길 수 있다.
다만 revalidate 주기 동안은 stale(오래된) 데이터가 노출될 수 있다.
상품 목록, 뉴스 기사같이 자주 업뎃되지만 실시간까지는 필요없는곳에 쓰면 좋다
SSR인데, 한번에 다 보내지 않고 준비된 부분부터 조금씩 보낸다.

기존 SSR의 문제는 서버에서 헤더, 본문, 댓글 데이터를 전부 fetch한 다음에야 HTML을 한 번에 보낸다.
댓글 API가 3초 걸리면? 사용자는 3초 동안 아무것도 못 본다.
Streaming SSR은 준비된 부분부터 먼저 보낸다.
[일반 SSR]
서버: 헤더 fetch... 본문 fetch... 댓글 fetch... (전부 완료) → HTML 한번에 전송
[Streaming SSR]
서버: 헤더 완료 → 먼저 전송!
본문 완료 → 전송!
댓글 완료 → 전송!
핵심은 React의 <Suspense>다. Suspense 경계가 곧 스트리밍 경계가 된다.
export default function Page() {
return (
<div>
<Header /> {/* 즉시 전송 */}
<Suspense fallback={<Skeleton />}>
<SlowContent /> {/* 준비되면 전송 */}
</Suspense>
<Suspense fallback={<Spinner />}>
<Comments /> {/* 준비되면 전송 */}
</Suspense>
</div>
);
}
느린 데이터가 전체 페이지를 블로킹하지 않으니까 TTFB가 크게 개선된다.
Next.js App Router에서는 이게 이미 기본 동작이다. 쓰고 있었는데 몰랐을 수도 있다.
컴포넌트를 서버용/클라이언트용으로 나눠서, 서버 컴포넌트는 JS 번들에 포함시키지 않는다.

이건 엄밀히 말하면 "렌더링 전략"이 아니라 "아키텍처"에 가깝다. SSR, Streaming과 함께 쓰인다.
Next.js App Router에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트다.
'use client'를 붙여야 클라이언트 컴포넌트가 된다.
서버 컴포넌트:
클라이언트 컴포넌트 ('use client'):
실제로는 주로 이렇게 쓰인다
// app/page.tsx — 서버 컴포넌트 (기본)
import { LikeButton } from './like-button';
export default async function PostPage() {
// 서버에서 직접 DB 조회 (별도 API 엔드포인트 없이!)
const post = await db.post.findUnique({ where: { id: 1 } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={post.id} /> {/* 인터랙션은 클라이언트로 */}
</article>
);
}
// app/like-button.tsx — 클라이언트 컴포넌트
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>👍</button>;
}
원칙은 간단하다: 서버에서 할 수 있는 건 서버에서 하고, 인터랙션이 필요한 것만 클라이언트로 내린다.
한 페이지 안에서 정적인 부분은 미리 빌드하고, 동적인 부분만 런타임에 스트리밍한다.

아직 베타지만, Next.js가 가려는 방향을 보여주는 기능이다.
기존에는 한 페이지가 정적이면 정적, 동적이면 동적이었다. PPR은 한 페이지 안에서 둘을 섞는다.
요청 시:
┌──────────────────────────────┐
│ 네비게이션 (정적, 즉시) │
│ ┌────────────────────────┐ │
│ │ 사용자 프로필 (동적) │ │ ← Suspense 경계 안
│ └────────────────────────┘ │
│ 메인 콘텐츠 (정적, 즉시) │
│ ┌────────────────────────┐ │
│ │ 추천 상품 (동적) │ │ ← Suspense 경계 안
│ └────────────────────────┘ │
│ 푸터 (정적, 즉시) │
└──────────────────────────────┘
정적 부분(네비게이션, 메인 콘텐츠, 푸터)은 빌드 타임에 미리 만들어서 CDN 속도로 나가고, 동적 부분(사용자 프로필, 추천 상품)만 런타임에 Streaming으로 채운다.
한 마디로: SSG + Streaming SSR의 하이브리드.
현재 next.config.js에서 experimental.ppr = true로 활성화할 수 있다.
Hydration을 아예 안 한다. 서버 상태를 직렬화해서 클라이언트가 그대로 이어받는다.

React 생태계에서는 아직 적용 안 됐고, Qwik 프레임워크가 사용하는 방식이다.
근데 개념 자체는 알아둘 가치가 있다.
또 SSR이다. 이 문제덩어리의 또 다른 문제는 서버가 HTML을 만들어서 보내주면, 브라우저는 그 HTML을 화면에 보여준 다음 JS를 다운로드하고 전체 컴포넌트 트리를 다시 실행해서 이벤트를 바인딩한다.
이게 hydration인데, 앱이 커질수록 비용이 겁나 비싸진다
[기존 SSR + Hydration]
서버: HTML 생성 → 전송
클라이언트: HTML 표시 → JS 다운로드 → 전체 컴포넌트 트리 재실행 → 이벤트 바인딩
^^^^^^^^^^^^^^^^^^^^^^^^
이 과정이 겁나비싸다
[Resumability]
서버: HTML + 직렬화된 상태 생성 → 전송
클라이언트: HTML 표시 → 사용자가 클릭한 부분의 JS만 로드 → 바로 실행
(전체적으로 재실행은 안함)
Resumability는 hydration 자체를 건너뛴다. 서버가 상태를 직렬화해서 HTML에 같이 보내주고, 클라이언트는 사용자가 실제로 인터랙션하는 부분의 JS만 필요할 때 로드한다. TTI(인터랙션까지 시간)가 거의 0에 가까워진다.
Qwik에서만 쓰이고 있고, React는 아직 도입이 안됐다.
SEO 필요한가?
├── NO → CSR (SPA)
└── YES
└── 데이터가 자주 바뀌나?
├── NO → SSG
└── YES
└── 실시간이어야 하나?
├── NO → ISR
└── YES
└── SSR (+ Streaming)
Next.js App Router를 쓰고 있다면 사실 고민할 게 줄어든다:
revalidate 옵션으로 ISR| 전략 | 렌더링 시점 | HTML 생성 위치 | 대표 사용처 |
|---|---|---|---|
| CSR | 런타임 (브라우저) | 클라이언트 | SPA (Vite + React) |
| SSG | 빌드 타임 | 서버 | 블로그, 문서 사이트 |
| SSR | 런타임 (요청마다) | 서버 | 개인화 페이지, 대시보드 |
| ISR | 빌드 + 주기적 재생성 | 서버 | 상품 목록, 뉴스 |
| Streaming SSR | 런타임 (점진적) | 서버 | Next.js App Router 기본 |
| RSC | 런타임 (서버 컴포넌트) | 서버 + 클라이언트 혼합 | Next.js App Router 기본 |
| PPR | 빌드 + 런타임 혼합 | 서버 | Next.js (experimental) |
| Resumability | 런타임 (hydration 없음) | 서버 → 클라이언트 이어받기 | Qwik |