React와 Next.js 13은 응용 프로그램에서 데이터를 가져오고 관리하기 위한 새로운 방법을 소개했다. 이 새로운 데이터 가져오기 시스템은 app
디렉토리에서 작동하며, fetch()
웹 API를 기반으로 구축되었다.
fetch()
는 원격 리소스를 가져오기 위해 사용되는 웹 API로, 프로미스를 반환한다. React는 fetch를 확장하여 자동 요청 중복 제거 기능을 제공하며, Next.js는 fetch
옵션 객체를 확장하여 각 요청이 자체 캐싱
과 재유효화
(revalidating)을 설정할 수 있도록 한다.
Server Components에서 데이터를 가져오기 위해 제안된 React RFC를 사용하면 async
와 await
를 사용할 수 있다.
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
💡 Async Server Component TypeScript 오류
async
Server Components를 사용하면 Promise<Element>' is not a valid JSX element
오류가 발생한다.이는 TypeScript의 알려진 문제이며, 업스트림에서 작업 중이다.
일시적인 해결책으로 컴포넌트 위에 {/* @ts-expect-error Async Server Component */}
를 추가하여 해당 컴포넌트의 타입 검사를 비활성화할 수 있다.
Next.js는 Server Components에서 데이터를 가져올 때 필요한 유용한 서버 함수를 제공한다.
cookies()
headers()
use
는 새로운 React 함수로, 개념적으로 await
과 유사한 프로미스를 받는다. use
는 컴포넌트, 훅 그리고 Suspense와 호환되는 방식으로 함수가 반환한 프로미스를 처리한다.
현재로서는 Client 컴포넌트에서 fetch
를 use
로 감싸는 것은 권장되지 않으며, 여러 번의 재렌더링을 발생시킬 수 있다. 현재는 Client 컴포넌트에서 데이터를 가져와야 할 경우, SWR
이나 React Query
와 같은 제3자 라이브러리를 사용하는 것을 권장한다.
기본적으로 fetch
는 데이터를 자동으로 가져와 영구적으로 캐시한다.
캐시된 데이터를 일정 간격으로 재유효화하기 위해, fetch()
에서 next.revalidate
옵션을 사용하여 리소스의 캐시 유효 기간(초 단위)을 설정할 수 있다.
fetch('https://...', { next: { revalidate: 10 } });
참고로 revalidate
또는 cache: 'force-cache'
를 통한 fetch 수준의 캐싱은 공유 캐시에 데이터를 저장한다. 따라서 쿠키(cookies()
)나 헤더(headers()
)에서 데이터를 가져오는 사용자별 데이터에는 사용하지 않는 것이 좋다.
매번 요청할 때마다 신선한 데이터를 가져오려면 cache: 'no-store'
옵션을 사용한다.
fetch('https://...', { cache: 'no-store' });
클라이언트-서버 간의 지연을 최소화하기 위해 데이터를 병렬로 가져오는 다음 패턴을 권장한다.
// 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 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 };
}) {
// Initiate both requests in parallel
const artistData = getArtist(username);
const albumsData = getArtistAlbums(username);
// Wait for the promises to resolve
const [artist, albums] = await Promise.all([artistData, albumsData]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
);
}
Server 컴포넌트에서 await
을 호출하기 전에 fetch
를 시작함으로써 각 요청은 동시에 요청을 가져오도록 할 수 있다. 이렇게 하면 컴포넌트가 워터폴을 피할 수 있도록 설정된다.
병렬로 두 요청을 시작하여 시간을 절약할 수 있지만, 두 프로미스가 모두 해결될 때까지 사용자에게 렌더링된 결과가 표시되지 않는다.
사용자 경험을 향상시키기 위해 서스펜스 경계를 추가하여 렌더링 작업을 분할하고 가능한 빨리 일부 결과를 표시할 수 있다.
import { getArtist, getArtistAlbums, type Album } from './api';
export default async function Page({
params: { username },
}: {
params: { username: string };
}) {
// Initiate both requests in parallel
const artistData = getArtist(username);
const albumData = getArtistAlbums(username);
// Wait for the artist's promise to resolve first
const artist = await artistData;
return (
<>
<h1>{artist.name}</h1>
{/* Send the artist information first,
and wrap albums in a suspense boundary */}
<Suspense fallback={<div>Loading...</div>}>
<Albums promise={albumData} />
</Suspense>
</>
);
}
// Albums Component
async function Albums({ promise }: { promise: Promise<Album[]> }) {
// Wait for the albums promise to resolve
const albums = await promise;
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.name}</li>
))}
</ul>
);
}
데이터를 순차적으로 가져오려면 필요한 컴포넌트 내에서 직접 fetch를 수행하거나, 필요한 컴포넌트 내에서 fetch의 결과를 기다릴 수 있다.
// app/artist/page.tsx
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>
</>
);
}
컴포넌트 내에서 데이터를 가져오면 각 fetch 요청 및 라우트의 중첩 세그먼트는 이전 요청이나 세그먼트가 완료될 때까지 데이터를 가져오고 렌더링할 수 없다.
레이아웃에서 데이터를 가져오면 해당 데이터가 로딩이 완료될 때까지 그 아래에 있는 모든 라우트 세그먼트에 대한 렌더링은 시작되지 않는다.
pages
디렉토리에서 서버 사이드 렌더링을 사용하는 페이지는 getServerSideProps
가 완료될 때까지 브라우저 로딩 스피너를 보여주고, 그 후에 해당 페이지의 리액트 컴포넌트를 렌더링한다. 이는 페이지에 대한 전체 데이터를 가져오거나 아무런 데이터도 가져오지 않는 것이다.
💡 app 디렉토리에서는 추가적인 옵션을 사용할 수 있다.
loading.js
를 사용하여 데이터 가져오기 함수의 결과를 스트리밍하는 동안 서버에서 즉시 로딩 상태를 보여줄 수 있다.
데이터 패칭을 컴포넌트 트리의 하위로 이동시켜 페이지의 필요한 부분만 렌더링을 차단할 수 있다. 예를 들어, 데이터 패칭을 루트 레이아웃에서 가져오는 대신 특정 컴포넌트로 이동시킬 수 있다.
가능하면 데이터를 사용하는 세그먼트에서 데이터를 가져오는 것이 좋다. 이렇게 하면 로딩 상태를 페이지의 일부분에만 표시할 수 있고 전체 페이지에 표시하지 않아도 된다.
fetch()
를 직접 사용하고 구성할 수 없는 경우, ORM이나 데이터베이스 클라이언트와 같은 제3자 라이브러리를 사용하고 있다면 항상 fetch 요청을 사용할 수 없을 수 있다.
fetch를 사용할 수 없지만 레이아웃이나 페이지의 캐싱 또는 재유효화 동작을 제어하고 싶은 경우, 세그먼트의 기본 캐싱 동작을 의존하거나 세그먼트 캐시 구성을 사용할 수 있다.
fetch
를 직접 사용하지 않는 모든 데이터 가져오기 라이브러리는 라우트의 캐싱에 영향을 주지 않으며, 라우트 세그먼트에 따라 정적(static) 또는 동적(dynamic)일 수 있다.
세그먼트가 정적인 경우(기본값), 요청의 출력은 세그먼트의 나머지 부분과 함께 캐싱되고 재유효화된다.(구성에 따라). 세그먼트가 동적인 경우, 요청의 출력은 캐시되지 않으며 세그먼트가 렌더링될 때마다 매 요청마다 다시 가져온다.
cookies()
와 headers()
와 같은 동적 함수는 라우트 세그먼트를 동적으로 만든다.
제3자 쿼리의 캐싱 동작을 구성할 수 있는 임시 솔루션으로, 세그먼트 구성을 사용하여 전체 세그먼트의 캐시 동작을 사용자 정의할 수 있다.
// app/page.tsx
import prisma from './lib/prisma';
export const revalidate = 3600; // revalidate every hour
async function getPosts() {
const posts = await prisma.post.findMany();
return posts;
}
export default async function Page() {
const posts = await getPosts();
// ...
}
[출처]
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching