Next.jsNext.js 기초 · 3기초

데이터 패칭 — 서버 컴포넌트와 캐싱 전략

Next.js데이터패칭fetch캐싱Suspense서버컴포넌트

서버 컴포넌트에서 데이터 패칭

// app/products/page.tsx
// async 함수로 직접 데이터 패칭 가능
async function ProductsPage() {
    // fetch는 서버에서 실행됨
    const products = await fetch("https://api.example.com/products")
        .then(r => r.json());

    return (
        <ul>
            {products.map((p: Product) => (
                <li key={p.id}>{p.name}</li>
            ))}
        </ul>
    );
}

fetch 캐싱 옵션

// 기본: 빌드 시 한 번 캐시 (SSG와 유사)
fetch("https://api.example.com/data");

// 캐시 없음: 매 요청마다 새로 가져옴 (SSR)
fetch("https://api.example.com/data", {
    cache: "no-store",
});

// 주기적 재검증 (ISR)
fetch("https://api.example.com/data", {
    next: { revalidate: 3600 },  // 1시간마다 재검증
});

// 태그 기반 재검증
fetch("https://api.example.com/products", {
    next: { tags: ["products"] },
});

태그 기반 revalidation

// app/actions/revalidate.ts
import { revalidateTag } from "next/cache";

// 특정 태그의 캐시를 즉시 무효화
export async function revalidateProducts() {
    revalidateTag("products");
}

// API Route나 Server Action에서 호출

병렬 데이터 패칭

async function DashboardPage() {
    // 순차적 (느림)
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);

    // 병렬 (빠름)
    const [user2, stats, notifications] = await Promise.all([
        fetchUser(),
        fetchStats(),
        fetchNotifications(),
    ]);

    return (
        <div>
            <UserCard user={user2} />
            <StatsGrid stats={stats} />
        </div>
    );
}

Suspense로 스트리밍 렌더링

import { Suspense } from "react";

// 느린 컴포넌트를 Suspense로 감싸면
// 나머지 페이지를 먼저 보여줌
async function SlowComponent() {
    await new Promise(r => setTimeout(r, 3000));  // 3초 대기
    const data = await fetchSlowData();
    return <div>{data}</div>;
}

export default function Page() {
    return (
        <div>
            <h1>즉시 표시</h1>
            <Suspense fallback={<div>데이터 로딩 중...</div>}>
                <SlowComponent />
            </Suspense>
            <p>이것도 즉시 표시</p>
        </div>
    );
}

클라이언트에서 데이터 패칭

인증이 필요하거나 실시간 업데이트가 필요한 경우:

"use client";
import { useState, useEffect } from "react";

function ClientDataComponent({ userId }: { userId: string }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetch(`/api/users/${userId}/private-data`)
            .then(r => r.json())
            .then(d => { setData(d); setLoading(false); });
    }, [userId]);

    if (loading) return <div>로딩 중...</div>;
    return <div>{JSON.stringify(data)}</div>;
}

generateMetadata: 동적 메타데이터

// app/products/[id]/page.tsx
import type { Metadata } from "next";

interface Props {
    params: { id: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
    const product = await fetch(`https://api.example.com/products/${params.id}`)
        .then(r => r.json());

    return {
        title: product.name,
        description: product.description,
        openGraph: {
            images: [product.imageUrl],
        },
    };
}

export default async function ProductPage({ params }: Props) {
    const product = await fetch(`https://api.example.com/products/${params.id}`)
        .then(r => r.json());

    return <div>{product.name}</div>;
}

정리

flowchart LR
    subgraph STRATEGY["캐싱 전략"]
        SSG["정적 생성\ncache: 기본값"]
        ISR["점진적 재생성\nrevalidate: N초"]
        SSR["서버 렌더링\ncache: no-store"]
    end
옵션동작사용 사례
기본 (캐시)빌드 시 생성블로그, 문서
revalidate: NN초마다 갱신제품 목록
no-store매 요청사용자 데이터
tags이벤트 기반 갱신CMS 연동

다음 편에서는 Server Actions와 API Routes — 서버 사이드 로직을 처리하는 방법을 배웁니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.