JUNSEOK
05 · 심화 FE·20분·5개 레슨

렌더링 패턴

CSR, SSR, SSG, ISR, RSC.

학습 목표

  • CSR, SSR, SSG, ISR의 차이와 각각의 Trade-off를 이해한다
  • Hydration의 비용과 Partial/Progressive Hydration 전략을 학습한다
  • Streaming SSR과 React Server Components(RSC)가 해결하는 문제를 파악한다
  • 상황별로 올바른 렌더링 패턴을 선택할 수 있어야 한다

0. "렌더링 패턴"이 뭔데 — 초심자용

0-1. "이 HTML을 누가, 언제 만드는가"의 문제

웹 페이지는 결국 HTML이다. 이 HTML을 만들어내는 주체가 누구냐에 따라 이름이 달라진다.

주체이름
빌드 시점에 한 번 만들어서 저장SSG (Static Site Generation)
요청이 올 때 서버가 그 자리에서 생성SSR (Server-Side Rendering)
빌드 시 한 번 + 주기적으로 재생성ISR (Incremental Static Regeneration)
빈 껍데기만 보내고 브라우저가 JS로 생성CSR (Client-Side Rendering)
서버가 조금씩 흘려 보냄Streaming SSR
서버 컴포넌트 + 클라이언트 컴포넌트RSC (React Server Components)

0-2. 왜 이렇게 많아졌나

처음엔 단순했다: 서버가 HTML 다 만들어서 보냄 (2000년대의 PHP, JSP, Ruby on Rails — 지금 분류하면 "SSR"이다).

그러다 React/Vue 등장 → CSR 유행 → SEO·초기 로딩 느림 → SSR 부활 → Hydration 비용 문제 → Streaming, RSC 등장...

각 방식이 이전 방식의 단점을 해결하면서 생겨났다. 이 장은 그 역사와 트레이드오프를 정리한다.

0-3. 핵심 지표 미리

각 방식을 비교할 때 쓰는 지표들:

  • TTFB (Time To First Byte): 첫 바이트 도착 시간
  • FCP (First Contentful Paint): 뭔가 눈에 보이는 시점
  • LCP (Largest Contentful Paint): 핵심 콘텐츠가 보이는 시점
  • TTI (Time To Interactive): 클릭·입력이 먹히기 시작하는 시점
  • INP (Interaction to Next Paint): 상호작용 후 반응 시간

"빨리 보이는 것" 과 "빨리 상호작용 되는 것" 은 다르다. SSR은 전자는 빠르지만 후자가 느릴 수 있다.

0-4. Hydration이 뭔가 — 미리 맛보기

SSR 방식에서는 서버가 보내준 HTML이 그림일 뿐이다. 클릭 이벤트가 안 붙어있음. 브라우저에서 JS가 실행되고 나서야 "살아있는" 페이지가 된다. 이 "생명을 불어넣는" 과정이 Hydration(수화).

Hydration이 느리면 → 화면은 보이는데 버튼 눌러도 반응 없음. 이 문제를 해결하려고 Partial Hydration, Islands, RSC가 나왔다.

0-5. FE 개발자가 이 장을 배우면 할 수 있는 것

  • Next.js, Remix, Astro, Nuxt 의 설계 철학 비교
  • "우리 서비스는 CSR이 맞을까 SSR이 맞을까?" — 근거 있는 판단
  • 성능 이슈 디버깅 시 렌더링 전략을 의심
  • RSC 같은 신기술이 왜 필요한지 납득

0-6. 선행 장 연결

  • 2-5: 브라우저가 HTML을 화면으로 그리는 과정
  • 2-9: SSG/ISR이 CDN 캐시와 어떻게 엮이나
  • 5-1: Hydration 비용이 왜 엔진 레벨에서 발생하나

1. 렌더링 패턴이 중요한 이유

웹 페이지가 사용자에게 보이기까지는 여러 단계를 거친다.

  1. HTML 생성 — 어디서? 서버/빌드타임/브라우저
  2. JS 로드·실행 — 언제?
  3. 인터랙션 가능 — 얼마나 빨리?

렌더링 패턴은 이 3단계를 어떻게 배치하느냐의 문제이다. 배치에 따라 TTFB / FCP / LCP / TTI / INP가 크게 달라진다.


2. CSR (Client-Side Rendering)

2-1. 동작 방식

