Archive.sakamoto
RenderingNext.js

CSR, SSR, SSG, ISR, RSC 렌더링 전략

5가지 렌더링 방식의 차이와 선택 기준을 한 번에 정리해보자!

Sakamoto·

1. 핵심 개념

웹 페이지는 "언제, 어디서 렌더링하느냐" 에 따라 완전히 다른 방식으로 동작한다.

레스토랑에 비유하면 이렇다.

  • SSG → 미리 만들어둔 도시락. 주문 오면 바로 줌
  • ISR → 도시락인데, 일정 시간 지나면 새로 만들어둠
  • SSR → 주문 들어올 때마다 그 자리에서 요리
  • CSR → 재료만 보내고, 손님이 직접 요리
  • RSC → SSR이지만, 클라이언트에 JS를 최소한으로 보내는 방식

기술적 정의: 렌더링이란 HTML을 생성하는 작업이다. 이 작업을 빌드 타임 / 요청 타임 / 클라이언트 중 어디서 하느냐가 5가지 전략을 가르는 핵심이다.


2. 왜 이렇게 설계하는가 ⭐

하나의 방식으로 모든 페이지를 처리하면 어떤 문제가 생길까?

  • CSR만 쓰면 → 검색 엔진에 안 잡히고, 초기 로딩이 느림
  • SSR만 쓰면 → 서버가 모든 요청을 매번 처리해야 해서 트래픽이 몰리면 부하가 급증
  • SSG만 쓰면 → 콘텐츠가 자주 바뀌는 페이지는 재배포 없이 갱신 불가

그래서 Next.js는 페이지 단위로 렌더링 전략을 다르게 가져갈 수 있도록 설계됐다. 하나의 앱 안에서도 /about은 SSG로, /dashboard는 SSR로, /feed는 ISR로 운영하는 게 실무 패턴이다.

 

💡
면접 tip
"Next.js를 왜 쓰나요?" 질문에 "파일 기반 라우팅이 편해서"보다,
"페이지마다 최적의 렌더링 전략을 선택할 수 있어서 성능과 SEO를 동시에 잡을 수 있기 때문"이라고 답하면 훨씬 임팩트 있다.

3. 각 전략 개념 + 흐름

CSR (Client Side Rendering)

브라우저에서 JS를 받아 직접 렌더링한다. 서버는 빈 HTML + JS 번들만 준다.

// [요청] 브라우저 → 서버: 페이지 요청
// [응답] 서버 → 브라우저: 빈 HTML + bundle.js
// [처리] 브라우저: JS 실행 → React가 DOM 생성
// [출력] 완성된 화면 표시 + 인터랙션 가능
 
// index.html에는 <div id="root"></div>만 있고
// bundle.js가 로드되면서 React가 DOM을 채움

✅ CSR이 적합한 경우

관리자 페이지(Admin), 로그인 후 대시보드, SEO가 필요 없는 내부 서비스

SSR (Server Side Rendering)

요청이 올 때마다 서버에서 HTML을 완성해서 보낸다.

// [요청] 브라우저 → 서버: /product/123 요청
// [처리] 서버: DB 조회 → HTML 완성
// [응답] 완성된 HTML → 브라우저에서 즉시 표시
// [Hydration] JS 로드 후 → 인터랙션 활성화
 
// Next.js Pages Router 기준
export async function getServerSideProps(context) {
  // [입력] 요청마다 실행, context에서 params/cookies 접근 가능
  const res = await fetch(`https://api.example.com/product/${context.params.id}`);
  const product = await res.json();
 
  // [출력] props로 컴포넌트에 전달
  return { props: { product } };
}

🤔 Hydration

서버에서 렌더링된 HTML을 클라이언트의 React 애플리케이션으로 변환하고 상호작용할 수 있도록 만드는 과정
단점도 존재한다. JS 번들이 로드되기 전까지 버튼 클릭 등 상호작용 불가
서버 HTML과 클라이언트 결과가 다르면 Hydration Error 발생

✅ SSR이 적합한 경우

실시간 데이터가 필요한 페이지(커머스 상품, 뉴스 피드), 로그인한 유저 맞춤 페이지

SSG (Static Site Generation)

빌드 시점에 HTML을 미리 만들어둔다. 요청이 와도 서버 작업 없이 파일을 그대로 준다.

// [빌드 타임] 서버: 데이터 fetch → HTML 파일 생성 → 저장
// [요청] 브라우저 → CDN: 페이지 요청
// [응답] CDN → 브라우저: 미리 만들어둔 HTML 즉시 반환
// [Hydration] JS 로드 후 → 인터랙션 활성화
 
export async function getStaticProps() {
  // [입력] 빌드 타임에 단 한 번만 실행
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
 
  // [출력] 빌드된 HTML에 데이터 포함
  return { props: { posts } };
}

✅ SSG가 적합한 경우

블로그, 문서 사이트, 회사 소개 페이지, 변경이 적은 상품 상세 페이지

ISR (Incremental Static Regeneration)

SSG의 정적 파일 방식을 유지하면서, 일정 시간마다 백그라운드에서 페이지를 갱신한다.

// 1. 최초 빌드 → 정적 HTML 생성 + 캐싱
// 2. revalidate 시간 내 요청 → 캐시 그대로 응답
// 3. revalidate 시간 초과 후 첫 요청
//    → 오래된 캐시 즉시 응답 (UX 유지)
//    → 백그라운드에서 새 HTML 생성 시작
// 4. 재생성 완료 → 이후 요청부터 새 페이지 제공
 
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/news');
  const news = await res.json();
 
  return {
    props: { news },
    revalidate: 60, // [핵심] 60초마다 백그라운드 재생성
  };
}

