Next.js의 렌더링 방식(SSR, SSG, ISR, CSR)을 비교 설명해주세요.
힌트
빌드 시점 vs 요청 시점, 데이터 신선도와 성능의 트레이드오프를 생각해보세요.
정답 및 해설
Next.js의 렌더링 방식(SSR, SSG, ISR, CSR)을 비교 설명해주세요.
Next.js는 하나의 프레임워크에서 SSR, SSG, ISR, CSR 네 가지 렌더링 전략을 모두 지원합니다. 각 방식은 HTML을 언제, 어디서 생성하느냐에 따라 구분되며, 데이터의 신선도와 성능 사이의 트레이드오프가 다릅니다.
렌더링 방식 개요
| 방식 | HTML 생성 시점 | 데이터 최신성 | 속도 | SEO |
|---|---|---|---|---|
| SSG | 빌드 시 | 낮음 (빌드 시점) | 매우 빠름 | 좋음 |
| ISR | 빌드 + 주기적 갱신 | 중간 | 빠름 | 좋음 |
| SSR | 요청마다 | 높음 (항상 최신) | 느림 | 좋음 |
| CSR | 클라이언트 (런타임) | 높음 | 초기 느림 | 나쁨 |
SSG (Static Site Generation)
개념
빌드 타임에 HTML을 미리 생성합니다. 생성된 HTML은 CDN에 캐싱되어 요청이 오면 즉시 반환합니다.
빌드 타임:
데이터 조회 → HTML 생성 → 파일 저장
요청 시:
사용자 → CDN → 저장된 HTML 즉시 반환
Pages Router에서 SSG
// pages/blog/[id].jsx
// 어떤 경로(id)를 미리 빌드할지 정의
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return {
paths: posts.map(post => ({ params: { id: String(post.id) } })),
fallback: false, // 정의되지 않은 경로는 404
};
}
// 빌드 시 각 경로에 대해 데이터 조회
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.id}`)
.then(r => r.json());
return {
props: { post },
};
}
export default function BlogPost({ post }) {
return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}
App Router에서 SSG
// app/blog/[id]/page.jsx
// App Router에서는 기본적으로 fetch 결과를 캐싱 (SSG와 유사)
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map(post => ({ id: String(post.id) }));
}
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.id}`, {
cache: 'force-cache', // 빌드 시 캐시 (SSG)
}).then(r => r.json());
return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}
적합한 사용 사례: 블로그, 마케팅 페이지, 문서, 공지사항
ISR (Incremental Static Regeneration)
개념
SSG의 성능과 SSR의 데이터 신선도를 절충한 방식입니다. 빌드 시 HTML을 생성하지만, 지정된 시간(revalidate)이 지나면 백그라운드에서 페이지를 재생성합니다.
첫 번째 요청: 빌드된 HTML 반환 (즉시)
60초 후 요청: 오래된 HTML 반환 + 백그라운드 재생성 시작
재생성 완료 후 요청: 새로운 HTML 반환
Pages Router에서 ISR
// pages/products/[id].jsx
export async function getStaticProps({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then(r => r.json());
return {
props: { product },
revalidate: 60, // 60초마다 백그라운드에서 재생성
};
}
App Router에서 ISR
// app/products/[id]/page.jsx
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 }, // 60초마다 재검증
}).then(r => r.json());
return <div><h1>{product.name}</h1><p>가격: {product.price}원</p></div>;
}
On-demand Revalidation (수동 재검증)
시간 기반이 아닌 이벤트 기반으로 페이지를 재생성할 수 있습니다.
// pages/api/revalidate.js
export default async function handler(req, res) {
const secret = req.query.secret;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
await res.revalidate(`/products/${req.query.id}`);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
적합한 사용 사례: 상품 페이지, 뉴스 기사, 가격이 자주 바뀌지 않는 데이터
SSR (Server-Side Rendering)
개념
요청이 올 때마다 서버에서 HTML을 새로 생성합니다. 항상 최신 데이터를 보여주지만, 서버에서 데이터를 조회하고 HTML을 생성하는 시간이 필요하여 TTFB(Time to First Byte)가 느릴 수 있습니다.
Pages Router에서 SSR
// pages/dashboard.jsx
export async function getServerSideProps(context) {
const { req, res, params, query } = context;
// 요청마다 실행 — 항상 최신 데이터
const session = await getSession(req);
if (!session) {
return { redirect: { destination: '/login', permanent: false } };
}
const data = await fetch(`https://api.example.com/dashboard`, {
headers: { Authorization: `Bearer ${session.token}` }
}).then(r => r.json());
return { props: { data, user: session.user } };
}
export default function Dashboard({ data, user }) {
return <div><h1>{user.name}의 대시보드</h1>...</div>;
}
App Router에서 SSR
// app/dashboard/page.jsx
// cache: 'no-store'로 캐싱을 비활성화하여 SSR 동작
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard', {
cache: 'no-store', // 매 요청마다 새로 가져옴 (SSR)
}).then(r => r.json());
return <div>...</div>;
}
적합한 사용 사례: 사용자별 개인화 페이지, 실시간 주식 가격, 로그인 필요 페이지
CSR (Client-Side Rendering)
개념
서버는 빈 HTML과 JavaScript 번들만 전달하고, 브라우저에서 JavaScript가 실행되면서 화면을 그립니다. 초기 로딩이 느리고 SEO에 불리하지만, 이후 상호작용은 빠릅니다.
// 'use client'가 있는 클라이언트 컴포넌트에서 데이터 페칭
'use client';
import { useState, useEffect } from 'react';
export default function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/me')
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) return <div>로딩 중...</div>;
return <div>{user.name}님 환영합니다.</div>;
}
SWR / React Query를 사용한 CSR (권장)
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
export default function UserProfile() {
const { data, error, isLoading } = useSWR('/api/me', fetcher, {
refreshInterval: 30000, // 30초마다 자동 갱신
});
if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>오류가 발생했습니다.</div>;
return <div>{data.name}님 환영합니다.</div>;
}
적합한 사용 사례: 실시간으로 변하는 데이터(채팅, 알림), SEO 불필요한 관리자 페이지, 사용자 인터랙션이 많은 컴포넌트
App Router의 기본 동작 (Next.js 13+)
App Router에서는 컴포넌트가 기본적으로 서버 컴포넌트이며, fetch 옵션으로 렌더링 방식을 제어합니다.
// 빌드 시 1회 캐시 (SSG)
fetch(url, { cache: 'force-cache' });
// 매 요청마다 최신 데이터 (SSR)
fetch(url, { cache: 'no-store' });
// 60초마다 재검증 (ISR)
fetch(url, { next: { revalidate: 60 } });
렌더링 방식 선택 가이드
데이터가 모든 사용자에게 동일한가?
└─ 예 → 데이터가 자주 변하는가?
└─ 아니오 → SSG (블로그, 문서)
└─ 예 (시간 기반) → ISR (상품, 뉴스)
└─ 예 (실시간) → SSR (주식 가격)
사용자별 맞춤 데이터가 필요한가?
└─ 예 → SEO가 중요한가?
└─ 예 → SSR (개인화 페이지)
└─ 아니오 → CSR (관리자 대시보드)