본문 바로가기

Study/NextJS 공식문서

[4주 차] Patterns and Best Practices

서버에서 데이터 가져오기

Next.js에서는 가능한 한 데이터를 서버에서 패칭하는 것을 권장한다. 서버에서 데이터를 패칭하면 API 키, 인증 정보가 클라이언트에 노출되지 않으며, 클라이언트에서 요청을 보내는 시간을 절약할 수 있다.  
// app/page.tsx (Server Component)
export default async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()

  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

 

데이터 캐싱 및 중복 요청 방지

fetch 요청은 자동으로 캐싱(memoization)된다. 즉, 같은 데이터를 여러 컴포넌트에서 요청하더라도 한 번만 요청하고, 재사용한다. 이를 활용하면 중복된 API 요청을 방지할 수 있다.
// app/components/User.tsx
export default async function User() {
  const res = await fetch('https://api.example.com/user')
  const user = await res.json()

  return <p>{user.name}</p>
}

// app/page.tsx
import User from './components/User'

export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <User />
      <User />
    </>
  )
}

 

Streaming and Suspense

Streaming과 Suspense를 사용하면, 데이터를 기다리지 않고 바로 UI를 표시한 후, 데이터를 가져오는 부분만 따로 로딩할 수 있다. 이를 활용하면 페이지가 블로킹되지 않고, 사용자가 빠르게 콘텐츠를 볼 수 있다.

// app/artist/[username]/page.tsx
import { Suspense } from 'react'

async function Playlists({ artistID }: { artistID: string }) {
  const res = await fetch(`https://api.example.com/artist/${artistID}/playlists`)
  const playlists = await res.json()

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({ params: { username } }: { params: { username: string } }) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  const artist = await res.json()

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading Playlists...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

 

순차 데이터 패칭

하나의 데이터 요청이 끝나야 다음 요청이 진행된다. 불필요한 waterfall이 발생하여 속도가 느려질 수 있다. 한 데이터 패치가 다른 패치의 결과에 따라 달라지거나 다음 패치 전에 조건이 충족되어 리소스를 절약하려는 경우 이 패턴을 원하는 경우가 있을 수 있다.

이런 경우 loading.ts 또는 Suspense를 사용하여 React가 결과를 스트리밍하는 동안 즉시 로딩 상태를 표시할 수 있다.
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>
    </>
  )
}

 

병렬 데이터 패칭

Promise.all()을 사용하면 모든 요청을 동시에 실행하여 성능을 최적화할 수 있다. 두 개의 API 요청을 동시에 실행하여 속도를 최적화한다.
export default async function Page({ params: { userId } }: { params: { userId: string } }) {
  const userPromise = fetch(`https://api.example.com/user/${userId}`).then((res) => res.json())
  const postsPromise = fetch(`https://api.example.com/posts?userId=${userId}`).then((res) => res.json())

  const [user, posts] = await Promise.all([userPromise, postsPromise]) // 병렬 요청

  return (
    <>
      <h1>{user.name}</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </>
  )
}

 

데이터 Preloading 패턴

페이지가 렌더링되기 전에 미리 데이터를 가져와 최적화할 수 있다. 데이터가 필요할 때 이미 캐싱되어 있기 때문에 빠르게 제공 가능하다. 데이터가 변경되지 않고 빌드 타임에 데이터를 미리 패칭하는 SSG, ISR은 필요하지 않다.
// utils/getItem.ts
import { cache } from 'react'
import 'server-only'

// Preloading 함수
export const preload = (id: string) => {
  void getItem(id) // 미리 데이터를 가져와서 캐시에 저장
}

// SSR을 적용한 데이터 요청 함수
export const getItem = cache(async (id: string) => {
  const res = await fetch(`https://api.example.com/item/${id}`, { cache: 'no-store' })
  return res.json()
})
// app/item/[id]/page.tsx (SSR 적용)
import { preload, getItem } from '@/utils/getItem'
import ItemDetails from '@/components/ItemDetails'
import ItemReviews from '@/components/ItemReviews'

export default async function Page({ params: { id } }: { params: { id: string } }) {
  preload(id) // 미리 데이터를 가져와서 캐시에 저장 (동일한 요청 방지)

  return (
    <>
      <ItemDetails id={id} />
      <ItemReviews id={id} />
    </>
  )
}

// app/components/ItemDetails.tsx
import { getItem } from '@/utils/getItem'

export default async function ItemDetails({ id }: { id: string }) {
  const item = await getItem(id) // 이미 캐싱된 데이터 사용
  return <div>{item.name}</div>
}

// app/components/ItemReviews.tsx
import { getItem } from '@/utils/getItem'

export default async function ItemReviews({ id }: { id: string }) {
  const item = await getItem(id) // 이미 캐싱된 데이터 사용
  return <div>Reviews for {item.name}</div>
}