Skill/Next.js

[Next.js 14] 2. Data Fetching - 3

뜸부깅 2025. 2. 20. 12:46
반응형

3. Data Fetching Patterns and Best Practices

  • React와 Next.js에서 추천할 만한 Data Fetching 방법을 소개한다.
  • 아주 일반적인 패턴을 어떻게 사용하는 지 소개한다.

3-1. Fetching data on the Server

  • 가능하면, Server Componente와 함께 Server에서 Data fetching 하는 것을 추천한다.
    • database와 같이 backend resources에 직접 접근이 가능.
    • Client에 민감한 정보를 노출시키지 않아 보안 향상.
    • data fetch와 render를 Server에서 함께 수행하면, client와 server 사이의 이동이 줄어들기 때문에 성능 우수.
    • Client에서 요청마다 data fetch하는 대신 Server에서 한 번의 data fetch로 여러 데이터를 제공.
    • Client와 Server 사이의 waterfalls 감소.

3-2. Fetching data where it's needed

  • 만약, 같은 데이터를 여러 컴포넌트에서 사용할 필요가 있을 때, 전역적으로 data fetch를 하거나 컴포넌트 사이의 props로 이동하지말고 fetch나 React의 cache를 사용하면 성능 걱정없이 여러 요청에 같은 데이터를 제공할 수 있다.
  • fetch 요청은 자동적으로 memoization이 되기 때문에 가능.
좀 헷갈렸던 개념이 있는데, fetch가 자동 caching되는 것과 여러 컴포넌트에 걸친 fetch가 memoization된다는 점이다.
1. caching : 동일 요청이 발생하면 caching된 data를 반환.
2. memoization : fetch 요청 자체를 memoization해서 1번만 요청하게 함.

3-3. Streaming

  • Streaming과 Suspense는 UI를 점진적으로 렌더링되게 도와주는 기능.
  • Server Components와 중첩된 레이아웃과 사용하면, 특별히 데이터가 필요하지 않은 페이지는 즉시 렌더링하고 데이터가 필요하면 로딩 UI를 보여준다.
  • 사용자가 모든 페이지의 로드를 기다리지 않고 상호작용이 가능하게 함.
  • https://fbtmdwhd33.tistory.com/270?category=1142940
 

[Next.js 14] 1. Routing - 3

4. Loading UI and Streamingloading.js 파일은 React Suspense Component를 활용하여 Loading UI를 만들 수 있다. 즉각적으로 Loading UI를 보여주고(instant loading state), 로딩된 컨텐츠가 한 번의 렌더링 안에서 자동으로

fbtmdwhd33.tistory.com

3-4. Parallel and sequential data fetching

  • React 컴포넌트에서 data fetching을 할 때, 2가지 data fetching pattern이 있다.
  • Sequential data fetching
    • route 내의 요청들이 의존적인 경우 waterfall 현상이 발생한다.
    • 하나의 fetch 결과가 다른 요청에 의존적이거나, 요청 전의 리소스 절약 목적으로 사용할 수 있다.
    • 이 방식은 예측이 불가능할 수 있고, fetchin 시간이 오래 걸리는 단점이 있다.
  • Parallel data fetching
    • 동시에 데이터를 로드하고 client와 server 사이의 waterfall 현상과 전체 데이터 로드 시간을 줄일 수 있다는 장점이 있다.

1. Sequential data fetching

  • 만약, 중첩된 컴포넌트가 있고 각자 다른 API로 데이터를 fetch한다면 연속적으로 발생하게 된다.
// ...
 
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>
    </>
  )
}
  • Playlist를 조회하는 API는 artist ID에 의존적이므로, Artistlist를 조회한 뒤 fetch가 시작된다.
  • 이 경우, loading.js(route Segment) or Suspense(nested components)를 활용하여 로딩 상태를 보여주면서 Streaming 렌더링을 사용하여 waterfall 현상을 방지할 수 있다.
  • 사용자는 artist 정보는 먼저 확인할 수 있고, Playlist 컴포넌트는 data fetching이 완료될 때 까지 block된다.

