서버에서 데이터 가져오기
Next.js에서는 가능한 한 데이터를 서버에서 패칭하는 것을 권장한다. 서버에서 데이터를 패칭하면 API 키, 인증 정보가 클라이언트에 노출되지 않으며, 클라이언트에서 요청을 보내는 시간을 절약할 수 있다.
// app/page.tsx (Server Component)
export default async function Page() {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
데이터 캐싱 및 중복 요청 방지
fetch 요청은 자동으로 캐싱(memoization)된다. 즉, 같은 데이터를 여러 컴포넌트에서 요청하더라도 한 번만 요청하고, 재사용한다. 이를 활용하면 중복된 API 요청을 방지할 수 있다.
// app/components/User.tsx
export default async function User() {
const res = await fetch('https://api.example.com/user')
const user = await res.json()
return <p>{user.name}</p>
}
// app/page.tsx
import User from './components/User'
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<User />
<User />
</>
)
}
Streaming and Suspense
Streaming과 Suspense를 사용하면, 데이터를 기다리지 않고 바로 UI를 표시한 후, 데이터를 가져오는 부분만 따로 로딩할 수 있다. 이를 활용하면 페이지가 블로킹되지 않고, 사용자가 빠르게 콘텐츠를 볼 수 있다.
// app/artist/[username]/page.tsx
import { Suspense } from 'react'
async function Playlists({ artistID }: { artistID: string }) {
const res = await fetch(`https://api.example.com/artist/${artistID}/playlists`)
const playlists = await res.json()
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({ params: { username } }: { params: { username: string } }) {
const res = await fetch(`https://api.example.com/artist/${username}`)
const artist = await res.json()
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading Playlists...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
순차 데이터 패칭
하나의 데이터 요청이 끝나야 다음 요청이 진행된다. 불필요한 waterfall이 발생하여 속도가 느려질 수 있다. 한 데이터 패치가 다른 패치의 결과에 따라 달라지거나 다음 패치 전에 조건이 충족되어 리소스를 절약하려는 경우 이 패턴을 원하는 경우가 있을 수 있다.
이런 경우 loading.ts 또는 Suspense를 사용하여 React가 결과를 스트리밍하는 동안 즉시 로딩 상태를 표시할 수 있다.
async function Playlists({ artistID }: { artistID: string }) {
// Wait for the playlists
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// Wait for the artist
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
병렬 데이터 패칭
Promise.all()을 사용하면 모든 요청을 동시에 실행하여 성능을 최적화할 수 있다. 두 개의 API 요청을 동시에 실행하여 속도를 최적화한다.
export default async function Page({ params: { userId } }: { params: { userId: string } }) {
const userPromise = fetch(`https://api.example.com/user/${userId}`).then((res) => res.json())
const postsPromise = fetch(`https://api.example.com/posts?userId=${userId}`).then((res) => res.json())
const [user, posts] = await Promise.all([userPromise, postsPromise]) // 병렬 요청
return (
<>
<h1>{user.name}</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</>
)
}
데이터 Preloading 패턴
페이지가 렌더링되기 전에 미리 데이터를 가져와 최적화할 수 있다. 데이터가 필요할 때 이미 캐싱되어 있기 때문에 빠르게 제공 가능하다. 데이터가 변경되지 않고 빌드 타임에 데이터를 미리 패칭하는 SSG, ISR은 필요하지 않다.
// utils/getItem.ts
import { cache } from 'react'
import 'server-only'
// Preloading 함수
export const preload = (id: string) => {
void getItem(id) // 미리 데이터를 가져와서 캐시에 저장
}
// SSR을 적용한 데이터 요청 함수
export const getItem = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/item/${id}`, { cache: 'no-store' })
return res.json()
})
// app/item/[id]/page.tsx (SSR 적용)
import { preload, getItem } from '@/utils/getItem'
import ItemDetails from '@/components/ItemDetails'
import ItemReviews from '@/components/ItemReviews'
export default async function Page({ params: { id } }: { params: { id: string } }) {
preload(id) // 미리 데이터를 가져와서 캐시에 저장 (동일한 요청 방지)
return (
<>
<ItemDetails id={id} />
<ItemReviews id={id} />
</>
)
}
// app/components/ItemDetails.tsx
import { getItem } from '@/utils/getItem'
export default async function ItemDetails({ id }: { id: string }) {
const item = await getItem(id) // 이미 캐싱된 데이터 사용
return <div>{item.name}</div>
}
// app/components/ItemReviews.tsx
import { getItem } from '@/utils/getItem'
export default async function ItemReviews({ id }: { id: string }) {
const item = await getItem(id) // 이미 캐싱된 데이터 사용
return <div>Reviews for {item.name}</div>
}