[2026] Next.js App Router: SSR, SSG, and ISR | Rendering Strategy and Caching
이 글의 핵심
Choose SSR, SSG, and ISR in the Next.js App Router: fetch cache, revalidate, Route Segment Config, and practical criteria for content and personalization.
Introduction
In the Next.js App Router (13+), how each page is built on the server and how long it is cached drives performance, SEO, and cost. Stopping at Next.js App Router SSR vs SSG names alone makes it easy to hit unintended caching from a single fetch line.
This post maps static generation, server rendering, and incremental revalidation (ISR) on top of the default server components model, and shows how to control behavior consistently with revalidate and Route Segment Config. It assumes App Router + fetch cache semantics (2026).
In production, CDN, data cache, and server component caches stack—“docs say X but I see old data” is common. Below: concepts → code → advanced settings → selection criteria → cases → troubleshooting.
After reading this post
- Distinguish what SSR, SSG, and ISR mean in the App Router
- Design per-fetch caching with
fetchoptions andrevalidate - Split strategies for dynamic routes, personalization, and admin UIs
Table of contents
- Concepts
- Hands-on implementation
- Advanced: Route Segment Config
- Performance comparison
- Real-world cases
- Troubleshooting
- Wrap-up
Concepts
Terminology (vs Pages Router intuition)
| Term | Intuitive meaning | App Router reality |
|---|---|---|
| SSG | HTML at build (or regen) time | Paths where server component trees can be cached statically—e.g. fetch(..., { cache: 'force-cache' }) |
| SSR | HTML per request | Dynamic rendering—closer to cache: 'no-store' or dynamic = 'force-dynamic' |
| ISR | Refresh static output on a schedule or on demand | fetch revalidate seconds or revalidatePath / revalidateTag |
The App Router creates cache boundaries from fetch calls and segment settings, not a single “page mode” switch. |
React Server Components (RSC)
Server components run on the server by default; the client receives serialized output. “SSR or SSG” becomes when and how that result is cached.
Hands-on implementation
2-1. SSG-like: fixed data at build
아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// app/posts/page.tsx — cache at build time (force-cache)
export default async function PostsPage() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
const posts = await res.json();
return (
<ul>
{posts.map((p: { id: string; title: string }) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
2-2. SSR: fresh data every request
아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// app/dashboard/page.tsx
export default async function DashboardPage() {
const res = await fetch('https://api.example.com/me', {
cache: 'no-store',
});
const user = await res.json();
return <div>{user.name}</div>;
}
2-3. ISR: time-based revalidation
아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// app/blog/[slug]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // background revalidate every hour
});
if (!res.ok) notFound();
const post = await res.json();
return <article>{post.body}</article>;
}
2-4. Tag-based invalidation (operations)
아래 코드는 typescript를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// on fetch
await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// in a Server Action or Route Handler
import { revalidateTag } from 'next/cache';
export async function POST() {
revalidateTag('posts');
return Response.json({ ok: true });
}
Summary: one route can mix strategies if different components use different fetch settings. A per-data-source cache policy table reduces mistakes.
Advanced: Route Segment Config and static/dynamic boundaries
Force dynamic at segment level
// app/admin/layout.tsx
export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';
- dynamic:
'auto' | 'force-dynamic' | 'error' | 'force-static'— default tendency for the route - revalidate: segment-level default ISR interval (understand together with route-wide revalidate)
generateStaticParams for SSG scope
아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const slugs = await getAllSlugs(); // build-time list
return slugs.map((slug) => ({ slug }));
}
If a CMS exports tens of thousands of slugs, do not prerender all at build—top N static, rest on-demand ISR.
Performance comparison
| Scenario | Direction | Why |
|---|---|---|
| Marketing landing, docs, legal | SSG + long revalidate or fully static | Max CDN hit rate, stable TTFB |
| Product dashboard, cart | SSR (no-store) or client split | Per-user data; avoid cache poisoning |
| Blog, catalog | ISR + revalidateTag | Balance traffic vs freshness |
| Real-time stock/price | SSR + short TTL or Edge + external cache | Next cache alone may not be enough |
Key: prefer agreeing “how stale can this fetch be?” over labeling a whole page “SSG.” |
Real-world cases
- E-commerce listing: list
revalidate: 300+ tagproducts; on price changerevalidateTag('products'). - Post-login header:
cache: 'no-store'for user info; keep shared nav static in a separate slice to minimize personalization surface. - Docs site: mostly SSG; search via client or separate API—split rendering from search indexing.
Troubleshooting
“Build is fresh; production shows stale data”
Check fetch cache vs revalidate, and host/CDN data cache settings.
“revalidatePath did nothing”
Verify the same segment tree and cache keys; tag-based invalidation is often more reliable.
“Almost leaked personal data across users”
Never force-cache per-user APIs. Session/cookie-sensitive fetch should use no-store or a separate permission boundary.
Wrap-up
SSR vs SSG in the App Router is best understood as the cache story from fetch and segment config, not labels alone. Document per-layer cache standards (TTL, tags, invalidation triggers) so teams spend less time re-debating performance vs freshness. Pair with the Node.js performance post for async and server load.