logo
PostsInvestingHomebarArticlesAbout

Next.js 13 / 1. Data Fetching

2023.06.28 / 9min

Intro

Next.js에서 App Router를 사용하면, async, await를 사용하여 비동기적으로 React Components에서 직접 데이터를 가져올 수 있다.

Data Fetching은 fetch() Web API와 React Server Components를 기반으로 만들어졌다. fetch()를 사용하면 자동으로 중복 requests 호출이 제거된다.

request의 caching and revalidating을 설정할 수 있도록 fetch() 확장 옵션을 제공한다.


TOC

async and await in Server Components

Server Components에서 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>
}

Server Component를 Typescript와 같이 사용하려면 TypeScript 5.1.3 이상 버전과 @types/react 18.2.8 이상 버전을 사용해야한다.

Server Component Functions

데이터를 가져올 때 필요할 수 있는 함수를 제공한다.

  • cookies()
  • headers()

use in Client Components

useawait과 개념적으로 유사한 promise를 사용할 수 있는 새로운 React 함수다. use는 components, hooks, Suspense와 호환되는 방식으로 함수가 반환한 프로미스를 처리한다.

현재 Client Components에서 use로 fetch를 감싸는 것은 여러 번의 재렌더링을 유발할 수 있어 권장되지 않는다. 현재로서는 Client Components에서 데이터를 불러와야 하는 경우 SWR 또는 React Query와 같은 라이브러리를 사용하는 것을 추천한다.

이 기능이 Client Components에서도 가능하다면 많은게 가능해질거 같다.


Static Data Fetching

기본적으로 fetch는 데이터를 가져와서 무기한 캐싱한다.

fetch('https://...') // cache: 'force-cache' is the default

Revalidating Data

시간 간격을 두고 캐시된 데이터(cached data)의 유효성을 재검증하려면 fetch()next.revalidate 옵션을 사용하여 자원의 cache 수명(초)을 설정하면 된다.

fetch('https://...', { next: { revalidate: 10 } })

revalidate 또는 cache: 'force-cache'를 사용한 fetch는 데이터를 공유 캐시에 저장한다. 사용자별 데이터(예: cookies() 또는 headers()에서 데이터를 요청하는 경우)에는 사용하면 안된다.


Dynamic Data Fetching

모든 요청에서 새로운 데이터를 가져오려면 cache: 'no-store' 옵션을 사용하면 된다.

fetch('https://...', { cache: 'no-store' })

Data Fetching Patterns

Parallel Data Fetching

client-server waterfalls을 최소화하려면, 이 패턴을 사용하여 데이터를 병렬로 가져오는 것이 좋다.

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 Component에서 await을 호출하기 전에 fetch를 시작하면 각 요청이 동시에 요청을 시작할 수 있다. 이렇게 하면 워터폴을 피할 수 있도록 컴포넌트가 설정된다.

두 요청을 동시에 시작하면 시간을 절약할 수 있지만, 두 프로미스가 모두 해결될 때까지 사용자는 렌더링된 결과를 볼 수 없는 문제가 있다.

사용자 경험을 개선하기 위해 suspense boundary를 추가하여 렌더링 작업을 분할하고, 결과의 일부를 가능한 한 빨리 표시할 수 있다.

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

Sequential Data Fetching

데이터를 순차적으로 가져오려면, 데이터가 필요한 컴포넌트 내부에서 직접 fetch를 사용하거나, fetchawait를 같이 사용하면 된다.

// ...
 
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>
    </>
  )
}

컴포넌트 내부에서 데이터를 가져오면 경로의 각 가져오기 요청과 중첩된 세그먼트는 이전 요청이나 세그먼트가 완료될 때까지 데이터 가져오기 및 렌더링을 시작할 수 없다.

Blocking Rendering in a Route

layout에서 데이터를 가져오면 그 아래의 모든 경로 세그먼트에 대한 렌더링은 데이터 로딩이 완료된 후에만 시작할 수 있다.

page 디렉토리에서 서버 렌더링을 사용하는 페이지는 getServerSideProps가 완료될 때까지 브라우저 로딩 스피너를 표시한 다음 해당 페이지에 대한 React Component를 렌더링한다. 이는 "all or nothing" 데이터 불러오기로 설명된다. 페이지에 대한 전체 데이터를 가져오거나, 아무것도 가져올 수 없는 2가지 경우만 있다.

app 디렉토리에는 탐색할 수 있는 추가 옵션이 있다.

  • 먼저 loading.js를 사용하여 데이터 불러오기 함수의 결과를 스트리밍하는 동안 서버에서 즉시 로딩되는 상태를 표시할 수 있다.
  • 두번째로, 컴포넌트 트리에서 데이터 가져오기를 더 아래(lower)로 이동하여 페이지에서 필요한 부분의 렌더링만 차단할 수 있다. 예를 들어 root layout에서 데이터를 가져오지 않고 특정 컴포넌트에서 데이터 가져오도록 하는 것이다.

가능하면 데이터를 사용하는 segment에서 데이터를 가져오는 것이 가장 좋다. 이렇게 하면 전체 페이지가 아닌 로드 중인 페이지의 일부분만 로드 상태를 표시할 수도 있다.


Data Fetching without fetch()

ORM이나 Database Client와 같은 타사 라이브러리를 사용하는 경우 가져오기 요청을 직접 사용하고 구성할 수 있는 기능이 모두 제공되는 것은 아니다.

fetch를 사용할 수 없지만 layout, page의 caching or revalidating 동작을 제어하려는 경우, segment의 기본 캐싱 동작에 의존하거나 segment 캐시 구성을 사용할 수 있다.

Default Caching Behavior

fetch를 사용하지 않는 데이터 가져오기 라이브러리는 경로 캐싱에 영향을 미치지 않으며 경로 segment에 따라 정적 또는 동적으로 작동한다.

segment가 정적(기본값)인 경우 요청의 출력은 나머지 segment와 함께 캐시되고 재검증됩니다(구성된 경우). segment가 동적인 경우 요청의 출력은 캐시되지 않으며 segment가 렌더링될 때마다 요청을 다시 가져온다.

Segment Cache Configuration

임시 해결책으로 타사 쿼리의 캐싱 동작을 구성할 수 있을 때까지 세그먼트 segment을 사용하여 전체 segment의 캐시 동작을 사용자 지정할 수 있다.

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()
  // ...
}

Reference


avatar
snyungSoftware Engineer(from. 2018)
social-mailsocial-githubsocial-facebooksocial-book