Next.js SSR·SSG·ISR 렌더링 전략 정리
어떤 페이지에 SSR을 쓰고, 어떤 페이지를 SSG로 굳혀두고, 어떤 페이지에 ISR을 얹을지 매번 헷갈렸다. 그래서 셋을 한자리에 두고 정리해뒀다. 기준은 Next.js Pages Router의 getServerSideProps와 getStaticProps다.
SSR — 매 요청마다 서버를 거치게 했다
페이지에 접근할 때마다 서버에서 getServerSideProps가 실행되고, 그 안에서 API를 호출해 데이터를 받아왔다. 페이지 단위로 서버 렌더링이 매번 일어난다는 뜻이다.
문제는 접근이 잦은 페이지였다. 매번 서버를 거치니 로딩 속도에서 불리했다. React Query 같은 클라이언트 캐싱 라이브러리를 같이 써봐도, 페이지에 재진입하는 순간 SSR 단계에서 서버가 다시 호출되기 때문에 효과가 제한적이었다.
그래서 응답 헤더의 Cache-Control을 활용해 캐시를 거는 방법을 살펴봤다. 공유 캐시(프록시·CDN)에는 s-maxage로, 브라우저를 포함한 모든 캐시에는 max-age로 캐싱 시간을 지정한다. 여기서 짚어둘 점은 Next.js 자체가 응답 캐시를 가지는 게 아니라, 응답이 도달한 캐시 계층이 헤더에 따라 캐싱한다는 사실이다. Next.js 공식 문서도 getServerSideProps 안에서 다음과 같이 응답 캐시를 거는 예시를 든다.
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
)
return { props: {} }
}다만 getServerSideProps 안에서 특정 데이터만 Next.js 서버 메모리에 일등 시민처럼 캐싱하는 공식 API는 따로 없었다. 응답 단위의 Cache-Control 헤더가 사실상 유일한 캐싱 수단이었다. App Router의 fetch 캐싱이나 unstable_cache는 다른 모델이라 여기서는 다루지 않는다.
SSG — 빌드 시점에 HTML을 만들어뒀다
getStaticProps는 next build 시점에 한 번 실행되어 API를 호출한다. 그 결과로 HTML과 JSON을 함께 생성하고, 이후 요청에 대해서는 생성된 HTML(또는 클라이언트 라우팅 시 JSON)을 재사용한다.
CRUD가 빈번하지 않은 페이지에는 캐싱된 HTML을 그대로 돌려쓰니 성능이 좋았다. 빌드 산출물이 곧 응답이 되니까 서버에서 매번 무언가를 계산할 필요가 없었다.
대신 빌드 시점에 데이터가 고정된다는 점이 단점이었다. 이후 CRUD로 데이터가 바뀌어도 다음 빌드가 일어나기 전까지는 변경이 반영되지 않는다. 자주 바뀌는 데이터를 SSG로 깔아뒀다가 사용자에게 옛 데이터를 그대로 보여주는 경우가 생긴다.
ISR — SSG에 stale-while-revalidate를 얹었다
ISR(Incremental Static Regeneration)은 getStaticProps에 revalidate 옵션을 추가한 형태다. 동작은 세 단계로 나뉜다.
revalidate시간이 지난 뒤 들어온 첫 요청에는 일단 캐싱된(stale) HTML을 그대로 돌려준다.- 그와 동시에 백그라운드에서 새 HTML을 재생성한다.
- 재생성이 끝나면 이후 요청부터 새 HTML이 캐싱된 상태로 나간다.
즉시 invalidate가 아니라 stale-while-revalidate 모델이라는 점이 ISR의 핵심이었다. 시간을 흐름으로 그려보면 이렇게 된다.
코드로는 revalidate만 더하면 끝이었다.
export async function getStaticProps() {
return {
props: {},
revalidate: 60,
}
}전체 사이트를 다시 빌드하지 않고, 해당 페이지만 revalidate 주기에 맞춰 백그라운드에서 다시 생성한다. SSG의 빌드된 HTML을 재사용하는 성능 이점은 그대로 가져가면서, 빌드 이후 데이터 변경에도 어느 정도 반응할 수 있었다.
함정도 있었다. revalidate 시간을 너무 짧게 잡으면 요청이 들어올 때마다 해당 페이지의 백그라운드 재생성이 잦아져 데이터 소스(API·DB)와 Next.js 서버 양쪽에 부하가 커진다. "모든 페이지를 재요청"이 아니라 "요청 들어온 페이지만 잦게 재생성"이라는 점도 헷갈리지 않게 적어뒀다.
셋을 같은 기준으로 다시 보면, SSR은 매 요청을 서버로 보내는 대신 응답 헤더 캐시에 기댄다. SSG는 빌드 시점에 한 번 만들고 끝낸다. ISR은 SSG의 캐시를 유지하면서 백그라운드 재생성으로 데이터 변경을 따라잡는다. 페이지의 데이터 갱신 주기와 트래픽 패턴을 기준으로 셋 중 하나를 골라 쓰면 됐다.