[ Data Fetching ] Fetching

차차·2023년 5월 19일
1

Next Docs

목록 보기
19/34
post-thumbnail

React와 Next.js 13은 응용 프로그램에서 데이터를 가져오고 관리하기 위한 새로운 방법을 소개했다. 이 새로운 데이터 가져오기 시스템은 app 디렉토리에서 작동하며, fetch() 웹 API를 기반으로 구축되었다.

fetch()는 원격 리소스를 가져오기 위해 사용되는 웹 API로, 프로미스를 반환한다. React는 fetch를 확장하여 자동 요청 중복 제거 기능을 제공하며, Next.js는 fetch 옵션 객체를 확장하여 각 요청이 자체 캐싱재유효화(revalidating)을 설정할 수 있도록 한다.



서버 컴포넌트에서 async / await

Server Components에서 데이터를 가져오기 위해 제안된 React RFC를 사용하면 asyncawait를 사용할 수 있다.

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()



Client 컴포넌트에서의 use 사용

use는 새로운 React 함수로, 개념적으로 await과 유사한 프로미스를 받는다. use는 컴포넌트, 훅 그리고 Suspense와 호환되는 방식으로 함수가 반환한 프로미스를 처리한다.


현재로서는 Client 컴포넌트에서 fetchuse로 감싸는 것은 권장되지 않으며, 여러 번의 재렌더링을 발생시킬 수 있다. 현재는 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 디렉토리에서는 추가적인 옵션을 사용할 수 있다.

  1. loading.js를 사용하여 데이터 가져오기 함수의 결과를 스트리밍하는 동안 서버에서 즉시 로딩 상태를 보여줄 수 있다.

  2. 데이터 패칭을 컴포넌트 트리의 하위로 이동시켜 페이지의 필요한 부분만 렌더링을 차단할 수 있다. 예를 들어, 데이터 패칭을 루트 레이아웃에서 가져오는 대신 특정 컴포넌트로 이동시킬 수 있다.
    가능하면 데이터를 사용하는 세그먼트에서 데이터를 가져오는 것이 좋다. 이렇게 하면 로딩 상태를 페이지의 일부분에만 표시할 수 있고 전체 페이지에 표시하지 않아도 된다.



Fetch( ) 없이 데이터 패칭

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

profile
나는야 프린이

0개의 댓글