App Router에서는 표준 fetch API를 사용해 async Server Component 내부에서 직접 데이터를 페칭합니다. getServerSideProps도, useEffect도, 별도의 데이터 계층도 없습니다. Next.js는 fetch에 캐싱 제어를 확장합니다.
// app/posts/page.tsx — Server Component
export default async function Posts() {
const res = await fetch("https://api.example.com/posts"); // 서버에서 실행됨
const posts = await res.json();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
컴포넌트가 서버에서 실행되므로 데이터를 그냥 await하여 사용하면 됩니다. 클라이언트 측의 useEffect + useState + 로딩 상태 처리 작업보다 훨씬 간단합니다.
Next.js는 fetch를 확장하여 각 호출이 자신의 렌더링 동작을 선택하게 합니다:
fetch(url, { cache: "force-cache" }); // SSG — 캐싱됨 (정적의 기본값)
fetch(url, { cache: "no-store" }); // SSR — 요청마다 신선함
fetch(url, { next: { revalidate: 60 } }); // ISR — 60초마다 재검증
fetch(url, { next: { tags: ["posts"] } }); // 온디맨드 재검증을 위한 태그
이것이 App Router의 통합 모델입니다. 페이지 수준의 전략을 고르는 대신 데이터 페칭 수준에서 캐싱을 제어합니다.
// ❌ 순차 — 각각 이전 것을 기다림 (느린 워터폴)
const user = await getUser();
const posts = await getPosts();
// ✅ 병렬 — 둘 다 동시에 시작
const [user, posts] = await Promise.all([getUser(), getPosts()]);
독립적인 요청은 Promise.all로 함께 시작하여 서로를 막지 않게 하세요.
"use client";
// 클라이언트 측 데이터(예: 상호작용 후)는 SWR이나 React Query 같은 라이브러리 사용
const { data } = useSWR("/api/user", fetcher);
Server Component가 대부분의 요구를 충족합니다. 클라이언트 측, 상호작용형, 자주 재검증되는 데이터에는 SWR/React Query를 사용하세요.
Next.js는 단일 렌더 내에서 동일한 fetch 호출을 자동으로 중복 제거합니다. 그래서 레이아웃과 페이지에서 같은 데이터를 페칭해도 이중 요청이 발생하지 않습니다.
Server Component에서의 직접 async 데이터 페칭은 App Router의 가장 큰 단순화 중 하나입니다. 보일러플레이트 없음, 안전한 서버 측 접근, 자동 중복 제거, SSG/SSR/ISR 선택을 겸하는 fetch별 캐싱.
독립적인 fetch를 병렬화(워터폴 회피)하는 것을 아는 것이 핵심 성능 관행입니다.