On-Demand Revalidation: 시간 기반 말고 특정 이벤트(관리자 글 수정, 배포 등)에 직접 갱신 트리거하는 방식. API Route에서 res.revalidate(path) 호출로 구현한다.

✅ ISR이 적합한 경우

뉴스, 이커머스 상품 리스트, 콘텐츠가 주기적으로 바뀌는 페이지

RSC (React Server Component)

컴포넌트 단위로 서버/클라이언트를 분리한다. SSR과 달리 서버에서 렌더링한 결과를 JS가 아닌 **직렬화된 UI 데이터(RSC Payload)**로 전달해 번들 크기를 줄인다.

// ❌ 기존 방식: 서버에서 HTML 생성하지만 라이브러리도 클라이언트 번들에 포함됨
// bundle.js에 marked, sanitize-html 전부 포함 → 번들 비대화
 
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
 
function Page({ page }) {
  const [content, setContent] = useState('');
  // [처리] 렌더 후, fetch로 데이터를 불러옴 → 추가 네트워크 왕복 발생
  useEffect(() => {
    fetch(`/api/content/${page}`).then((data) => {
      setContent(data.content);
    });
  }, [page]);
 
  return <div>{sanitizeHtml(marked(content))}</div>;
}
// ✅ RSC 방식: 서버 전용 컴포넌트
// 'use client' 없으면 기본이 Server Component (App Router 기준)
 
// [처리] 서버에서만 실행 → 아래 라이브러리는 클라이언트 번들에 포함되지 않음
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
 
async function Page({ page }) {
  // [입력] 서버에서 직접 파일/DB 접근 가능 → fetch 왕복 없음
  const content = await readFile(`posts/${page}.md`);
 
  // [출력] 완성된 HTML 구조로 반환
  return <div>{sanitizeHtml(marked(content))}</div>;
}

App Router에서의 기본 규칙

  • 기본 컴포넌트 = Server Component
  • useState, useEffect, 브라우저 API가 필요하면 'use client' 선언
  • Server Component 안에 Client Component를 포함하는 건 가능

✅ RSC가 적합한 경우

데이터 fetching이 많은 페이지, DB/파일에 직접 접근하는 컴포넌트, 번들 크기를 줄여야 하는 경우


4. 비교 및 선택 기준

구분렌더링 시점SEO초기 속도서버 부하데이터 신선도
CSR브라우저느림낮음실시간
SSR요청 시빠름높음실시간
SSG빌드 시매우 빠름거의 없음빌드 시점
ISR빌드+주기빠름낮음주기적
RSC요청 시(서버)빠름중간실시간

5. 실무 렌더링 전략 선택 흐름

페이지에 실시간 데이터가 필요한가?
├── YES → 로그인/개인화 데이터인가?
│         ├── YES → SSR 또는 RSC (App Router 권장)
│         └── NO  → SSR (커머스, 뉴스)

└── NO  → 콘텐츠가 주기적으로 바뀌는가?
           ├── YES → ISR (갱신 주기 설정)
           └── NO  → SEO가 필요한가?
                      ├── YES → SSG (블로그, 랜딩페이지)
                      └── NO  → CSR (어드민, 대시보드)
🔧
실무 point
실무에서는 하나의 앱에서 전략을 섞어 쓴다.
/ → SSG, /products/[id] → ISR, /dashboard → CSR or SSR,
공통 레이아웃·네비게이션 → RSC (App Router 기준)

6. 자주 하는 실수 / 주의사항

SSR인데 데이터 캐싱 없이 운영

✅ SSR에 적절한 캐싱 전략 추가 (Next.js fetch 캐시 옵션 활용)

🔎 이유: 요청마다 DB/API를 치면 트래픽 증가 시 서버 부하가 폭증한다


App Router에서 모든 컴포넌트에 'use client' 추가

✅ 인터랙션이 필요한 최소 단위에만 선언

🔎 이유: 'use client' 선언 시 해당 컴포넌트와 그 하위 전체가 클라이언트 번들에 포함됨. RSC의 번들 최적화 이점이 사라진다


SSG로 구현했는데 데이터가 안 바뀐다고 신고 받음

✅ 갱신이 필요한 페이지는 ISR 또는 SSR 사용

🔎 이유: SSG는 빌드 시 데이터가 고정됨. 재배포 없이는 업데이트 불가


7. 한 줄 요약

CSR은 브라우저에서, SSR은 요청 시 서버에서, SSG는 빌드 시 서버에서 렌더링하며,
ISR은 SSG에 주기적 갱신을 더한 방식이고, RSC는 컴포넌트 단위로 서버/클라이언트를 분리해 번들을 최소화한다.
Next.js는 이 5가지를 페이지/컴포넌트 단위로 혼합 적용할 수 있어서 성능과 SEO를 함께 최적화할 수 있다.

 

💡
면접 tip
"ISR과 SSR의 차이는?" 질문엔 "SSR은 요청마다 서버에서 렌더링하지만, ISR은 정적 파일을 제공하면서 백그라운드에서 주기적으로만 갱신하기 때문에 서버 부하가 훨씬 낮고 응답 속도가 일정하다"고 답하면 된다.
CSR은 브라우저에서, SSR은 요청 시 서버에서, SSG는 빌드 시 서버에서 렌더링하며, ISR은 SSG에 주기적 갱신을 더한 방식이고, RSC는 컴포넌트 단위로 서버/클라이언트를 분리해 번들을 최소화한다.
Next.js는 이 5가지를 페이지/컴포넌트 단위로 혼합 적용할 수 있어서 성능과 SEO를 함께 최적화할 수 있다.