
바이브 코더를 위한 Next.js 최적화 (2/3): 렌더링 전략
CSR, SSR, SSG, ISR, PPR까지 5가지 렌더링 전략의 차이와 선택 기준을 코드 예시로 정리했어요.
빈 HTML을 보내고 있다면
프로젝트를 배포했는데 Google 검색에 잡히지 않아요. Lighthouse 점수도 낮고요. 원인을 찾아보니 페이지 소스가 텅 비어 있어요.
전통적인 React SPA(Single Page Application)는 빈 HTML을 브라우저에 보내고, 자바스크립트가 화면을 그리는 방식이에요. 브라우저가 자바스크립트를 다운로드하고 실행할 때까지 사용자는 흰 화면만 보게 되죠. 검색 엔진 크롤러도 마찬가지예요. 자바스크립트를 실행하지 못하면 빈 페이지로 인식해요.
Next.js는 이 문제를 해결하기 위해 여러 렌더링 전략을 제공해요. 어떤 전략을 고르느냐에 따라 초기 로딩 속도, SEO(Search Engine Optimization, 검색 엔진 최적화), 서버 부하가 달라져요. 5가지 렌더링 전략을 비교하고, 상황별 선택 기준을 정리해 볼게요.
1편: 이미지 최적화
img vs Image, next.config.js, 비용 구조, 아키텍처
2편: 렌더링 전략 (현재 글)
CSR, SSR, SSG, ISR, PPR 비교와 선택 기준
3편: 코드 분할과 캐싱
dynamic import, Script 최적화, unstable_cache
5가지 렌더링 전략 한눈에 보기
Next.js에서 선택할 수 있는 렌더링 전략은 다섯 가지예요.
| 전략 | 렌더링 시점 | 초기 로딩 | SEO | 서버 부하 | 사용 사례 |
|---|---|---|---|---|---|
| CSR | 브라우저 | 느림 | 불리 | 낮음 | 관리자 페이지, 실시간 대시보드 |
| SSR | 요청마다 서버 | 빠름 | 좋음 | 높음 | 실시간 데이터(주식 차트) |
| SSG | 빌드 시 1회 | 최상 | 최상 | 없음 | 약관, 문서 |
| ISR | 빌드 + 주기적 갱신 | 빠름 | 최상 | 낮음 | 블로그, 뉴스, 상품 페이지 |
| PPR | 정적 껍데기 + 동적 스트리밍 | 빠름 | 좋음 | 중간 | 정적/동적 혼합 |
각 전략을 짧게 정리하면 이래요.
CSR(Client-Side Rendering, 클라이언트 사이드 렌더링)
브라우저에서 화면을 그려요. 서버는 빈 HTML만 보내고, 자바스크립트가 로드된 후에야 콘텐츠가 나타나죠. SEO가 필요 없는 관리자 페이지나 실시간 대시보드에서 주로 사용해요.
SSR(Server-Side Rendering, 서버 사이드 렌더링)
요청이 들어올 때마다 서버에서 HTML을 생성해요. 천만 건의 요청이 오면 천만 번 새로 그려내는 거예요. 서버 부하가 높은 대신, 항상 최신 데이터를 반영할 수 있죠. 주식 차트처럼 실시간성이 중요한 곳에서 사용해요.
SSG(Static Site Generation, 정적 사이트 생성)
가장 빠른 전략이에요. 빌드할 때 HTML을 한 번 만들어 두고, 이후 요청에는 만들어진 파일을 그대로 보내죠. 서버 부하가 없는 대신, 빌드 시점의 데이터로 고정돼요. 약관 페이지, 기술 문서처럼 수개월 단위로 바뀌는 곳에 적합하죠.
ISR(Incremental Static Regeneration, 점진적 정적 재생성)
SSG의 속도는 갖고 싶은데 데이터가 주기적으로 바뀐다면? ISR이 적합해요. revalidate: 60으로 설정하면 60초마다 백그라운드에서 페이지를 새로 만들어요. 블로그, 상품 목록, 뉴스 페이지에서 가장 자주 쓰이는 전략이에요.
PPR(Partial Prerendering, 부분 사전 렌더링)
정적인 껍데기를 먼저 보내고, 그 안에서 Suspense로 동적 데이터를 스트리밍으로 채워 넣는 방식이에요. 뒤에서 따로 다룰게요.
클라이언트 컴포넌트(Client Component) vs 서버 컴포넌트(Server Component)
Next.js App Router에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트예요. 파일 상단에 'use client'를 선언해야 클라이언트 컴포넌트가 되죠.
아래 두 코드를 비교해 보세요. 같은 데이터를 가져오지만 방식이 완전히 달라요.
'use client';
import { useState, useEffect } from 'react';
export default function BadBlogPost({ params }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/posts/${params.id}`)
.then(res => res.json())
.then(data => setPost(data));
}, [params.id]);
if (!post) return <div>Loading...</div>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Client Component는 브라우저에서 자바스크립트가 실행된 뒤 API를 호출해요. 그동안 사용자는 "Loading..." 텍스트만 보게 되고, 검색 엔진 크롤러도 실제 콘텐츠를 읽지 못하죠.
반면 Server Component는 서버에서 데이터를 가져온 뒤 완성된 HTML을 보내요. 브라우저에 도착하는 순간 콘텐츠가 바로 보이고, 검색 엔진도 정상적으로 인덱싱할 수 있어요.
차이를 정리하면:
- 서버 컴포넌트는
useState,useEffect없이async/await로 데이터를 가져와요 - 자바스크립트 번들에 포함되지 않으므로 번들 사이즈가 줄어요
- 데이터베이스나 파일 시스템에 직접 접근할 수 있어요
use client를 지우면 서버 컴포넌트가 되는 게 아닙니다. useState, useEffect 같은 클라이언트 전용 훅을 사용하고 있다면 로직 자체를 서버 사이드 방식으로 재설계해야 합니다.
SSG와 ISR 코드 예시
정적 생성과 주기적 갱신을 조합하는 패턴이에요. generateStaticParams로 빌드 시점에 페이지를 미리 만들고, revalidate로 갱신 주기를 설정해요.
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post) => ({ slug: post.slug }));
}
export const revalidate = 60; // 60초마다 갱신
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 60 },
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}이 코드가 동작하는 순서는 이래요:
- 빌드 시점:
generateStaticParams가 모든 슬러그를 가져와서 각 페이지를 미리 생성해요 - 배포 후: 사용자가 페이지를 요청하면 미리 만들어진 HTML을 바로 보내요 (SSG처럼 빠름)
- 60초 경과 후: 다음 요청이 오면 백그라운드에서 새 HTML을 생성하고, 완료되면 기존 페이지를 교체해요
SSG와 ISR 중 고민된다면 ISR을 선택하세요. revalidate: 60이면 콘텐츠를 수정한 뒤 최대 1분만 기다리면 반영돼요. SSG는 약관 페이지처럼 정말 안 바뀌는 곳에만 쓰면 충분해요.
SSG만으로 충분한 경우는 약관 페이지, 회사 소개 페이지처럼 콘텐츠가 수개월 단위로 바뀌는 곳이에요. 블로그, 상품 목록, 뉴스처럼 콘텐츠가 자주 업데이트되는 곳이라면 ISR을 선택하세요.
PPR: 점진적으로 도입할 수 있어요
PPR(Partial Prerendering)은 정적 렌더링과 동적 렌더링을 한 페이지 안에서 결합하는 전략이에요. 정적인 레이아웃(헤더, 사이드바)을 먼저 보내고, 동적인 영역(추천 상품, 사용자 정보)은 Suspense 경계를 기준으로 스트리밍으로 채워 넣죠.
Next.js 16부터 incremental 모드로 특정 라우트에만 점진적으로 적용할 수 있어요.
const nextConfig = {
experimental: {
ppr: 'incremental',
},
};PPR은 정적/동적 콘텐츠가 명확히 분리된 페이지에서 효과적이에요. 상품 상세 페이지(정적 정보 + 동적 추천)나 대시보드(정적 레이아웃 + 실시간 데이터)가 좋은 후보예요. 전체 프로젝트에 일괄 적용하기보다는 한두 페이지에서 먼저 시도해보세요.
어떤 전략을 골라야 할까?
페이지의 성격에 따라 판단하면 돼요.
- 콘텐츠가 사용자마다 다른가? → SSR
- 콘텐츠가 공개되어 있고 자주 바뀌는가? → ISR (
revalidate설정) - 콘텐츠가 거의 안 바뀌는가? → SSG
- 한 페이지에 정적/동적 영역이 공존하는가? → PPR
하나만 고를 필요는 없어요. Next.js는 페이지마다 다른 전략을 적용할 수 있으니까요. 블로그 페이지는 ISR, 관리자 페이지는 CSR, 상품 상세는 PPR처럼 페이지 성격에 맞춰 조합하는 방식이 효과적이에요.
AI로 렌더링 전략 점검하기
프로젝트의 렌더링 전략이 적절한지 AI에게 점검을 맡길 수 있어요. 아래 프롬프트를 AI 코딩 도구에 붙여 넣고 프로젝트 코드를 함께 전달하면, 파일별로 현재 방식과 권장 전략을 표로 정리해 줘요.
내 Next.js 프로젝트의 렌더링 전략을 점검해줘.
분석 기준:
1. use client가 불필요하게 사용된 컴포넌트를 찾아서 서버 컴포넌트로 전환 가능한지 확인
2. useEffect로 초기 데이터를 가져오는 패턴을 서버 사이드 페칭으로 변경 제안
3. 정적 데이터는 SSG, 주기적 갱신은 ISR, 실시간 데이터는 RSC + Suspense로 분류
아래 포맷으로 리포트를 작성해줘:
| 파일명 | 현재 방식 | 권장 전략 | 변경 근거 |이 프롬프트의 핵심은 판단 기준을 명확히 제시하는 거예요. "최적화해줘"라는 막연한 요청보다, 분석 기준 세 가지를 구체적으로 나열하면 일관된 결과를 받을 수 있어요.
TL;DR
useEffect로 데이터를 가져오고 있다면 서버 컴포넌트 전환을 검토하세요. SEO와 초기 로딩 속도가 개선돼요.- 대부분의 페이지는 ISR(
revalidate설정)로 충분해요. SSG보다 유연하고, SSR보다 서버 부하가 낮아요. use client를 지울 때는useState,useEffect같은 클라이언트 전용 로직을 먼저 제거하세요.- PPR은
incremental모드로 특정 페이지에 점진적으로 적용할 수 있어요. 정적/동적 영역이 명확한 페이지에서 시도해보세요. - AI에게 렌더링 전략 점검 프롬프트를 주면 파일별 개선 포인트를 빠르게 파악할 수 있어요.
참고 자료
- 원본 영상: 바이브 코더를 위한 웹사이트 최적화 — 코드팩토리 YouTube
- Next.js Rendering 공식 문서 — Next.js Documentation
- 렌더링 전략 선택 가이드 — Vercel 공식 블로그
- Next.js PPR 시작하기 — PPR 공식 가이드