본문 바로가기

넥스트

[next.js] 12. 데이터 가져오기 패턴

반응형
본 내용은 next.js 공식 문서를 한글로 번역하여 정리한 자료입니다. 자세한 내용은 공식 사이트를 참조해 주세요.

https://nextjs.org/docs/app/building-your-application/data-fetching/patterns

 

Data Fetching: Data Fetching Patterns | Next.js

There are a few recommended patterns and best practices for fetching data in React and Next.js. This page will go over some of the most common patterns and how to use them. If you need to use the same data (e.g. current user) in multiple components in a tr

nextjs.org

 


  데이터 가져오기 패턴

React 및 Next.js에서 데이터를 가져오기 위한 몇 가지 권장 패턴과 모범 사례가 있습니다. 이 페이지에서는 가장 일반적인 패턴 중 일부와 사용 방법을 살펴보겠습니다.

  서버에서 데이터 가져오기

가능하다면 서버에서 데이터를 가져오는 것이 좋습니다. 이를 통해 다음을 수행할 수 있습니다.

 

  • 직접 백엔드 데이터 리소스(예: 데이터베이스)에 액세스할 수 있습니다.
  • 액세스 토큰 및 API 키와 같은 민감한 정보가 클라이언트에 노출되지 않도록 애플리케이션을 더 안전하게 유지합니다.
  • 데이터를 가져오고 동일한 환경에서 렌더링합니다. 이렇게 하면 클라이언트와 서버 간의 통신을 줄이고 클라이언트의 메인 스레드에서 작업을 줄입니다.
  • 클라이언트에서 여러 개별 요청 대신 단일 라운드 트립으로 여러 데이터 가져오기를 수행합니다.
  • 클라이언트-서버 폭포를 줄입니다.
  • 귀하의 지역에 따라 데이터 가져오기가 데이터 소스와 더 가까운 곳에서 발생할 수도 있습니다. 이는 지연을 줄이고 성능을 향상시킵니다.

   데이터가 필요한 곳에서 가져오기

트리 구조의 여러 구성 요소에서 동일한 데이터(예: 현재 사용자)를 사용해야 하는 경우 전역에서 데이터를 가져오거나 구성 요소 간에 props를 전달할 필요가 없습니다. 대신, 동일한 데이터에 대한 여러 요청을 하는 성능 영향에 대해 걱정하지 않고 데이터가 필요한 구성 요소에서 fetch 또는 React 캐시를 사용할 수 있습니다. 

 

이는 fetch 요청이 자동으로 메모이제이션되기 때문입니다. 요청 메모이제이션에 대해 자세히 알아보세요.

알아두면 좋은 점
 부모 레이아웃과 자식 간에 데이터를 전달할 수 없기 때문에 레이아웃에도 적용됩니다.

 

(개인정리) 동일한 데이터에 대한 중복 요청을 여러 컴포넌트에서 하더라도, fetch 요청에 대한 정보가 메모리에 별도로 저장되기 때문에, 향후 동일한 데이터에 대한 요청이 있을 경우 메모리 상에 존재하는 데이터를 사용하므로 중복요청에 의한 성능 저하를 염려할 필요가 없다

   스트리밍(Streaming)

스트리밍Suspense React 기능으로 UI의 렌더링 단위를 점진적으로 렌더링하고 클라이언트로 증분 스트리밍할 수 있습니다.

 

서버 구성 요소와 중첩된 레이아웃을 사용하면 데이터가 특별히 필요하지 않은 페이지의 일부를 즉시 렌더링하고 데이터를 가져오는 페이지의 일부에 로딩 상태를 표시할 수 있습니다. 즉, 사용자는 전체 페이지가 로드되기 전에 상호 작용하기 시작할 필요가 없습니다.


스트리밍 및 일시 중단에 대한 자세한 내용은 UI 로딩(https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming) 및 스트리밍 및 일시 중단 페이지(https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense)를 참조하세요.

(개인정리) 스트리밍과 Subpense 기능을 통해 사용자에게 미리 정적인 레이아웃을 보여줄 수 있고, 데이터를 가져오고 있는 페이지의 일부는 로딩 상태를 표시할 수 있다.

   병렬 및 순차 데이터 가져오기

 

 

