Next.js App Router는 브라우저의 fetch API를 서버 환경에서 그대로 사용할 수 있도록 확장했다. 단순히 데이터를 불러오는 것을 넘어, 캐싱과 재검증 전략을 fetch 옵션 하나로 제어할 수 있다. 어디서 어떻게 데이터를 가져오느냐에 따라 성능과 사용자 경험이 크게 달라진다.
Next.js는 fetch에 cache 옵션과 next 네임스페이스를 추가했다.
// 정적 캐싱 (기본값, force-cache와 동일)
const res = await fetch('https://api.example.com/posts');
// 캐시 사용 안 함 — 매 요청마다 새로 fetch
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});
// 주기적 재검증 — 60초마다 캐시 갱신
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});
force-cache: 빌드 시 또는 첫 요청 시 데이터를 캐싱하고 이후 요청은 캐시를 반환한다. no-store: 캐시를 전혀 사용하지 않는 동적 렌더링 모드다.
| 옵션 | 렌더링 방식 | 사용 사례 |
|---|---|---|
cache: 'force-cache' | 정적 (SSG) | 거의 변하지 않는 콘텐츠 |
next: { revalidate: N } | ISR | 주기적 갱신이 필요한 데이터 |
cache: 'no-store' | 동적 (SSR) | 요청마다 최신 데이터 필요 |
데이터 간 의존성이 없다면 병렬 패칭으로 총 대기 시간을 줄인다.
순차 패칭 병렬 패칭
───────────────── ─────────────────
fetch A ──────┐ fetch A ──┐
▼ ├──▶ Promise.all ──▶ 렌더
fetch B ──┐ fetch B ──┘
▼
render
총 시간: A + B 총 시간: max(A, B)
// 병렬 패칭
async function Page() {
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
return <div>...</div>;
}
반면 B가 A의 결과에 의존한다면 순차 패칭이 불가피하다.
async function Page({ userId }: { userId: string }) {
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
// user.teamId가 있어야 다음 요청 가능
const team = await fetch(`/api/teams/${user.teamId}`).then(r => r.json());
return <div>...</div>;
}
순차 패칭에서 각 단계가 길어지면 워터폴(Waterfall) 문제가 발생한다. 가능한 한 의존성을 끊어 병렬 처리로 전환하는 것이 좋다.
순차 패칭이나 느린 데이터 소스가 있을 때, loading.tsx와 <Suspense>로 부분적인 UI를 먼저 보여줄 수 있다.
브라우저
│
├─▶ Shell (레이아웃, 헤더) ──▶ 즉시 렌더
│
└─▶ <Suspense fallback={<Spinner />}>
│
└─▶ 데이터 로드 완료 후 실제 컴포넌트 교체
// app/posts/page.tsx
import { Suspense } from 'react';
import PostList from './PostList';
export default function Page() {
return (
<main>
<h1>Posts</h1>
<Suspense fallback={<p>불러오는 중...</p>}>
<PostList />
</Suspense>
</main>
);
}
// app/posts/PostList.tsx — async Server Component
async function PostList() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
}).then(r => r.json());
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
async Server Component: App Router에서 서버 컴포넌트는 async/await를 직접 사용할 수 있다. useEffect + useState 없이 서버에서 데이터를 가져와 렌더링한다.
next.revalidate와 Suspense를 조합하면, 정적 성능을 유지하면서도 사용자에게 즉각적인 피드백을 줄 수 있다. 캐시 전략은 데이터의 변경 빈도와 정확도 요구사항을 기준으로 선택한다.