브라우저 → 서버에서 빈 HTML + JS 번들 받음
JS 실행 → DOM 생성 → 데이터 fetch → 렌더링
<!-- 서버가 내려주는 초기 HTML -->
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

대표 스택: Create React App, Vite SPA

2-2. 장단점

장점단점
서버 부하 적음초기 로딩 느림 (빈 화면 시간 길음)
SPA 전환 빠름SEO 약함 (JS 실행 후 내용 채워짐)
인프라 단순 (정적 호스팅)LCP/FCP 나쁨

2-3. 언제 써야 하나

  • 로그인 뒤에만 쓰는 대시보드/어드민 — SEO 불필요, 초기 로딩 단 한 번
  • 내부 툴 — 검색엔진 크롤러 노출 불필요

⚠️ 함정

  • 요즘 Google은 JS를 실행하지만 지연이 있고, Bing이나 SNS 공유 크롤러는 JS를 실행하지 않는다. 마케팅 페이지를 CSR로 만들면 공유 미리보기가 비어 있다.

3. SSR (Server-Side Rendering)

3-1. 동작 방식

요청마다 서버가 HTML을 생성해서 내려준다.

브라우저 → 서버 요청
서버 → React 컴포넌트 렌더링 → 완성된 HTML 반환
브라우저 → HTML 즉시 표시 (FCP 빠름)
JS 로드 → Hydration → 인터랙티브

대표 스택: Next.js getServerSideProps / App Router dynamic, Remix, Nuxt SSR

3-2. Hydration이란

서버가 렌더링한 정적 HTML에 이벤트 핸들러를 연결해 인터랙티브하게 만드는 과정이다.

// 서버에서 HTML 문자열로 변환
const html = renderToString(<App />);

// 클라이언트에서 기존 DOM에 이벤트 리스너 붙이기
hydrateRoot(document.getElementById('root'), <App />);

⚠️ Hydration Mismatch: 서버와 클라이언트 출력이 다르면 에러 발생. Date.now(), Math.random(), window.innerWidth처럼 환경에 따라 달라지는 값 사용 금지.

// ❌ 서버와 클라이언트가 다른 값을 출력
function Clock() {
  return <div>{new Date().toLocaleTimeString()}</div>;
}

// ✅ useEffect로 클라이언트 마운트 후 업데이트
function Clock() {
  const [time, setTime] = useState(null);
  useEffect(() => setTime(new Date().toLocaleTimeString()), []);
  return <div>{time ?? 'Loading...'}</div>;
}

3-3. 장단점

장점단점
SEO 좋음서버 부하 큼 (매 요청마다 렌더링)
FCP/LCP 빠름TTFB가 데이터 fetch 시간에 비례
개인화 페이지에 적합TTI는 Hydration 때문에 느릴 수 있음

3-4. TTFB vs FCP Trade-off

  • SSR: 서버에서 데이터 fetch·렌더링 완료 후 응답 → TTFB 느림, FCP 빠름
  • CSR: 빈 HTML 즉시 응답 → TTFB 빠름, FCP 느림

⚠️ 느린 API가 SSR TTFB를 죽인다. SSR 페이지에서 3초 걸리는 API를 호출하면 사용자는 3초간 흰 화면을 본다. → Streaming SSR 필요.


4. SSG (Static Site Generation)

4-1. 동작 방식

빌드 타임에 미리 HTML을 생성해서 CDN에 올린다.

빌드 : 모든 페이지를 HTML 파일로 생성 → CDN 배포
요청 : CDN이 HTML 반환 (서버 없음)

대표 스택: Next.js getStaticProps / Gatsby / Astro / Hugo / Jekyll

// Next.js pages router
export async function getStaticProps() {
  const posts = await fetchPosts();
  return { props: { posts } };
}

export async function getStaticPaths() {
  const slugs = await fetchAllSlugs();
  return {
    paths: slugs.map(slug => ({ params: { slug } })),
    fallback: false,
  };
}

4-2. 장단점

장점단점
매우 빠름 (CDN 캐시)데이터 업데이트되면 재빌드 필요
서버 부하 0빌드 시간 증가 (페이지 수 많으면)
SEO 최상개인화 불가

4-3. 언제 써야 하나

  • 블로그, 문서 사이트, 마케팅 페이지, 제품 카탈로그
  • 데이터 변경 빈도가 시간/일 단위로 낮은 경우