React 구성 요소 내에서 데이터를 가져올 때 병렬 및 순차라는 두 가지 데이터 가져오기 패턴을 알아야 합니다.

순차적 데이터 가져오기를 사용하면 경로의 요청이 서로 종속되므로 폭포가 생성됩니다. 한 가져오기가 다른 가져오기의 결과에 따라 달라지기 때문에 이 패턴을 원하거나 리소스를 절약하기 위해 다음 가져오기 전에 조건이 충족되기를 원하는 경우가 있을 수 있습니다.

 

그러나 이 동작은 의도하지 않은 것일 수도 있으며 로딩 시간이 길어질 수도 있습니다.


병렬 데이터 가져오기를 사용하면 경로의 요청이 즉시 시작되고 동시에 데이터가 로드됩니다. 이렇게 하면 클라이언트-서버 폭포수와 데이터를 로드하는 데 걸리는 총 시간이 줄어듭니다.

(개인정리) 순차적 데이터 가져오기는 동기식을 말하는 듯하다. 하나의 요청이 처리된 후에 다음 요청이 처리되는 방식이므로 이전 요청이 실패하면 다음 요청은 실행되지 못하는 문제가 있을 수 있고, 이전 요청이 오래걸리면 그 만큼 전체 요청을 처리하는 데 많은 시간이 소요될 수 있다. 

이 때 병렬 데이터 가져오기를 통해 이전 요청을 기다리지 않고, 곧바로 동시에 데이터를 로드할 수 있고, 동기식 처리로 인한 폭포수 및 데이터 로드 시간을 줄일 수 있게 된다

   순차적 데이터 가져오기

중첩된 구성 요소가 있고 각 구성 요소가 자체 데이터를 가져오는 경우 해당 데이터 요청이 다르면 데이터 가져오기가 순차적으로 발생합니다(자동으로 메모 되므로 동일한 데이터에 대한 요청에는 적용되지 않습니다) .

예를 들어, Playlists 구성 요소는 Artist 구성 요소가 데이터 가져오기를 완료한 후에만 데이터 가져오기를 시작합니다. 왜냐하면 Playlists는 artistID prop에 의존하기 때문입니다.

app/aritist/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>
    </>
  )
}

 

이와 같은 경우 loading.js(경로 세그먼트의 경우) 또는 React<Suspense> (중첩 구성 요소의 경우)를 사용하여 React가 결과를 스트리밍하는 동안 즉각적인 로딩 상태를 표시할 수 있습니다.

이렇게 하면 데이터 가져오기로 인해 전체 경로가 차단되는 것을 방지할 수 있으며 사용자는 차단되지 않은 페이지 부분과 상호 작용할 수 있습니다.

 

데이터 요청 차단:

데이터 폭포를 방지하기 위한 또 다른 접근 방식은 애플리케이션의 루트에서 전역으로 데이터를 가져오는 것입니다. 그러나 이렇게 하면 데이터가로드가 완료될 때까지 그 아래에 있는 모든 라우트 세그먼트의 렌더링이 차단됩니다. 이를 "전부 아니면 아무것도 아님" 데이터 가져오기라고 할 수 있습니다. 페이지나 애플리케이션의 전체 데이터가 있어야 하거나 아무것도 없어야 합니다.

 

await가 있는 모든 fetch 요청은 Suspense 경계나 loading.js가 사용되지 않는 한 그 아래의 전체 트리의 렌더링과 데이터 가져오기를 차단합니다. 또 다른 대안은 병렬 데이터 가져오기 또는 preload 패턴을 사용하는 것입니다.

(개인정리) 데이터 폭포수(데이터가 위에서 아래로 순차적으로 흐르는) 를 방지하는 방법 중에 하나가 루트 경로의 페이지에서 데이터를 전역적으로 가져오는 것이 있다. 하지만, 이 방식을 사용하는 경우 루트 경로의 데이터의 로드가 완료될 때 까지 그 아래에 존재하는 모든 컴포넌트 또한 해당 데이터를 사용하지 못하고 기다려야 하는 문제가 발생할 수 있다. 즉, 하나가 안 되면 그 아래 전체가 작동하지 않는 것이다.

