Next.js에서는 서버 컴포넌트와 클라이언트 컴포넌트 모두에서 데이터를 패칭할 수 있습니다. 서버 컴포넌트에서 데이터를 패칭하면 보안, 성능, SEO 측면에서 이점이 있습니다.
서버 컴포넌트에서 데이터 패칭하기
서버 컴포넌트(기본값)에서는 fetch
를 비롯한 비동기 데이터 패칭을 자유롭게 사용할 수 있습니다. 예시:
// app/page.tsx
export default async function Page() {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
return <div>{data.title}</div>
}
클라이언트 컴포넌트에서 데이터 패칭하기
클라이언트 컴포넌트(상단에 'use client' 선언)에서는 React의 훅(useEffect
, useState
) 또는 SWR, React Query 등 클라이언트 데이터 패칭 라이브러리를 사용할 수 있습니다.
// app/ui/client-component.tsx
'use client'
import { useEffect, useState } from 'react'
export default function ClientComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData)
}, [])
return <div>{data ? data.title : 'Loading...'}</div>
}
fetch 함수의 확장 기능
Next.js의 fetch
는 기본적으로 캐싱, 재검증, 동시성 제어 등 다양한 기능을 내장하고 있습니다. 예를 들어, cache: 'no-store'
옵션을 주면 항상 최신 데이터를 가져옵니다.
const res = await fetch('https://api.example.com/data', { cache: 'no-store' })
스트리밍(Streaming)과 Suspense
Next.js는 React의 Suspense와 streaming을 활용해 데이터 패칭 중에도 UI를 점진적으로 렌더링할 수 있습니다.
loading.js 파일로 전체 페이지 스트리밍
특정 라우트(예: app/blog/page.js
)에 loading.js
파일을 추가하면, 데이터 패칭 중에도 레이아웃과 로딩 UI가 먼저 보여지고, 데이터가 준비되면 자동으로 교체됩니다.
// app/blog/loading.tsx
export default function Loading() {
return <div>Loading...</div>
}
로 부분 스트리밍
<Suspense>
를 사용하면 페이지의 일부만 스트리밍할 수 있습니다. 예를 들어, 블로그 리스트만 별도로 로딩 UI와 함께 스트리밍할 수 있습니다.
// app/blog/page.tsx
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
의미 있는 로딩 상태 만들기
즉각적인 로딩 상태는 사용자가 네비게이션 후 바로 볼 수 있는 UI입니다. 스켈레톤, 스피너, 대표 이미지 등 의미 있는 로딩 UI를 설계하는 것이 UX에 좋습니다.
데이터 패칭 패턴 예시
순차적 데이터 패칭(Sequential)
중첩된 컴포넌트가 각자 데이터를 패칭하면, 요청이 순차적으로 일어나 전체 응답 시간이 길어질 수 있습니다. 하지만 한 fetch가 다른 fetch의 결과에 의존하는 경우에는 이 패턴이 필요할 수 있습니다.
// app/artist/[username]/page.tsx
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }: { artistID: string }) {
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
병렬 데이터 패칭(Parallel)
여러 데이터 요청을 동시에 시작하면 전체 응답 속도를 높일 수 있습니다. Promise.all
을 활용하면 여러 fetch를 병렬로 처리할 수 있습니다.
// app/artist/[username]/page.tsx
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params
const artistData = getArtist(username)
const albumsData = getAlbums(username)
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
참고:
Promise.all
에서 하나의 요청이라도 실패하면 전체가 실패합니다. 이럴 때는Promise.allSettled
를 사용할 수 있습니다.
데이터 프리로딩(Preloading)
데이터 프리로딩은 실제로 필요한 fetch보다 미리 데이터를 요청해두는 패턴입니다. 예를 들어, preload()
를 먼저 호출해두면, 실제 컴포넌트가 렌더링될 때 이미 데이터가 준비되어 있을 수 있습니다.
// app/item/[id]/page.tsx
import { getItem } from '@/lib/data'
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
preload(id)
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id: string) => {
void getItem(id)
}
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
React의 cache
함수와 server-only
패키지를 활용하면 서버에서만 실행되는 캐시 유틸리티를 만들 수 있습니다.
// utils/get-item.ts
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
API Reference
이 페이지에서 언급된 주요 API는 아래에서 더 자세히 확인할 수 있습니다.
출처: https://nextjs.org/docs/app/getting-started/fetching-data