⚠️ 10,000개 페이지를 빌드하면 빌드 시간이 수십 분이 될 수 있다. → ISR로 해결.


5. ISR (Incremental Static Regeneration)

5-1. 동작 방식

SSG + 주기적 재생성. 처음에는 정적 HTML, 일정 시간 후 백그라운드에서 재빌드.

export async function getStaticProps() {
  const data = await fetchData();
  return {
    props: { data },
    revalidate: 60, // 60초 후 stale, 다음 요청 시 재생성
  };
}

동작 흐름 (stale-while-revalidate):

  1. 첫 사용자: 정적 HTML 반환
  2. 60초 경과 후 첫 사용자: stale HTML 반환 + 백그라운드 재생성
  3. 그 다음 사용자: 재생성된 신선한 HTML 반환

5-2. On-demand Revalidation

// API 라우트에서 특정 페이지를 즉시 재생성
res.revalidate('/blog/my-post');

CMS에서 콘텐츠 발행하자마자 해당 페이지만 재빌드 가능.

5-3. 장단점

장점단점
SSG의 속도 + SSR의 신선함일시적 stale 데이터 노출
빌드 시간 단축 (필요 시 생성)인프라 복잡 (Vercel/Netlify 의존)

6. Streaming SSR

6-1. 왜 필요한가

기존 SSR은 모든 데이터가 준비될 때까지 응답을 지연시킨다.

요청 → [DB 쿼리 500ms] [API 호출 2000ms] → HTML 응답 → 렌더링
TTFB 2.5초

Streaming SSR은 HTML을 청크 단위로 스트리밍한다.

요청 → [Header 즉시 응답] → [본문 A 스트림] → [본문 B 스트림]
TTFB 50ms

6-2. React Suspense + renderToPipeableStream

import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(
    <html>
      <body>
        <Header />
        <Suspense fallback={<Spinner />}>
          <SlowComponent /> {/* 데이터 fetch 중이어도 먼저 껍데기 반환 */}
        </Suspense>
      </body>
    </html>,
    {
      onShellReady() {
        pipe(res); // 껍데기 준비되면 스트리밍 시작
      },
    }
  );
});

동작:

  1. <Header />는 즉시 HTML로 스트리밍 → 사용자에게 보임
  2. <SlowComponent /><Spinner />로 먼저 렌더
  3. 서버에서 데이터 준비되면 해당 자리만 나중에 HTML 스트리밍 + 런타임 JS로 교체

6-3. Selective Hydration

React 18은 스트리밍된 순서가 아니라, 사용자가 상호작용하는 순서대로 Hydration한다.

사용자가 버튼을 클릭하면 그 컴포넌트부터 Hydration → TTI 체감 개선.


7. React Server Components (RSC)

7-1. 기존 SSR과 무엇이 다른가

항목SSRRSC
컴포넌트 JS가 클라이언트로모두 번들에 포함서버 컴포넌트 JS는 클라이언트로 안 감
Hydration모든 컴포넌트클라이언트 컴포넌트만
DB 직접 접근불가 (API 분리)가능
번들 크기풀 번들서버 컴포넌트만큼 감소

7-2. 예제

// app/posts/page.jsx  (기본: 서버 컴포넌트)
import { db } from '@/lib/db';
import LikeButton from './LikeButton';

export default async function PostsPage() {
  const posts = await db.post.findMany(); // 서버에서 DB 직접 접근
  return (
    <div>
      {posts.map(p => (
        <article key={p.id}>
          <h2>{p.title}</h2>
          <LikeButton postId={p.id} /> {/* 클라이언트 컴포넌트 */}
        </article>
      ))}
    </div>
  );
}
// app/posts/LikeButton.jsx
'use client'; // 클라이언트 컴포넌트 선언
import { useState } from 'react';

export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '♥' : '♡'}</button>;
}

7-3. 직렬화 경계

서버 컴포넌트 → 클라이언트 컴포넌트로 넘길 수 있는 props는 JSON 직렬화 가능한 것만. 함수·Date·Map·Class 인스턴스 불가.

// ❌ 서버 → 클라이언트에 함수 전달 불가
<ClientComp onClick={() => { /* ... */ }} />

// ✅ Server Action은 가능
<ClientComp action={async () => { 'use server'; /* ... */ }} />

7-4. Server Actions