이 외에 또 다른 데이터 폭포를 방지하는 대안으로 병렬 데이터 가져오기 및 사전로드를 사용하는 것이다.

  병렬 데이터 가져오기

병렬로 데이터를 가져오려면 구성 요소에서 사용하는 요청을 외부에서 정의한 다음 구성 요소 내에서 호출할 수 있습니다. 이렇게 하면 두 요청을 병렬로 시작하여 시간을 절약할 수 있지만 사용자는 두 약속이 해결될 때까지 렌더링 결과를 볼 수 없습니다.

 

아래 예에서 getArtist 및 getArtistAlbums 함수는 Page 구성 요소 외부에서 정의되고 구성 요소 내에서 호출되며 두 약속이 모두 해결될 때까지 기다립니다.

// 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 }
}) {
  // 병렬로 두 요청을 시작합니다.
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // 약속이 해결될 때까지 기다립니다.
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}
(개인정리) 위 방식은 데이터를 가져오는 함수를 외부(전역)에 정의하고,  해당 함수를 await 과 promise.all 을 사용하여 병렬적으로 데이터를 가져오는 방식이다. 위 방식을 활용하면, 데이터를 병렬적으로 가져올 수는 있으나, 모든 데이터가 도착할 때 까지 대기상태에 놓여 있기 때문에, 하나의 요청 처리가 오래걸린다면 이후 로직을 실행하지 못하는 문제는 사라지지 않는다.

사용자 경험을 개선하려면 Suspense Boundary를 추가하여 렌더링 작업을 분할하고 가능한 한 빨리 결과의 일부를 표시할 수 있습니다.

(개인정리) 이 때 Suspense 경계를 추가한다면, 모든 렌더링 작업이 끝나기 전 일부 완료된 결과를 미리 표시할 수 있음으로 사용자 경험을 개선할 수 있다.

   preload 패턴을 사용한 데이터 폭포 방지

데이터 폭포를 방지하는 또 다른 방법은 preload 패턴을 사용하는 것입니다. 선택적으로 preload 함수를 만들어 병렬 데이터 가져오기를 더욱 최적화할 수 있습니다. 이 접근 방식을 사용하면 약속을 props로 전달할 필요가 없습니다. preload 함수의 이름은 패턴이므로 API가 아닙니다.

// components/Item.tsx
import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
  // void는 주어진 표현식을 평가하고 undefined를 반환합니다.
  // https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
// app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // 아이템 데이터 로딩 시작
  preload(id)
  // 다른 비동기 작업 수행
  const isAvailable = await checkIsAvailable()

  return isAvailable ?  : null
}

      React cache, server-only, 및 Preload 패턴 사용

cache 함수, preload 패턴, 및 server-only 패키지를 결합하여 애플리케이션 전체에서 사용할 수 있는 데이터 가져오기 유틸리티를 만들 수 있습니다.

// utils/get-item.ts

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})

 

이 접근 방식을 사용하면 데이터를 미리 가져오고, 응답을 캐시하고, 데이터 가져오기가 서버에서만 발생하도록 보장할 수 있습니다.

(개인정리) preload 를 사용하면, 미리 데이터를 로드하여 가져올 수 있다(즉, 빌드시에 데이터를 가져와서 빠르게 로드한다) 그리고, 리액트의 cache 를 사용하면 해당 요청에 대해 캐싱할 수 있으므로, 불필요한 데이터 중복 요청도 방지할 수 있다.

 

utils/get-item exports는 Layouts, Pages, 또는 다른 구성 요소에서 사용할 수 있습니다. 이렇게 하면 구성 요소가 아이템 데이터를 언제 가져올지 제어할 수 있습니다.

알아두면 좋은 점:
서버 데이터 가져오기 함수가 클라이언트에서 절대 사용되지 않도록 하려면 server-only 패키지를 사용하는 것이 좋습니다.

 

* server-only 패키지

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment

 

반응형