반응형
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