서버 컴포넌트에서 데이터 패칭
// 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: N | N초마다 갱신 | 제품 목록 |
no-store | 매 요청 | 사용자 데이터 |
tags | 이벤트 기반 갱신 | CMS 연동 |
다음 편에서는 Server Actions와 API Routes — 서버 사이드 로직을 처리하는 방법을 배웁니다.