렌더링 전략은 "HTML을 어디서, 언제 만드느냐"를 정하는 일이고, 이 선택 하나로 첫 바이트 시간(TTFB), 첫 화면(LCP), 서버 비용, 데이터 신선도가 함께 정해진다. 이 글은 SSG·ISR·SSR·CSR이 각각 HTML을 만드는 시점과 성능 특성을 비교하고, 이 블로그가 SSR에 CDN 캐시를 얹어 ISR처럼 쓰는 실제 설정과 페이지별로 전략을 고르는 기준을 정리한다.

한눈에 보기
| 전략 | HTML을 만드는 곳·시점 | 데이터 신선도 | TTFB | 서버 부하 | 맞는 페이지 |
|---|---|---|---|---|---|
| SSG | 빌드 서버, 배포 때 한 번 | 다음 배포까지 고정 | CDN 수준 | 거의 없음 | 문서, 소개, 랜딩 |
| ISR | 서버, 첫 요청 또는 만료 뒤 백그라운드 | 재검증 주기만큼 늦음 | CDN 수준(캐시 적중 시) | 주기당 1회 | 글 목록, 상품 상세 |
| SSR | 서버, 매 요청 | 요청 시점 | 데이터 조회 시간만큼 늘어남 | 요청 수에 비례 | 개인화, 검색 결과 |
| CSR | 브라우저, JS 실행 뒤 | 요청 시점 | 빈 셸이라 빠름 | API 호출만 | 로그인 뒤 관리 화면 |
핵심은 두 축이다. 하나는 장소(서버인가 브라우저인가), 다른 하나는 시점(빌드 때인가, 요청 때인가, 요청과 상관없이 백그라운드에서인가)이다. 표의 나머지 칸은 이 두 축에서 따라 나온다.

