핵심 요약
RSC가 안정화되면서 데이터 페칭 패턴이 4가지로 정리됐다. 각각 적합한 상황이 다른데, 잘못 섞으면 워터폴·하이드레이션 문제로 페이지가 멈춘다. Next.js 16 기준 실전 정리.
1. 패턴 A — 서버 컴포넌트에서 직접 await
가장 흔한 패턴. 페이지 로드 시 한 번 필요한 데이터.
// app/products/[id]/page.tsx — Server Component
async function ProductPage({ params }) {
const { id } = await params
const product = await db.product.findUnique({ where: { id } })
return <ProductDetail product={product} />
}
장점: 클라이언트 JS 0줄. SEO 친화. 단점: 모든 데이터 도착할 때까지 페이지 안 보임.
2. 패턴 B — Suspense + use() 스트리밍
일부는 빨리 보이고 일부는 늦게 와도 되는 경우.
// 서버에서 promise만 만들고 await 안 함
async function ProductPage({ params }) {
const { id } = await params
const productPromise = db.product.findUnique({ where: { id } })
const reviewsPromise = db.review.findMany({ where: { productId: id } })
return (
<>
<Suspense fallback={<Skeleton />}>
<Product promise={productPromise} />
</Suspense>
<Suspense fallback={<ReviewSkeleton />}>
<Reviews promise={reviewsPromise} />
</Suspense>
</>
)
}
'use client'
import { use } from 'react'
function Product({ promise }) {
const product = use(promise)
return <h1>{product.name}</h1>
}
핵심: promise를 await 없이 클라이언트로 넘기고 use()로 풀어준다. 핫 데이터는 먼저 그리고 느린 데이터는 streaming.
3. 패턴 C — Server Action 뮤테이션
폼 제출·버튼 클릭으로 데이터 변경.
// app/actions/posts.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
revalidateTag('posts')
}
// 사용
<form action={createPost}>
<input name="title" />
<button>저장</button>
</form>
장점: API 라우트 안 만들어도 됨. 단점: 진행률·낙관적 업데이트는 별도 처리.
4. 패턴 D — 클라이언트 페칭 (SWR/React Query)
실시간성 강한 위젯·정기 폴링·무한 스크롤.
'use client'
import useSWR from 'swr'
function LiveStock({ symbol }) {
const { data } = useSWR(`/api/stock/${symbol}`, fetcher, {
refreshInterval: 1000,
})
return <span>{data?.price}</span>
}
5. 결정 트리
| 상황 | 패턴 |
|---|---|
| SEO 중요·페이지 로드 시 1회 | A (서버 await) |
| 한 페이지에 빠른+느린 데이터 섞임 | B (Suspense + use) |
| 폼·버튼으로 변경 | C (Server Action) |
| 1초마다 갱신·실시간성 | D (클라이언트 페칭) |
| 무한 스크롤 | D + 초기 데이터는 A |
| 사용자별 다른 데이터, 캐시 의미 없음 | A 또는 D |
6. 워터폴 피하기
안티패턴: 의존 없는 두 쿼리를 순차로 await.
// ❌
const user = await getUser(id)
const posts = await getPosts(id) // user에 의존 안 함
// ✅
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
])
// ✅✅ — 둘 다 streaming
const userPromise = getUser(id)
const postsPromise = getPosts(id)
return <Suspense...>...</Suspense>
7. fetch + 16의 캐시 변화 주의
Next.js 16부터 fetch는 기본 동적이다(이전 글 참조). 캐시 원하면 명시.
const data = await fetch(url, {
cache: 'force-cache',
next: { revalidate: 60, tags: ['products'] },
})
8. 흔한 함정
- 서버 컴포넌트에 useState/useEffect: 불가. 'use client' 분리 필수.
- Server Action에서 redirect 후 finally: 동작 안 함. redirect는 throw로 흐름 끊김.
- use(promise)를 매 렌더링 새로 생성된 promise에: 무한 루프. 부모에서 만들어 내려야 함.
- client 컴포넌트가 server 컴포넌트를 children으로 받음: 가능. import는 불가, props로만.
9. 실측 — 같은 페이지 4패턴 비교
상품 상세 페이지(상품+리뷰+추천 3섹션)을 4가지로 구현해 비교.
| 패턴 | TTFB | FCP | LCP | JS |
|---|---|---|---|---|
| A (서버 await 전체) | 180ms | 800ms | 820ms | 0kB |
| B (스트리밍) | 40ms | 180ms | 820ms | 2kB |
| D (클라이언트 페칭) | 30ms | 110ms | 1,200ms | 34kB |
B 패턴이 균형 최적. D는 LCP가 늦지만 사용자 인터랙션 빠름.
참고
- react.dev/reference/react/use
- nextjs.org/docs/app — data fetching

댓글 0