개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Next.js Data Fetching — 서버에서 데이터를 가져오는 방법

2025년 5월 17일
Next.jsData Fetchingfetchcacherevalidate

Next.js App Router는 브라우저의 fetch API를 서버 환경에서 그대로 사용할 수 있도록 확장했다. 단순히 데이터를 불러오는 것을 넘어, 캐싱과 재검증 전략을 fetch 옵션 하나로 제어할 수 있다. 어디서 어떻게 데이터를 가져오느냐에 따라 성능과 사용자 경험이 크게 달라진다.

fetch 확장과 cache 옵션

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)요청마다 최신 데이터 필요

병렬 패칭 vs 순차 패칭

데이터 간 의존성이 없다면 병렬 패칭으로 총 대기 시간을 줄인다.

순차 패칭                 병렬 패칭
─────────────────        ─────────────────
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 UI와 Suspense

순차 패칭이나 느린 데이터 소스가 있을 때, 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를 조합하면, 정적 성능을 유지하면서도 사용자에게 즉각적인 피드백을 줄 수 있다. 캐시 전략은 데이터의 변경 빈도와 정확도 요구사항을 기준으로 선택한다.

← 이전 글Next.js Client Components — 브라우저에서 실행되는 컴포넌트
다음 글 →Next.js Server Actions — 서버 함수를 직접 호출하는 방법