폼 제출을 서버 함수로 직접 처리.

// 서버 컴포넌트
async function createPost(formData) {
  'use server';
  await db.post.create({ data: { title: formData.get('title') } });
}

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

8. Partial / Islands / Progressive Hydration

8-1. 문제 의식

전통적 SSR은 페이지 전체를 Hydration한다. 버튼 하나 움직이려고 100KB JS를 다 실행한다.

8-2. Islands Architecture (Astro)

정적 HTML이 기본, 인터랙티브한 부분만 "섬"처럼 Hydration한다.

---
import Counter from './Counter.svelte';
---
<html>
  <body>
    <h1>정적 콘텐츠 (JS 0KB)</h1>
    <Counter client:visible /> <!-- 뷰포트에 들어올 때만 Hydration -->
  </body>
</html>

client:load, client:visible, client:idle, client:media 지시자로 Hydration 타이밍 제어.

8-3. Progressive Hydration

중요한 컴포넌트부터 순차 Hydration. React 18 startTransition + Suspense 경계로 구현.


9. 렌더링 패턴 선택 의사결정

데이터가 모든 사용자에게 동일한가?
├─ Yes → 변경이 잦은가?
│   ├─ 거의 안 변함 → SSG
│   ├─ 주기적 변경 → ISR
│   └─ 실시간 → SSR
└─ No (개인화) → SEO 필요한가?
    ├─ Yes → SSR (+ Streaming)
    └─ No → CSR

실무 조합 예시:

  • 마케팅 페이지: SSG (Astro/Next SSG)
  • 제품 상세: ISR (빌드 타임 + 60초 재검증)
  • 검색 결과: SSR Streaming (쿼리별 동적)
  • 마이페이지: CSR 또는 SSR (로그인 쿠키 기반)
  • 어드민: CSR

10. 코어 지표와 렌더링 패턴

지표CSRSSRSSGISRRSC
TTFB빠름느림매우 빠름매우 빠름보통
FCP느림빠름매우 빠름매우 빠름빠름
LCP느림빠름매우 빠름매우 빠름빠름
TTI보통느림 (Hydration)느림느림빠름 (클라 JS 감소)
SEO약함좋음매우 좋음매우 좋음좋음

11. 실무 체크리스트

  • 페이지별 SEO 필요 여부·개인화 여부·데이터 변경 빈도를 정리했는가
  • SSR 페이지의 TTFB가 API 응답 속도에 묶여 있는가 → Streaming 도입 검토
  • Hydration Mismatch 경고가 콘솔에 뜨지 않는가
  • Date.now(), Math.random(), window 접근을 SSR 렌더링에서 사용하지 않는가
  • 클라이언트 컴포넌트에 불필요한 무거운 라이브러리가 들어있지 않은가 (RSC로 이동 가능한지)
  • ISR revalidate 값이 비즈니스 신선도 요구와 맞는가
  • 크롤러별 JS 실행 정책을 파악했는가 (공유 미리보기 포함)

12. 연습 문제

Q1. CSR과 SSR의 가장 큰 차이를 TTFB·FCP 관점에서 설명하라.

정답
  • CSR: 빈 HTML을 즉시 내려주므로 TTFB는 빠르나, JS 로드·실행·데이터 fetch 후에야 화면이 보이므로 FCP가 느리다.
  • SSR: 서버가 데이터 조회·렌더링을 완료한 뒤 HTML을 내려주므로 TTFB는 느리나, HTML 자체에 콘텐츠가 있으므로 FCP는 빠르다.

Q2. Hydration Mismatch가 발생하는 대표적 상황 3가지를 들고, 해결법을 제시하라.

정답
  1. new Date() 사용 — 서버 시간과 클라이언트 시간 불일치 → useEffect에서 클라이언트 마운트 후 설정
  2. window.innerWidth 같은 브라우저 API — 서버에는 window 없음 → typeof window !== 'undefined' 체크 또는 useEffect
  3. Math.random() — 서버·클라이언트 다른 값 → 서버에서 한 번만 계산해 props로 전달하거나 mount 후 설정

Q3. SSG의 한계를 보완하는 ISR의 revalidate 메커니즘을 stale-while-revalidate 관점에서 설명하라.

정답

