Next.JS 13 App Directory 중 Data Fetching/Fetching 문서를 보고 정리해야겠다 생각했다. 이 웹페이지의 내용을 한국어로 번역과 요약해보겠다.
Next.js 앱 라우터는 함수를 비동기로 표시하고 Promise에 대해 await를 사용하여 React 컴포넌트에서 직접 데이터를 가져올 수 있게 해줍니다. 데이터 가져오기는 fetch() API
와 React 서버 컴포넌트 위에 구축되어 있습니다. fetch()
를 사용하면 요청이 기본적으로 자동으로 중복 제거됩니다. Next.js는 각 요청이 자체 캐싱 및 재검증을 설정할 수 있도록 fetch()
옵션 객체를 확장합니다.
서버 컴포넌트에서 데이터를 가져오기 위해 async와 await를 사용할 수 있습니다.
async function getData() {
const res = await fetch('https://api.example.com/...')
// 반환 값은 *직렬화되지 않습니다*
// Date, Map, Set 등을 반환할 수 있습니다.
// 추천: 오류 핸들링
if (!res.ok) {
// 가장 가까운 `error.js` 오류 경계를 활성화합니다.
throw new Error('데이터를 가져오는 데 실패했습니다.')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
알아두면 좋은 점: TypeScript와 함께 비동기 서버 컴포넌트를 사용하려면
TypeScript 5.1.3
이상 및@types/react 18.2.8
이상을 사용해야 합니다.
Next.js는 서버 컴포넌트에서 데이터를 가져올 때 필요할 수 있는 유용한 서버 함수를 제공합니다:
cookies()
headers()
use
훅use
는 await와 개념적으로 유사한 promise를 받아들이는 새로운 React 함수입니다. use
는 함수가 반환하는 promise를 컴포넌트, 훅, 그리고 Suspense와 호환되는 방식으로 처리합니다. React RFC에서 use
에 대해 더 자세히 알아보세요.
현재로서는 클라이언트 컴포넌트에서 fetch를 use
로 감싸는 것은 권장되지 않으며, 여러 번의 리렌더링을 유발할 수 있습니다. 현재로서는 클라이언트 컴포넌트에서 데이터를 가져오는 경우, SWR 또는 React Query와 같은 서드파티 라이브러리를 사용하는 것을 권장합니다.
알아두면 좋은 점:
fetch
와use
가 클라이언트 컴포넌트에서 작동하게 되면 더 많은 예제를 추가할 예정입니다.
기본적으로, fetch()
는 자동으로 데이터를 가져오고 무기한으로 캐시합니다.
fetch('https://...') // cache: 'force-cache'가 기본값입니다.
타임 인터벌에 따라 캐시된 데이터를 재검증하려면, fetch()에서 next.revalidate 옵션을 사용하여 리소스의 캐시 수명(초 단위)을 설정할 수 있습니다.
fetch('https://...', { next: { revalidate: 10 } })
재검증 데이터에 대한 자세한 정보를 보려면 여기를 클릭하세요.
알아두면 좋은 점:
revalidate
또는cache: 'force-cache'
를 사용하여 fetch 수준에서 캐싱하면 데이터가 공유 캐시에서 요청 간에 저장됩니다. 사용자 특정 데이터(즉,cookies()
또는headers()
에서 데이터를 파생하는 요청)에 대해 사용하는 것을 피해야 합니다.
모든 fetch 요청에서 신선한 데이터를 가져오려면, cache: 'no-store'
옵션을 사용하세요.
fetch('https://...', { cache: 'no-store' })
클라이언트-서버 워터폴(네트워크 요청이 동기적, 순차적으로 이루어지는 패턴)을 최소화하기 위해, 데이터를 병렬로 가져오는 이 패턴을 권장합니다:
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getArtistAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 두 요청을 병렬로 시작합니다.
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// promise가 모두 해결될 때까지 기다립니다.
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
서버 컴포넌트에서 await를 호출하기 전에 fetch를 시작함으로써, 각 요청은 동시에 요청을 적극적으로 시작할 수 있습니다. 이렇게 하면 컴포넌트가 워터폴을 피할 수 있게 설정됩니다. 두 요청을 병렬로 시작함으로써 시간을
절약할 수 있지만, 사용자는 두 promise가 모두 해결될 때까지 렌더링 결과를 볼 수 없습니다.
사용자 경험을 향상시키기 위해, suspense 경계를 추가하여 렌더링 작업을 분할하고 가능한 한 빨리 결과의 일부를 보여줄 수 있습니다:
import { getArtist, getArtistAlbums, type Album } from './api'
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 두 요청을 병렬로 시작합니다.
const artistData = getArtist(username)
const albumData = getArtistAlbums(username)
// 먼저 아티스트의 promise를 해결합니다.
const artist = await artistData
return (
<>
<h1>{artist.name}</h1>
{/* 아티스트 정보를 먼저 보내고,
앨범을 suspense 경계로 감쌉니다. */}
<Suspense fallback={<div>Loading...</div>}>
<Albums promise={albumData} />
</Suspense>
</>
)
}
// Albums Component
async function Albums({ promise }: { promise: Promise<Album[]> }) {
// 앨범 promise가 해결될 때까지 기다립니다.
const albums = await promise
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.name}</li>
))}
</ul>
)
}
컴포넌트 구조를 개선하는 데 대한 자세한 정보는 프리로딩 패턴을 참조하세요.
데이터를 순차적으로 가져오려면, 필요한 컴포넌트 내에서 직접 가져오거나, 필요한 컴포넌트 내에서 fetch의 결과를 await할 수 있습니다:
// ...
async function Playlists({ artistID }: { artistID: string }) {
// 플레이리스트를 기다립니다.
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 }
}) {
// 아티스트를 기다립니다.
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
컴포넌트 내에서 데이터를 가져오면, 각 fetch 요청과 루트의 중첩된 세그먼트는 이전 요청 또는 세그먼트가 완료될 때까지 데이터를 가져오고
렌더링을 시작할 수 없습니다.
레이아웃에서 데이터를 가져오면, 그 아래의 모든 루트 세그먼트는 데이터가 로드 완료될 때까지 렌더링을 시작할 수 없습니다.
페이지 디렉토리에서, 서버 렌더링을 사용하는 페이지는 getServerSideProps가 완료될 때까지 브라우저 로딩 스피너를 보여주고, 그 페이지에 대한 React 컴포넌트를 렌더링합니다. 이를 "모든 것 또는 아무 것도 없는" 데이터 가져오기라고 할 수 있습니다. 전체 페이지에 대한 데이터가 있거나 없습니다.
앱 디렉토리에서는 탐색할 추가 옵션이 있습니다:
가능한 경우, 그것을 사용하는 세그먼트에서 데이터를 가져오는 것이 가장 좋습니다. 이렇게 하면 페이지의 로딩 부분만 로딩 상태를 보여줄 수 있습니다.
fetch()
없는 데이터 가져오기ORM이나 데이터베이스 클라이언트와 같은 서드파티 라이브러리를 사용하는 경우, 직접 fetch 요청을 사용하고 구성할 수 있는 능력이 항상 있는 것은 아닙니다.
fetch를 사용할 수 없지만 여전히 레이아웃 또는 페이지의 캐싱 또는 재검증 동작을 제어하려는 경우, 세그먼트의 기본 캐싱 동작에 의존하거나 세그먼트 캐시 구성을 사용할 수 있습니다.
fetch를 직접 사용하지 않는 데이터 가져오기 라이브러리는 라우트의 캐싱에 영향을 주지 않으며, 라우트 세그먼트에 따라 정적 또는 동적일 것입니다.
세그먼트가 정적(기본)인 경우, 요청의 출력은 세그먼트의 나머지 부분과 함께 캐시되고 재검증됩니다(설정된 경우). 세그먼트가 동적인 경우, 요청의 출력은 캐시되지 않고 세그먼트가 렌더링될 때마다 요청이 다시 가져와집니다.
알아두면 좋은 점: 동적 함수인
cookies()
와headers()
는 라우트 세그먼트를 동적으로 만듭니다.
서드파티 쿼리의 캐싱 동작을 구성할 수 있을 때까지의 임시 해결책으로, 세그먼트 구성을 사용하여 전체 세그먼트의 캐시 동작을 사용자 정의할 수 있습니다.
import prisma from './lib/prisma'
export const revalidate = 3600 // 매시간마다 재검증합니다.
async function getPosts() {
const posts = await prisma.post.findMany()
return posts
}
export default async function Page() {
const posts = await getPosts()
// ...
}
Next.js에서 데이터 가져오기는 fetch() API
와 React 서버 컴포넌트를 기반으로 한다. 이를 통해 비동기 함수를 사용하여 React 컴포넌트에서 직접 데이터를 가져올 수 있다.
서버 컴포넌트: 데이터를 가져오기 위해 async와 await를 사용할 수 있음. 이를 통해 데이터를 직접 가져오고 오류를 처리할 수 있음.
클라이언트 컴포넌트: 'use'라는 새로운 React 함수를 사용하여 promise를 처리할 수 있음. 현재로서는 클라이언트 컴포넌트에서 fetch를 use로 감싸는 것은 권장되지 않음.
데이터 가져오기 패턴: 병렬 데이터 가져오기를 통해 클라이언트-서버 워터폴을 최소화할 수 있음. 순차적 데이터 가져오기는 필요한 컴포넌트 내에서 직접 데이터를 가져오는 방식.
캐싱: fetch는 기본적으로 데이터를 자동으로 가져오고 무기한으로 캐시. 캐시된 데이터를 재검증하려면, fetch()에서 next.revalidate 옵션을 사용할 수 있음.
세그먼트 캐시 구성: 세그먼트의 기본 캐싱 동작에 의존하거나 세그먼트 캐시 구성을 사용하여 캐싱 또는 재검증 동작을 제어할 수 있음.