데이터 가져오기
알아두면 좋습니다:
이 새로운 데이터 Fetching 모델은 현재 React 팀에서 개발 중입니다. 서버 컴포넌트에 비동기/대기(async/await)를 도입하고 클라이언트 컴포넌트에 새로운 use() 훅을 도입하는 약속에 대한 지원 React RFC를 읽어보시길 권장합니다.
사용해 볼 수는 있지만 아직 안정적이지 않습니다. 최신 개발 사항을 반영하기 위해 이 문서를 계속 업데이트할 예정입니다.
React와 Next.js 13에는 애플리케이션에서 데이터를 가져오고 관리하는 새로운 방법이 도입되었습니다. 새로운 데이터 불러오기 시스템은 app 디렉토리에서 작동하며 fetch() 웹 API 위에 구축됩니다.
fetch()는 프로미스를 반환하는 원격 리소스를 가져오는 데 사용되는 웹 API입니다. React는 fetch를 확장하여 자동 요청 중복 제거를 제공하고, Next.js는 fetch 옵션 객체를 확장하여 각 요청이 자체 캐싱 및 재검증을 설정할 수 있도록 합니다.
서버 컴포넌트에서 async/await
제안된 React RFC를 사용하면 서버 컴포넌트에서 데이터를 가져오는 데 async 및 await를 사용할 수 있습니다.
// app/page.tsx
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(비동기) 서버 컴포넌트 타입스크립트 오류
비동기 서버 컴포넌트가 사용된 곳에서 'Promise<Element>'가 유효한 JSX 엘리먼트 유형이 아니라는 오류를 발생시킵니다.
이는 TypeScript의 알려진 문제이며 업스트림에서 작업 중입니다.
임시 해결 방법으로 컴포넌트 위에 {/* @ts-expect-error Async Server Component */} 를 추가하여 해당 컴포넌트에 대한 유형 검사를 비활성화할 수 있습니다.
서버 컴포넌트 함수
Next.js는 서버 컴포넌트에서 데이터를 가져올 때 필요할 수 있는 유용한 서버 함수를 제공합니다:
클라이언트 컴포넌트에서의 use
use는 await과 개념적으로 유사한 프로미스를 수락하는 새로운 React 함수입니다. use는 컴포넌트, hook, Suspense와 호환되는 방식으로 함수가 반환한 프로미스를 처리합니다. React RFC에서 use에 대해 자세히 알아보세요.
현재 클라이언트 컴포넌트에서 use안의 fetch를 래핑하는 것은 권장되지 않으며 여러 번의 재렌더링을 유발할 수 있습니다. 현재로서는 클라이언트 컴포넌트에서 데이터를 불러와야 하는 경우 SWR 또는 React Query와 같은 서드파티 라이브러리를 사용하는 것이 좋습니다.
참고: 클라이언트 컴포넌트에서 fetch 와 use가 동작하는 더 많은 예제를 추가할 예정입니다.
정적 데이터 가져오기
기본적으로 fetch는 데이터를 자동으로 가져와서 무기한 캐시합니다.
fetch('https://...'); // cache: 'force-cache' is the default
데이터 재검증
시간 간격을 두고 캐시된 데이터의 유효성을 재검증하려면 fetch()의 next.revalidate 옵션을 사용하여 리소스의 cache lifetime(초)을 설정하면 됩니다.
fetch('https://...', { next: { revalidate: 10 } });
자세한 내용은 데이터 재검증을 참조하세요.
참고: 캐시를 통한 가져오기 수준에서의 cache: 'force-cache' 와 revalidate 는 여러 요청에 걸쳐 데이터를 공유 캐시에 저장합니다. 사용자 특정 데이터(예: cookies() 또는 headers()에서 데이터를 파생하는 요청)에는 사용하지 않아야 합니다.
동적 데이터 가져오기
모든 fetch 요청에서 새로운 데이터를 가져오려면 cache: 'no-store' 옵션을 사용하세요.
fetch('https://...', { cache: 'no-store' });
병렬 데이터 가져오기
클라이언트-서버 워터폴을 최소화하려면 데이터를 병렬로 가져오는 이 패턴을 권장합니다:
// app/artist/[username]/page.jsx
import Albums from './albums';
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`);
return res.json();
}
async function getArtistAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`);
return res.json();
}
export default async function Page({ params: { username } }) {
// 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>
</>
);
}
서버 컴포넌트에서 await을 호출하기 전에 가져오기를 시작하면 각 요청이 동시에 열심히 요청을 가져오기 시작할 수 있습니다. 이렇게 하면 워터폴을 피할 수 있도록 컴포넌트가 설정됩니다.
두 요청을 동시에 시작하면 시간을 절약할 수 있지만 두 프로미스가 모두 해결될 때까지 사용자는 렌더링된 결과를 볼 수 없습니다.
사용자 경험을 개선하기 위해 서스펜스 경계를 추가하여 렌더링 작업을 분할하고 결과의 일부를 가능한 한 빨리 표시할 수 있습니다:
// ...
export default async function Page({ params: { username } }) {
// 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 }) {
// 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 결과를 await 할 수 있습니다:
// ...
async function Playlists({ artistID }) {
// 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 } }) {
// Wait for the artist
const artist = await getArtist(username);
return (
<div>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</div>
);
}
컴포넌트 내부에서 데이터를 가져오면 경로의 각 fetch 요청과 중첩된 세그먼트는 이전 요청이나 세그먼트가 완료될 때까지 데이터 가져오기 및 렌더링을 시작할 수 없습니다.
Route에서 렌더링 차단
layout에서 데이터를 가져오면 그 아래의 모든 경로 세그먼트에 대한 렌더링은 데이터 로딩이 완료된 후에만 시작할 수 있습니다.
pages 디렉토리에서 서버 렌더링을 사용하는 페이지는 getServerSideProps가 완료될 때까지 브라우저 로딩 스피너를 표시한 다음 해당 페이지에 대한 React 컴포넌트를 렌더링합니다. 이것은 "전부 아니면 아무것도" 데이터 불러오기라고 설명할 수 있습니다. 페이지에 대한 전체 데이터를 가져올 수도 있고, 아무것도 안가져올 수도 있습니다.
app 디렉토리에서 탐색할 수 있는 추가 옵션이 있습니다:
- 첫째, loading.js를 사용하여 데이터 불러오기 함수에서 결과를 스트리밍하는 동안 서버에서 즉시 로딩 상태를 표시할 수 있습니다.
- 둘째, 컴포넌트 트리에서 데이터 불러오기를 아래로 이동하여 페이지에서 필요한 부분의 렌더링만 차단할 수 있습니다. 예를 들어 루트 레이아웃에서 데이터를 가져오지 않고 특정 컴포넌트로 데이터 가져오기를 이동하는 것입니다.
가능하면 데이터를 사용하는 세그먼트에서 데이터를 가져오는 것이 가장 좋습니다. 이렇게 하면 페이지 전체가 아닌 로딩 중인 부분만 로딩 상태를 표시할 수도 있습니다.
fetch() 없이 데이터 가져오기
ORM(객체와 데이터베이스의 관계를 매핑해주는 도구)이나 데이터베이스 클라이언트와 같은 타사 라이브러리를 사용하는 경우 fetch 요청을 직접 사용하고 구성할 수 있는 기능이 항상 있는 것은 아닙니다.
fetch를 사용할 수 없지만 레이아웃이나 페이지의 캐싱 또는 재검증 동작을 제어하려는 경우 세그먼트의 기본 캐싱 동작을 사용하거나 세그먼트 캐시 구성을 사용할 수 있습니다.
기본 캐싱 동작
fetch를 직접 사용하지 않는 데이터 가져오기 라이브러리는 route 캐싱에 영향을 주지 않으며, route 세그먼트에 따라 정적이거나 동적입니다.
세그먼트가 정적(기본값)인 경우 요청의 출력은 나머지 세그먼트와 함께 캐시되고 재검증됩니다(구성된 경우). 세그먼트가 동적인 경우 요청의 출력은 캐시되지 않으며 세그먼트가 렌더링될 때마다 요청을 다시 가져옵니다.
알아두면 좋습니다: cookies() 및 headers()와 같은 동적 함수는 route 세그먼트를 동적으로 만듭니다.
세그먼트 캐시 구성
임시 해결책으로 타사 쿼리의 캐시 동작을 구성할 수 있을 때까지 세그먼트 구성을 사용하여 전체 세그먼트의 캐시 동작을 사용자 지정할 수 있습니다.
// app/page.tsx
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();
// ...
}