ISR은 정적 HTML을 캐싱하되 revalidate 시간이 지나면 stale로 마크한다. 그 이후 첫 요청자는 오래된 HTML을 즉시 받고(빠른 응답), 백그라운드에서 새 HTML을 생성한다. 두 번째 요청자부터는 새 HTML을 받는다. 따라서 사용자 응답 속도는 SSG 수준, 데이터 신선도는 revalidate 주기 수준을 유지한다.

Q4. Streaming SSR이 기존 SSR의 어떤 문제를 해결하는지 설명하라.

정답

기존 SSR은 가장 느린 데이터 소스에 TTFB가 묶인다. 느린 API 하나가 있으면 전체 응답이 그만큼 지연된다. Streaming SSR은 Suspense 경계로 페이지를 쪼개, 준비된 부분부터 먼저 HTML 청크를 내려보낸다. 사용자는 빠른 부분을 먼저 보고, 느린 부분은 스피너 → 스트림으로 교체되는 것을 본다. TTFB와 FCP가 사실상 가장 빠른 컴포넌트 속도로 결정된다.

Q5. RSC(React Server Components)가 기존 SSR과 번들 크기 측면에서 어떻게 다른가?

정답

기존 SSR은 모든 컴포넌트의 JS를 클라이언트 번들에 포함하고 Hydration 시 실행한다. RSC는 서버 컴포넌트의 JS를 클라이언트 번들에 포함하지 않는다. 서버에서 렌더링된 결과(직렬화된 RSC Payload)만 클라이언트로 전송된다. 따라서 DB 드라이버·마크다운 파서 같은 무거운 의존성을 서버에만 두고 번들 크기를 크게 줄일 수 있으며, 해당 컴포넌트는 Hydration도 필요 없다.

Q6. 다음 페이지에 가장 적절한 렌더링 패턴을 각각 고르고 이유를 설명하라. (a) 개인별 추천이 표시되는 쇼핑몰 홈 (b) 마크다운 기반 기술 블로그 (c) 사내 데이터 분석 대시보드 (d) 뉴스 포털의 기사 상세

정답
  • (a): SSR + Streaming — 개인화 필요(SSR) + SEO 중요(쇼핑몰 홈) + 상품 카드 여러 개가 서로 다른 API → Streaming으로 부분별 렌더
  • (b): SSG — 콘텐츠 변경 빈도 낮음, SEO 중요, 서버 비용 최소
  • (c): CSR — 로그인 필수로 SEO 불필요, 초기 로딩 속도보다 상호작용 후 빠른 UX 중요
  • (d): ISR — 기사는 한 번 게시되면 거의 변하지 않으나 수정·속보 반영 필요, 페이지 수 많아 전량 SSG 빌드 부담

Q7. Islands Architecture(Astro)가 React SSR과 비교해 어떤 이점이 있는가?

정답

React SSR은 페이지 전체를 Hydration하므로, 정적 텍스트로만 구성된 부분도 JS가 로드·실행된다. Islands Architecture는 기본이 정적 HTML이고 인터랙티브 "섬"만 부분 Hydration한다. 따라서 마케팅/문서 페이지처럼 대부분이 정적이고 일부만 인터랙티브할 때 JS를 극적으로 줄일 수 있다(0KB에 근접 가능). 단, 아일랜드 간 상태 공유가 어렵다는 단점이 있다.


13. 정리

  • CSR은 SEO·초기 로딩을 희생하고 서버 부하를 줄인다.
  • SSR은 SEO·FCP를 얻지만 TTFB와 Hydration 비용이 든다.
  • SSG는 빌드 타임 HTML로 최고 속도를 얻지만 동적 데이터에 약하다.
  • ISR은 SSG + 주기적 재생성으로 신선함을 보완한다.
  • Streaming SSR은 Suspense로 청크 단위 HTML을 내려 TTFB를 해제한다.
  • RSC는 서버 컴포넌트 JS를 클라에 보내지 않아 번들을 줄인다.
  • Islands는 기본 정적, 일부만 Hydration으로 JS를 최소화한다.
  • 페이지 특성에 따라 한 앱에서도 여러 패턴을 조합하는 것이 실무 표준이다.

← 5-4. 모듈 시스템과 번들러 | 5-6. 웹 성능 최적화 →

진도 체크시작 전
NEXT · 5-6

웹 성능 최적화

Core Web Vitals(LCP, CLS, INP), 번들·이미지.

이어서 학습하기 →