SSG와 ISR: 미리 만들어 두고 CDN에서 응답
SSG는 빌드 때 데이터를 읽어 HTML 파일을 만들고 CDN에 올린다. 요청은 CDN이 파일을 그대로 돌려주므로 TTFB가 가장 짧고 서버가 죽어도 페이지가 뜬다. 대신 글 하나를 고쳐도 다시 빌드해야 하고, 페이지가 수만 개면 빌드 시간이 배포 주기를 잡아먹는다.
ISR은 이 단점을 고친 방식이다. 페이지를 처음 요청받을 때(또는 빌드 때) 만들어 캐시에 두고, 정해진 시간이 지나면 다음 요청에는 이전 HTML을 먼저 주고 백그라운드에서 새로 만든다. HTTP의 stale-while-revalidate와 같은 생각이다.
- 장점: 대부분의 요청이 캐시 적중이라 SSG 수준으로 빠르다.
- 대가: 만료 직후 첫 방문자는 낡은 내용을 본다. 재검증 주기가 60초면 최대 60초 늦은 화면이 나갈 수 있다.
- 즉시 반영: 글을 저장하는 순간 해당 경로나 태그의 캐시를 지우는 on-demand 재검증을 붙이면 "주기"를 기다리지 않아도 된다.
// Next.js App Router: 세그먼트 단위 재검증 주기(초)
export const revalidate = 3600;
// 글 저장 서버 액션에서 즉시 무효화
import { revalidatePath, revalidateTag } from 'next/cache';
revalidatePath('/blog');
revalidateTag('posts');
Next.js는 버전마다 fetch 캐시 기본값이 바뀌어 왔다. 같은 코드라도 버전에 따라 정적·동적으로 갈리므로, 쓰는 버전의 캐싱 문서와 빌드 로그의 라우트 표시(정적/동적)를 확인한다.
SSR: 요청마다 서버에서 만들기
SSR은 요청이 올 때마다 서버가 데이터를 조회하고 HTML을 만든다. 로그인 사용자마다 다른 화면, 검색어마다 다른 결과처럼 미리 만들 수 없는 페이지에 맞다.
- TTFB = 네트워크 + 데이터 조회 + 렌더링이다. DB가 200ms 걸리면 TTFB도 최소 200ms 늘어난다.
- 서버 위치가 성능이다. 함수가 서울에 있어도 DB가 미국에 있으면 조회마다 왕복 지연이 붙는다. 함수를 DB 가까운 리전에 둔다.
- 꼬리 지연이 그대로 보인다. 캐시가 없으니 DB의 느린 1%가 사용자의 느린 1%가 된다. 평균보다 P95·P99를 봐야 하는 이유다.
- 스트리밍으로 느린 부분만 나중에 보내면 첫 화면은 일찍 그릴 수 있다. 다만 프레임워크와 호스팅이 스트리밍을 지원하는지 먼저 확인한다.
CSR: 브라우저에서 만들기
CSR은 서버가 거의 빈 HTML과 JS 번들만 주고, 브라우저가 JS를 실행해 API를 호출한 뒤 화면을 그린다. 첫 응답은 빠르지만 의미 있는 내용은 늦게 나온다.
- LCP가 "JS 다운로드 → 실행 → API 응답 → 렌더"를 모두 기다린다. 느린 휴대폰에서 이 차이가 가장 크다.
- 검색 엔진이 JS를 실행해 색인하더라도 시간이 걸리고, 공유 미리보기(OG 태그)는 JS를 실행하지 않는 크롤러가 많다. 공개 페이지에 CSR만 쓰면 노출에서 손해를 본다.
- 로그인 뒤의 관리 화면, 대시보드처럼 검색 노출이 필요 없고 상호작용이 많은 화면에는 여전히 가장 단순한 선택이다. 이때 서버 데이터는 TanStack Query 같은 서버 상태 도구로 캐시하면 화면 전환이 빨라진다.
SSR·SSG 페이지도 버튼이 동작하려면 JS가 내려와 하이드레이션을 거친다. 이 JS가 크면 화면은 보이는데 클릭이 늦는 INP 문제가 생긴다. 이 부분은 INP·LCP·CLS 실전 분석에서 따로 다룬다.
같은 페이지로 본 성능 비교
글 상세 페이지 하나를 네 방식으로 만들었다고 가정한 예시 수치다. 조건은 DB 조회 200ms, 서버 렌더링 100ms, CDN까지 왕복 50ms, JS 번들 150KB, 클라이언트 API 응답 400ms다. 실측이 아니라 각 단계 시간을 더해 만든 값이며, 전략 사이의 차이가 어디서 오는지 보이려는 것이다.
| 전략 | TTFB | FCP | LCP | 시간이 드는 곳 |
|---|---|---|---|---|
| SSG (CDN 적중) | 50ms | 0.6s | 0.9s | 네트워크와 이미지 로딩만 |
| ISR (CDN 적중) | 50ms | 0.6s | 0.9s | 만료 직후에도 이전 HTML로 응답 |
| SSR (캐시 없음) | 350ms | 0.9s | 1.2s | DB 200ms + 렌더 100ms가 TTFB에 더해짐 |
| CSR | 50ms | 0.7s (빈 틀) | 2.0s | JS 실행 뒤 API 400ms를 기다려야 본문이 나옴 |
세 지표의 순서(TTFB ≤ FCP ≤ LCP)는 어느 전략에서도 바뀌지 않는다. 달라지는 것은 어느 구간이 늘어나느냐다. SSR은 앞쪽(TTFB)이, CSR은 뒤쪽(FCP와 LCP 사이)이 길어진다. ISR은 캐시가 비어 있는 첫 생성 요청에서 SSR과 같은 값을 낸다.
이 블로그의 선택: SSR에 CDN 캐시를 얹어 ISR처럼 쓰기
이 블로그는 SvelteKit을 Vercel에서 SSR로 돌린다. 글·댓글·방명록이 DB에 있어 SSG로는 저장할 때마다 재빌드가 필요하고, 그렇다고 매 요청 DB를 읽으면 TTFB가 늘어난다. 그래서 src/hooks.server.ts에서 공개 페이지 응답에 CDN 캐시 헤더를 붙인다.
const PAGE_CACHE_TAG = 'pages';
const PAGE_CDN_CACHE = 'max-age=3600, stale-while-revalidate=86400';
// GET 200 응답 중 관리자·비공개 경로·쿠키·자체 cache-control이 없는 것만
response.headers.set('Vercel-CDN-Cache-Control', PAGE_CDN_CACHE);
response.headers.set('Vercel-Cache-Tag', PAGE_CACHE_TAG);
// GET이 아닌 요청(글·댓글 저장)이 성공하면 태그째 캐시 삭제
await dangerouslyDeleteByTag(PAGE_CACHE_TAG);
동작은 ISR과 같다. CDN이 1시간 동안 HTML을 들고 있고, 만료 뒤 하루 동안은 이전 응답을 주면서 뒤에서 새로 받는다. 글을 저장하면 pages 태그를 지워 다음 요청이 새 내용을 받는다. 조회수 증가처럼 화면을 바꾸지 않는 POST는 캐시를 지우지 않게 예외로 뺐다. 함수 리전은 DB(Turso 도쿄)와 같은 hnd1에 두어 캐시가 빈 요청의 TTFB도 줄였다.
캐시 하나가 한꺼번에 만료될 때 원본에 요청이 몰리는 문제는 서버 캐시에서도 똑같이 생긴다. 대응 방식은 Redis Look-aside Cache와 Cache Stampede에서 정리했다.
페이지마다 전략을 고르는 기준
렌더링 전략은 사이트 전체에 하나를 고르는 것이 아니라 라우트마다 고른다. 질문 세 개로 대부분 정해진다.

