nextjs.org/docs 공식문서의 내용을 번역, 정리한 내용입니다.
사진 출처는 nextjs.org 공식문서입니다.
Next.js의 App Router는 함수를 async
로 표기하고, 프로미스를 await
하는 방식을 사용하며 React 컴포넌트 내에서 직접 데이터를 fetch 할 수 있도록 한다.
async
and await
in Server Components서버 컴포넌트에서 데이터를 fetch 하기 위해 async
와 await
을 사용할 수 있다.
async function getData() {
const res = await fetch('https://api.example.com/...);
// 에러 처리가 권장됨
if (!res.ok) {
// 가장 가까운 Error Boundary의 error.js를 활성화 시킨다.
throw new Error('데이터를 가져오지 못했습니다.');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
Next.js는 서버 컴포넌트에서 데이터 fetching 시 필요할 수 있는 서버 함수를 제공한다.
cookies()
headers()
use
in Client Componentsuse
는 await
와 개념적으로는 비슷하게 프로미스를 받는 새로운 React 함수이다. use
는 함수가 반환하는 프로미스를 컴포넌트, hooks, Suspense에 호환되는 방식으로 다룬다.
클라이언트 컴포넌트에서 fetch
를 use
로 감싸는 것은 현재로써는 추천되지 않는 방식이고, 여러번의 re-rendering을 발생시킬 수 있다. 그래서 Next.js는 만약 클라이언트 컴포넌트에서 데이터를 fetch 해야 한다면 SWR, React Query 같은 라이브러리를 활용하는 것을 추천한다고 한다.
기본적으로 fetch
는 자동으로 데이터를 fetch 하고 무기한 캐싱해 준다.
fetch('https://...'); // force-cache 가 기본 동작
캐싱된 데이터를 일정 시간마다 갱신하기 위해서는 fetch()
에서 next.revalidate
옵션을 사용하여 캐시의 생명주기(초 단위로)를 설정해 줄 수 있다.
fetch('https://...', { next: { revalidate: 10 } });
revalidate
또는 cache: 'force-cache'
로 fetch 단계에서 캐싱을 하는 것은 데이터를 요청들 사이에서 공유되는 캐시에 저장한다. 따라서 cookies()
함수에서 데이터를 가져오는 요청과 같이 유저에 특정된 데이터에는 사용하지 말아야 한다.
fetch
요청 때마다 최신 데이터를 fetch 하기 위해서는 cache: 'no-store'
옵션을 사용하면 된다.
fetch('https://...', { cache: 'no-store' });
클라이언트-서버 waterfall을 최소화 하기 위해서 데이터를 병렬적으로 fetch하는 패턴이 추천된다.
// app/artist/[username]/page.js
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 } }) {
// 두 요청을 병렬적으로 개시
const artistData = getArtist(username);
const albumsData = getArtistAlbums(username);
// 프로미스가 resolve 상태가 되는 것을 기다린다.
const [artist, albums] = await Promise.all([artistData, albumsData]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
);
}
서버 컴포넌트에서 await
을 부르기 전에 fetch를 시작하게 되면 각 요청이 동일한 시간에 fetch를 시작할 수 있다. 이는 waterfall을 피할 수 있도록 컴포넌트를 설정해 준다.
여러 요청을 병렬적으로 개시하여 시간을 아낄 수 있고, 유저는 프로미스가 resolve 되기 전까지는 렌더링 된 결과를 볼 수 없다.
UX를 개선시키기 위해 suspense boundary를 추가하여 렌더링 작업을 작게 쪼개고, 일부 결과를 최대한 빠르게 보여줄 수 있다.
import { getArtist, getArtistAlbums } from './api';
export default async function Page({ params: { username } }) {
// 두 요청을 병렬적으로 개시
const artistData = getArtist(username);
const albumData = getArtistAlbums(username);
// artist 프로미스가 먼저 resolve 되도록 하여
const artist = await artistData;
return (
<>
<h1>{artist.name}</h1>
{/* artist 정보를 먼저 처리하고,
albums를 suspense boundary로 감싸준다. */}
<Suspense fallback={<div>Loading...</div>}>
<Albums promise={albumData} />
</Suspense>
</>
);
}
// Albums 컴포넌트
async function Albums({ promise }) {
// albums 프로미스가 resolve 될 때까지 기다린 후 진행
const albums = await promise;
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.name}</li>
))}
</ul>
);
}
순차적으로 데이터를 fetch 하기 위해서 데이터를 필요로 하는 컴포넌트 내에서 직접 fetch
하거나, fetch
의 결과를 await
할 수 있다.
// ...
async function Playlists({ artistID }) {
// 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 } }) {
// artist를 기다림
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
);
}
컴포넌트 안에서 데이터를 fetch 한다면, route 내에 있는 각 fetch 요청과 중첩된 segment는 전에 진행되는 요청이나 segment가 완료 되기 전까지 데이터를 fetch 하는 것이나 렌더링을 시작할 수 없다.
데이터를 레이아웃에서 fetch하게 되면, 그 하위에 있는 route segment들의 렌더링의 경우 데이터 로딩이 끝난 이후에만 시작될 수 있다.
pages
디렉토리에서 서버 렌더링을 하는 페이지들은 getServerSideProps
가 종료될 때까지 로딩 스피너를 보여주었고, 그 뒤에 컴포넌트를 렌더링 해 주었다. 이를 "전부 또는 아무것도 없는" 데이터 fetching이라고 설명한다. 페이지를 위한 모든 데이터를 가지거나, 아예 갖고 있지 못하는 것이다.
app
디렉토리에서는 추가적인 옵션들도 있다.
loading.js
를 사용하면 데이터 fetching 함수에서 결과를 streaming 하는 동안 서버에서 로딩 상태를 보여줄 수 있다.가능하다면 항상 데이터를 사용하는 부분에서 fetch 하는 것이 가장 좋다. 이 방법은 전체 페이지가 아닌 실제로 로딩 중인 페이지의 부분에만 로딩 상태를 보여주는 것을 가능하게 해준다.
fetch()
ORM 같은 제3자 라이브러리르 사용하거나 데이터 베이스를 사용한다면 fetch
요청을 직접 사용하거나 설정하는 것이 어려울 수 있다.
fetch
를 사용할 수 없지만 레이아웃 또는 페이지의 캐싱이나 갱신 동작을 조절하고 싶다면 segment의 기본 캐싱 동작에 의존하거나 sement 캐싱 configuration(환경 설정)를 사용하면 된다.
fetch
를 직접 사용하지 않는 데이터 fechtching 라이브러리는 route의 캐싱 방식에 영향을 주지 않는다. 그리고 route segment에 따라 정적이거나 동적이게 된다.
만약 segment가 정적이라면 요청의 결과는 캐싱될 것이고, 만약 따로 설정되었다면 나머지 segment와 함께 갱신될 것이다. 만약 segment가 동적이라면 요청의 결과는 캐싱되지 않고, 매 요청 시 segment가 렌더링 될 때 re-fetch 될 것이다.
cookies()
와 headers()
같은 동적 함수들이 route segment를 동적으로 만든다.
임시적인 해결책으로 3자 쿼리의 캐싱 동작을 설정될 때까지 segment 환경설정으로 전체 segment의 캐싱 동작을 커스터마이징 할 수 있다.
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();
// ...
}