2. Parallel Data Fetching

  • 병렬적으로 데이터를 fetch하기 위해서는 컴포넌트 외부에 data fetch를 정의하여 사용해야 한다.
  • 이는 병력적으로 실행되지만, 사용자는 fetch가 끝날 때 까지 렌더링된 화면을 볼 수 없다.
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>
    </>
  )
}
  • 마찬가지로, 사용자 경험을 고려하면 Suspense를 활용할 수 있다.

3-5 Preloading Data

  • waterfall 현상을 방지하기 위한 다른 방법은 preload pattern을 사용하는 것.
  • Parallel Data Fetching에 추가적인 최적화를 위한 preload 함수를 사용할 수 있다.
  • preload라는 함수는 API가 아니기 때문에 어떠한 이름으로 써도 문제 없다.
import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void evaluates the given expression and returns undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id) // 반환 값 무시.
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}


import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}
  • Page에서 정의되어 있는 preload 함수를 호출해 요청의 응답을 기다리지 않고 다른 비동기 작업을 병렬적으로 실행.
  • 내가 여기서 이해가 되지않은 점은 getItem이 preload에서 호출되었는데, 해당 결과물에 대한 처리가 없고 void가 사용된 것.
  • 그리고 Item 컴포넌트에서도 getItem 요청이 존재하는 것.
  • 이러면 결과적으로 getItem 요청이 2번 발생하지 않나? -> 최적화 방식이 아닌 거 같은데? 의문.
  • 최대한 이해한 바로는 getItem이 fetch 요청이라고 가정했을 때, 응답값이 caching 된다는 점을 착안.
  • preload를 통해 getItem의 응답은 Item 컴포넌트 렌더링 시점에 올 수도 안 올 수도 있다. (복잡한 게 아니라면 올 거다 웬만하면) -> checkIsAvaillable 비동기 요청 시작 및 응답 수신 -> true 인 경우 Item 컴포넌트가 렌더링 되면서 getItem 요청 실행 -> preload에서 실행한 응답값이 캐싱되어 있으면 추가 요청을 보내지 않고 사용 -> 만약, caching 응답값이 없으면 async/await으로 응답값을 기다림 -> Item 컴포넌트 렌더링. 

1. Using React cache, server-only and the Preload Pattern

  • preload pattern은 React의 cache 및 server-only 패키지와 조합하여 data fetching 유틸을 만들 수 있다.
import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})
  • server-only는 서버에서만 실행되는 것을 보장하여 Client 성능에 지장을 주지 않기 위함.
  • 이렇게 만든 유틸리티는 여러 페이지 및 레이아웃에 걸쳐 사용 가능.

3-6. Preventing sensitive data from being exposed to the client

  • 민감한 정보가 client에 노출되는 것을 막기 위해 React의 taint API에서 제공하는 taintObjectReference, taintUniqueValue 사용을 추천.
  • next.config에 experimental.taint option에 true를 지정하여 사용 가능.
module.exports = {
  experimental: {
    taint: true,
  },
}
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Do not pass the whole user object to the client',
    data
  )
  experimental_taintUniqueValue(
    "Do not pass the user's address to the client",
    data,
    data.address
  )
  return data
}


import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // this will cause an error because of taintObjectReference
      address={userData.address} // this will cause an error because of taintUniqueValue
    />
  )
}
  • 민감한 정보에 대한 경고 표시를 할 수 있다. 로직에는 영향 x

 

처음 뵙겠습니다.

Next.js 사용하면서 처음 본 개념이거나, 이해하기 어려운 부분(?)은 붉은색 볼드처리 해봤다.

 

 

Preference

 

Data Fetching: Data Fetching Patterns and Best Practices | Next.js

Learn about common data fetching patterns in React and Next.js.

nextjs.org