SvelteKit은 페이지 옵션으로 라우트마다 정한다. +page.ts나 +layout.ts에서 내보낸다.
// 빌드 때 HTML 생성 (SSG)
export const prerender = true;
// 서버 렌더링 끄고 브라우저에서만 그림 (CSR)
export const ssr = false;
// JS를 아예 보내지 않음 (상호작용 없는 정적 페이지)
export const csr = false;
// adapter-vercel 기준 ISR: 60초 뒤 재생성
export const config = { isr: { expiration: 60 } };
화면을 팀별로 나눠 배포하는 문제는 렌더링 전략과 다른 축이다. 그 판단은 Micro Frontend와 Module Federation 도입 판단 기준을 참고한다.
| 페이지 유형 | 고를 전략 | 잘못 고르면 생기는 일 |
|---|---|---|
| 회사 소개, 문서 | SSG | SSR로 두면 바뀌지 않는 페이지에 매번 서버 비용을 낸다 |
| 글 목록, 상품 상세 | ISR 또는 SSR + CDN 캐시 | SSG면 수정마다 재빌드, SSR만이면 트래픽에 비례해 DB 부하 |
| 마이페이지, 장바구니 | SSR (공통 틀만 캐시) | 개인 화면을 CDN에 캐시하면 다른 사람 정보가 노출된다 |
| 관리자 대시보드 | CSR | SSR로 만들면 얻는 것 없이 서버 코드만 복잡해진다 |
자주 하는 실수
개인화 응답을 CDN에 캐시한다. 쿠키에 따라 내용이 달라지는 페이지가 공용 캐시에 들어가면 첫 사용자의 화면이 모두에게 나간다. 캐시 조건에서 쿠키·관리자 요청을 반드시 뺀다.
ISR 재검증 주기를 "실시간"처럼 짧게 잡는다. 1초 주기는 사실상 SSR인데 캐시 관리 비용만 더해진다. 즉시 반영이 필요하면 주기 대신 on-demand 재검증을 쓴다.
CSR 페이지의 TTFB만 보고 빠르다고 판단한다. 빈 HTML은 빨리 오지만 사용자가 보는 본문은 API 뒤에 나온다. LCP로 판단한다.
SSR 함수와 DB를 다른 대륙에 둔다. 조회 한 번마다 왕복 지연이 붙어 TTFB가 렌더링 전략보다 배치 때문에 느려진다.
쓰기 뒤 캐시 무효화를 빠뜨린다. 글을 고쳤는데 한 시간 동안 이전 글이 보인다. 쓰기 경로마다 무효화가 붙어 있는지 확인한다.
점검표
- 라우트마다 렌더링 전략(SSG·ISR·SSR·CSR)이 표로 정리되어 있다.
- 공개 페이지는 JS 없이도 본문과 OG 태그가 HTML에 들어 있다.
- 쿠키나 로그인에 따라 달라지는 응답은 공용 캐시에서 빠진다.
- 쓰기 요청이 성공하면 관련 경로나 태그의 캐시가 무효화된다.
- ISR 재검증 주기와 "최대 몇 초 늦은 화면이 허용되는가"가 같은 숫자로 정해져 있다.
- SSR 함수 리전이 주 DB와 같은 리전 또는 가까운 리전이다.
- 캐시가 빈 요청의 TTFB를 P95로 따로 측정한다.
- CSR 페이지는 LCP를 기준으로 성능을 판단한다.
- 프레임워크 버전의 캐시 기본값을 문서와 빌드 로그로 확인했다.
렌더링 전략은 페이지마다 "HTML을 누가 언제 만들면 가장 싸고 충분히 신선한가"에 답하는 일이고, 대부분의 공개 페이지에서는 미리 만들어 CDN에 두고 쓰기 순간에만 지우는 쪽이 답이다.
댓글 0개
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.