사전 렌더링 방식 - SSR, SSG, ISR
사전 렌더링이란
Next.js는 모든 페이지를 기본적으로 사전 렌더링(Pre-rendering)한다. 클라이언트 측 JavaScript가 모든 작업을 처리하는 전통적인 React SPA와 달리, Next.js는 서버에서 미리 HTML을 생성한다.
사전 렌더링의 핵심 장점은 초기 로딩 성능과 SEO 개선이다. 서버에서 완성된 HTML을 보내기 때문에 브라우저는 즉시 컨텐츠를 표시할 수 있고, 검색 엔진 크롤러도 페이지 내용을 정확히 파악할 수 있다.
Next.js는 세 가지 사전 렌더링 방식을 제공한다. 각 방식은 페이지를 생성하는 시점과 주기가 다르며, 데이터의 신선도와 성능 사이에서 서로 다른 균형점을 찾는다.
SSR (Server-Side Rendering)
왜 필요한가
서버 사이드 렌더링은 요청이 들어올 때마다 서버에서 HTML을 새로 생성한다. 사용자마다 다른 컨텐츠를 보여주거나, 실시간으로 변하는 데이터를 표시해야 할 때 필요하다.
예를 들어 대시보드 페이지는 로그인한 사용자의 최신 활동 내역을 보여줘야 한다. 이런 경우 빌드 타임에 미리 만들어둔 정적 페이지로는 해결할 수 없다.
동작 방식
SSR은 getServerSideProps 함수로 구현한다. 이 함수는 매 요청마다 서버에서 실행되며, 데이터를 가져온 뒤 페이지 컴포넌트에 props로 전달한다.
export default function Dashboard({ userData }) {
return (
<div>
<h1>{userData.name}의 대시보드</h1>
<p>마지막 로그인: {userData.lastLogin}</p>
</div>
)
}
export async function getServerSideProps(context) {
const { req, res } = context
// 쿠키에서 사용자 정보 가져오기
const userId = req.cookies.userId
// API에서 최신 데이터 조회
const response = await fetch(`https://api.example.com/users/${userId}`)
const userData = await response.json()
return {
props: {
userData
}
}
}
위 코드에서 사용자가 /dashboard 페이지를 요청하면 Next.js는 먼저 getServerSideProps를 실행한다. 이 함수는 쿠키에서 사용자 ID를 읽고, API를 호출해서 최신 데이터를 가져온다. 그 다음 이 데이터를 props로 전달받은 컴포넌트가 렌더링되어 HTML로 변환되고, 최종적으로 브라우저에 전송된다.
타입 추론
TypeScript를 사용할 때는 InferGetServerSidePropsType으로 props 타입을 자동 추론할 수 있다.
import type { InferGetServerSidePropsType } from 'next'
export default function Dashboard({
userData
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
// userData의 타입이 자동으로 추론됨
}
실행 환경
getServerSideProps는 오직 서버에서만 실행된다. 이 함수 안에서 console.log를 찍으면 브라우저 콘솔이 아닌 Next.js 서버 터미널에 출력된다. 따라서 데이터베이스 직접 조회나 서버 파일 시스템 접근 같은 서버 전용 작업을 안전하게 수행할 수 있다.
장단점
SSR의 장점은 항상 최신 데이터를 보여줄 수 있다는 점이다. 매 요청마다 새로 렌더링하기 때문에 실시간성이 중요한 페이지에 적합하다.
하지만 단점도 명확하다. 데이터를 가져오는 API가 느리면 사용자는 빈 화면을 오래 봐야 한다. 서버는 매번 렌더링 작업을 수행해야 하므로 트래픽이 많을수록 서버 부하가 증가한다. CDN 캐싱도 기본적으로는 불가능하다.
SSG (Static Site Generation)
왜 필요한가
정적 사이트 생성은 빌드 타임에 페이지를 미리 만들어둔다. SSR의 성능 문제를 해결하기 위해 등장했다. 내용이 자주 바뀌지 않는 페이지라면 매번 서버에서 렌더링할 필요가 없다.
블로그 글, 문서 페이지, 상품 목록처럼 모든 사용자에게 동일한 컨텐츠를 보여주는 경우 SSG가 가장 효율적이다.
동작 방식
SSG는 getStaticProps 함수로 구현한다. 이 함수는 빌드할 때 한 번만 실행되며, 가져온 데이터로 HTML 파일을 생성한다.
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
export async function getStaticProps() {
const response = await fetch('<https://api.example.com/posts/1>')
const post = await response.json()
return {
props: {
post
}
}
}
위 코드는 빌드 시점에 실행되어 HTML 파일을 생성한다. 사용자가 페이지를 요청하면 서버는 이미 만들어진 HTML을 즉시 반환한다. API 호출이나 렌더링 과정이 필요 없으므로 응답 속도가 빠르다.
동적 라우팅
동적 라우팅(예: /posts/[id])을 사용하는 경우 getStaticPaths로 어떤 경로들을 미리 생성할지 지정해야 한다.
// pages/posts/[id].tsx
export async function getStaticPaths() {
const response = await fetch('<https://api.example.com/posts>')
const posts = await response.json()
const paths = posts.map(post => ({
params: { id: post.id.toString() }
}))
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const response = await fetch(`https://api.example.com/posts/${params.id}`)
const post = await response.json()
return {
props: {
post
}
}
}
getStaticPaths는 빌드 시점에 어떤 ID들로 페이지를 만들지 결정한다. 위 예시에서는 API에서 모든 포스트 목록을 가져와서, 각 포스트마다 정적 페이지를 생성한다. 예를 들어 포스트가 10개라면 /posts/1, /posts/2, ... /posts/10 경로의 HTML이 모두 빌드 시점에 만들어진다.
fallback 옵션
fallback 옵션은 getStaticPaths에서 지정하지 않은 경로로 요청이 들어왔을 때 어떻게 처리할지 결정한다.
return {
paths,
fallback: false // 404 반환
// fallback: true // 빌드 후 생성
// fallback: 'blocking' // SSR처럼 동작
}
fallback: false는 지정한 경로 외에는 모두 404를 반환한다. fallback: true는 존재하지 않는 경로를 요청하면 즉시 빈 페이지를 보여주고 백그라운드에서 페이지를 생성한 뒤 교체한다. fallback: 'blocking'은 SSR처럼 동작해서 페이지 생성이 완료될 때까지 기다린 후 완성된 HTML을 반환한다.
대규모 사이트에서 모든 페이지를 빌드 시점에 생성하면 시간이 오래 걸린다. 이럴 때는 자주 접근하는 페이지만 미리 생성하고, 나머지는 fallback: 'blocking'으로 처리하는 것이 효율적이다.
제약사항
SSG는 요청 시점의 정보에 접근할 수 없다. 예를 들어 검색 페이지에서 쿼리 파라미터로 검색어를 받는다면, 빌드 시점에는 사용자가 어떤 검색어를 입력할지 알 수 없으므로 SSG를 사용할 수 없다.
// pages/search.tsx
export async function getStaticProps(context) {
// context.query는 빌드 시점에 undefined
const searchTerm = context.query.q // 이 값을 알 수 없음
return {
props: {}
}
}
이런 경우에는 클라이언트에서 직접 데이터를 가져오거나 SSR을 사용해야 한다.
장단점
SSG의 장점은 속도다. 이미 만들어진 HTML을 반환하기만 하면 되므로 응답이 즉각적이다. CDN에 캐싱할 수 있어서 전 세계 어디서든 빠른 접근이 가능하다. 서버 부하도 거의 없다.
단점은 데이터 신선도다. 빌드 이후로는 페이지가 업데이트되지 않기 때문에 컨텐츠가 변경되어도 재배포하기 전까지는 이전 내용이 표시된다.
ISR (Incremental Static Regeneration)
왜 필요한가
증분 정적 재생성은 SSG의 성능 장점을 유지하면서 데이터 신선도 문제를 해결한다. 모든 요청마다 렌더링하지는 않지만, 일정 주기로 페이지를 재생성해서 최신 상태를 유지한다.
블로그 조회수처럼 실시간성이 중요하지는 않지만 완전히 정적이지도 않은 데이터를 다룰 때 유용하다.
시간 기반 ISR
getStaticProps에서 revalidate 옵션을 추가하면 ISR이 활성화된다.
export async function getStaticProps() {
const response = await fetch('<https://api.example.com/posts>')
const posts = await response.json()
return {
props: {
posts
},
revalidate: 60 // 60초
}
}
위 코드는 60초마다 페이지를 재검증한다. 동작 방식은 다음과 같다.
첫 번째 요청이 들어오면 캐시된 페이지를 즉시 반환한다. 만약 마지막 생성 시점으로부터 60초가 지났다면, Next.js는 백그라운드에서 페이지를 재생성한다. 재생성이 완료되면 새로운 버전으로 교체되고, 그 다음 요청부터는 업데이트된 페이지가 제공된다.
중요한 점은 재생성 중에도 이전 버전의 페이지가 계속 제공된다는 것이다. 사용자는 절대 로딩 화면을 보지 않는다. 이를 Stale-While-Revalidate 전략이라고 한다.
On-Demand ISR
시간 기반 ISR은 주기적으로 재생성하지만, 때로는 특정 시점에 즉시 업데이트해야 할 때가 있다. 예를 들어 관리자가 블로그 글을 수정했다면, 다음 revalidate 주기를 기다리지 않고 바로 페이지를 갱신하고 싶다.
On-Demand ISR은 API 라우트에서 res.revalidate() 메서드로 특정 경로를 즉시 재생성할 수 있다.
// pages/api/revalidate.ts
export default async function handler(req, res) {
// 비밀 토큰으로 인증
if (req.query.secret !== process.env.REVALIDATE_TOKEN) {
return res.status(401).json({ message: 'Invalid token' })
}
try {
// 특정 경로 재검증
await res.revalidate('/posts/1')
return res.json({ revalidated: true })
} catch (err) {
return res.status(500).send('Error revalidating')
}
}
위 코드는 /api/revalidate?secret=TOKEN 엔드포인트를 호출하면 /posts/1 페이지를 즉시 재생성한다. CMS 웹훅과 연결하면 컨텐츠 수정 시 자동으로 페이지를 갱신할 수 있다.
// CMS에서 글 수정 후 웹훅으로 호출
await fetch(`https://yoursite.com/api/revalidate?secret=${token}&path=/posts/${postId}`)
주의사항
ISR은 Node.js 런타임에서만 동작한다. next export로 생성한 정적 사이트에서는 사용할 수 없다.
On-Demand ISR로 재검증을 트리거할 때 미들웨어는 실행되지 않는다. 인증이나 로깅이 필요하다면 API 라우트 내부에서 직접 처리해야 한다.
revalidate 값이 너무 짧으면 서버 부하가 증가하고, 너무 길면 데이터가 오래된 채로 유지된다. 컨텐츠 업데이트 빈도와 트래픽 패턴을 고려해서 적절한 값을 설정해야 한다.
페이지 별 특성에 맞는 렌더링 방식 선택
각 페이지의 특성에 맞는 렌더링 방식을 선택해야 한다.
SSR을 사용해야 하는 경우는 사용자별로 다른 컨텐츠를 보여주는 대시보드나 마이페이지, 실시간 데이터를 표시하는 주식 시세나 스포츠 스코어, 요청 헤더나 쿠키를 읽어야 하는 인증 페이지에 적합하다.
SSG를 사용해야 하는 경우는 마케팅 랜딩 페이지, 블로그 글, 제품 소개 페이지, 도움말 문서처럼 모든 사용자에게 동일한 컨텐츠를 보여주고 데이터가 거의 바뀌지 않는 페이지다.
ISR을 사용해야 하는 경우는 블로그 조회수나 좋아요 수처럼 자주 변하지만 실시간성이 중요하지 않은 데이터, 뉴스 기사처럼 주기적으로 업데이트되는 컨텐츠, 전자상거래 상품 목록처럼 재고나 가격이 변경되는 페이지다.
하나의 애플리케이션에서 여러 방식을 혼합해서 사용하는 것이 일반적이다. Next.js는 페이지별로 다른 렌더링 방식을 적용할 수 있으므로, 각 페이지의 요구사항에 맞는 최적의 전략을 선택하면 된다.