Next.js(ver.13)

Data Fetching_Fetching

ecoEarth 2023. 7. 10. 22:33

Data Fetching

  • Next.js의 App router에서는 async와 await개념을 통해 promise기반의 데이터통신을이전 Next.js버전(Pages router)과 달리 페이지단위가 아니라 컴포넌트단위로 데이터를 fetch할 수 있다.
  • Next.js의 Data fetching은 웹 API인 fetch API와 React의 Sever components에 기반한다. fetch API를 사용할 경우 자동적으로 중복된 요청이 제거된다.
  • Next.js fetch API는 fetch 옵션객체에 caching옵션과 revalidating옵션을 설정할 수 있게 했다.

 

async and await in Server Components

async와 await를 통해 Server Components안에서 데이터를 받아올 수 있다.

async function getData() {
  const res = await fetch('https://api.example.com/...')
  // return value는 직렬화되지 않은 상태이다.
  // You can return Date, Map, Set, etc.
 
  // Recommendation: handle errors
  if (!res.ok) {
    // 이 조건문 안으로 들어온다면 '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>
}
Good to know:
To use an async Server Component with TypeScript, ensure you are using TypeScript 5.1.3 or higher and @types/react 18.2.8 or higher.

 

Server Component Functions

Next.js는 유용한 server 함수들을 Server Components안에서 사용할 수 있도록 해준다.

 

'use' in Client Components

use는 await와 개념적으로 비슷하게 promise를 받아들이는 React의 함수이다. use는 컴포넌트, hooks, Suspense와 호환되는 방식으로 함수에 의해 반환되는 promise를 처리한다. 관련 내용을 React RFC에서 알아보자.

현재 use에서 사용되는 fetch래핑은 Client Components에서 권장되지 않으며, 복수의 리렌더링을 촉발할 수 있다. 현재상황으로는 client component에서 data를 fetch할 필요가 있다면 SWR이나 React Query와 같은 서드파티 라이브러리를 사용할 것을 권한다.

 

 

Static Data Fetching

기본적으로 fetch API는 데이터를 송신함과 함께 데이터를 캐시한다. -> fetch API에 'force-cache'옵션이 기본적으로 적용됨

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

 

Revalidating Data

캐시된 데이터를 시간간격에 의해 다시 받아오도록 하고싶다면 fetch()의 next.revalidate 옵션을 사용하여 리소스의 캐시 수명(초)을 설정할 수 있다.

 

fetch('https://...', { next: { revalidate: 10 } })
fetch API를 이용한 캐시는 공유될 수 있다. 따라서 유저의 사적인 데이터는 캐싱을 사용하지 않도록 옵션을 설정해야 한다.

 

Dynamic Data Fetching

매번의 fetch 요청마다 데이터를 fetch하려면, cache: 'no-store' 옵션을 사용하자.

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

 

Data Fetching Patterns

parallel Data fetching

client-server waterfalls를 최소화하기 위해 데이터를 수평적으로 fetch하는 parallel Data fetching기법을 추천한다.

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를 시작하면, 각 요청은 동시에 요청을 fetch하는 것을 즉시 시작한다. 이것은 컴포넌트를 설정하여 waterfalls를 피할 수 있도록 해준다. 이러한 방식은 두 요청을 동시에 시작하여 시간을 절약할 수 있다. 그러나 사용자는 두 promise들이 resolve되기 전에는 렌더링된 결과를 볼 수 없다. 사용자 경험을 개선하려면 suspense 영역을 추가하여 렌더링 작업을 분할하고 결과의 일부를 가능한 빨리 표시할 수 있다.

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하려면 데이터를 필요로하는 컴포넌트에서 직접적으로 fetch하거나 데이터를 필요로 하는 컴포넌트 안에서 fetch한 결과에 await처리를 할 수 있다.

// ...
 
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 및 렌더링을 시작할 수 없다.

 

Blocking Rendering in a Route

레이아웃단에서 데이터를 fetch하면 그 아래의 모든 경로 세그먼트에 대한 렌더링은 데이터로딩이 완료된 후에서야 시작될 수 있다.

  1. 첫째로, data fetching함수의 결과를 스트리밍하는동안  서버의 로딩 상태를 보여줄 수 있는 loading.js를 사용할 수 있다.
  2. 둘째로, 렌더링 블라킹을 필요한 페이지에만 적용하기 위하여 data fetching을 컴포넌트 트리의 더 낮은 곳으로 옮길 수 있다.
    예를 들어 root layout에서 데이터를 fetch하기 보다 특정 컴포넌트로 옮기는 것이다.

가능하면 항상 데이터를 사용하는 segment에서 fetch하는 것이 좋다. 그렇게 되면 로딩이 되는 페이지에서만 로딩 상태를 보여주는 것을 가능케 할 수 있다.

 

Data fetching without fetch()

만약 ORM이나 DB client와 같은 서드-파티 라이브러리를 사용하고 있다면 항상 fetch 요청을 사용할 수는 없을 것이다.

만약 fetch를 쓸 수 없는데 layout이나 page에서의 caching이나 revalidating을 제어하고 싶다면 default caching behavior에 의존하거나 segment chache configuration을 사용하면 된다.

 

 Default Caching Behavior

fetch 를 직접적으로 사용하지 않는 어떤 data fetching library도 route에서의 캐싱에 영향을 미치지 않을 것이며, route segment에 따라 static하거나 dynamic할 것이다.

segment가 static(default)하다면, 요청의 결과는 캐시될 것이며 segment의 나머지 부분과 함께 유효성 revalidate될 것이다. segment가 dynamic한 경우 요청의 결과는 캐시되지 않으며, segment가 렌더링될 때 모든 요청을 re-fetch할 것이다.

Good to know: cookies()와 headers()와 같은 Dynamic functions들은 route segment를 dynamic하기 만들 것이다.

 

Segment Cache Configuration

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

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