본문 바로가기

넥스트

[next.js] 11. Data Fetching, Caching, and Revalidating

반응형
next.js 공식 사이트 문서를 번역하여 정리한 내용입니다. 자세한 내용은 공식 사이트를 참고해주세요.
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

 

  데이터 가져오기, 캐싱 및 재유효화

데이터 가져오기는 모든 응용 프로그램의 핵심 부분입니다. 이 페이지에서는 React Next.js에서 데이터를 가져 오고, 캐시하고, 데이터를 재유효화하는 방법을 살펴봅니다.

 

데이터를 가져 오는 방법은 다음과 같이 네 가지가 있습니다.

- 서버에서 fetch를 사용하여

- 서버에서 서드파티 라이브러리를 사용하여

- 클라이언트에서 Route 핸들러를 통해

- 클라이언트에서 서드파티 라이브러리를 사용하여

서드파티 라이브러리(third-party library)란?
서드파티 라이브러리는 개발자의 직접 개발이 아닌 타사에서 개발하고 제공한 라이브러리를 의미.

  fetch를 사용하여 서버에서 데이터 가져오기

Next.js 서버에서 각 fetch 요청의 캐싱 및 재유효화 동작을 구성할 수 있게 확장된 기본 fetch API를 제공합니다. React는 React 컴포넌트 트리를 렌더링하는 동안 fetch 요청을 자동으로 메모이즈합니다.

메모이즈란?
프로그램이 특정 데이터나 값을 메모리에 저장하고, 이를 필요할 때마다 빠르게 참조할 수 있도록 하는 것

 

서버 구성 요소(서버 컴포넌트), 루트 핸들러 및 서버 액션에서 fetchasync/await를 사용할 수 있습니다.

 

예시)

app/page.tsx

// 비동기적으로 서버에서 데이터를 패치하여 들고 온다(주석 개인추가)
async function getData() {
const res = await fetch('https://api.example.com/...')

// 반환 값은 직렬화되지 않습니다.
// Date, Map, Set 등을 반환할 수 있습니다.
   if (!res.ok) {
// 이것은 가장 가까운 error.js 에러 바운더리를 활성화시킵니다.
     throw new Error('데이터를 가져오는 데 실패했습니다.')
   }
  return res.json()
}

// 페이지(주석 개인 추가)
export default async function Page() {
const data = await getData()
   return <main></main>
}

알아두어야 할 점:

- Next.jsServer Components에서 데이터를 가져 올 때 필요한 유용한 함수를 제공합니다. 이러한 함수는 요청 시간 정보에 의존하기 때문에 경로가 동적으로 렌더링되도록 할 것입니다.

- Route 핸들러에서 fetch 요청은 React 컴포넌트 트리의 일부가 아니기 때문에 메모이즈되지 않습니다.

- TypeScript를 사용하여 Server Component에서 async/await를 사용하려면 TypeScript 5.1.3 이상 및 @types/react 18.2.8 이상을 사용해야 합니다.


  데이터 캐싱

캐싱 데이터를 데이터 소스(원본 데이터)에서 매번 다시 가져 오지 않도록 데이터를 저장하는 것을 의미합니다.

(개인정리) 즉, 캐싱이란 변동없는 같은 데이터를 새롭게 계속 가져오는 것을 피하기 위해 메모리에 데이터를 저장해두고, 필요할 때 저장해둔 데이터를 재사용하기 위한 기법

 

기본적으로 Next.js는 서버의 데이터 캐시에 fetch의 반환 값들을 자동으로 캐시합니다. 이는 데이터를 빌드 시간이나 요청 시간에 가져 오고 캐시하며 각 데이터 요청에서 재사용됨을 의미합니다.

// 'force-cache'는 기본값이며 생략할 수 있습니다.
fetch('https://...', { cache: 'force-cache' })

 

POST 메서드를 사용하는 fetch 요청도 자동으로 캐시됩니다. POST 메서드를 사용하는 Route Handler 내부에 있는 경우를 제외하고는 캐시되지 않습니다.

루트 핸들러 관련 공식 문서 링크
https://nextjs.org/docs/app/building-your-application/routing/route-handlers

  데이터 캐시란 무엇인가요?

데이터 캐시는 지속적인(혹은 영구적인) HTTP 캐시입니다. 플랫폼에 따라 캐시는 자동으로 확장되고 여러 지역에서 공유될 수 있습니다.

데이터 캐시에 대해 자세히 알아보기.
https://nextjs.org/docs/app/building-your-application/caching#data-cache

  데이터 재유효화(Data Revalidate)

재유효화는 데이터 캐시를 지우고 최신 데이터를 다시 가져 오는 프로세스입니다. 이터가 변경되었을 때 최신 정보를 표시하려는 경우 유용합니다.

 

캐시된 데이터는 두 가지 방법으로 재유효화할 수 있습니다.

- 시간 기반 재유효화: 일정 시간이 경과한 후 자동으로 데이터를 재유효화합니다. 이는 데이터가 드물게 변경되고 신선도가 그다지 중요하지 않을 때 유용합니다.

- 요청 기반 재유효화: 이벤트(: 양식 제출)에 따라 데이터를 수동으로 재유효화할 수 있습니다. 요청 기반 재유효화는 태그 기반 또는 경로 기반 접근 방식을 사용하여 한 번에 데이터 그룹을 재유효화할 수 있습니다. 이는 최신 데이터가 가능한 빨리 표시되도록 하려는 경우 유용합니다(: 헤드리스 CMS의 콘텐츠가 업데이트될 때).

 

   시간 기반 재유효화

일정 시간 간격으로 데이터를 재유효화하려면 fetchnext.revalidate 옵션을 사용하여 리소스의 캐시 수명(초 단위)을 설정할 수 있습니다.

// 60분 마다 원본 소스에서 데이터를 가져온다.
fetch('https://...', { next: { revalidate: 3600 } })

 

또는 정적으로 렌더링된 경로의 모든 fetch 요청을 재유효화하려면 Segment Config 옵션을 사용할 수 있습니다.

// layout.js / page.js
export const revalidate = 3600 // 최대 1시간마다 재유효화 ==> 보통 컴포넌트의 상단에 위치시킴

 

여러 개의 fetch 요청이 정적으로 렌더링된 경로에 있고 각각 다른 재유효화 빈도를 가지고 있다면 가장 낮은 시간이 모든 요청에 대해 사용됩니다. 동적으로 렌더링된 경로의 경우 각 fetch 요청이 독립적으로 재유효화됩니다.

 

시간 기반 재유효화에 대해 자세히 알아보기.
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config

   요청 기반 재유효화

데이터는 Route Handler 또는 Server Action 내에서 경로(revalidatePath) 또는 캐시 태그(revalidateTag)를 기반으로 수동으로 재유효화할 수 있습니다.

 

Next.js는 경로를 통한 여러 경로 간의 fetch 요청을 무효화하기 위한 캐시 태그 시스템을 갖추고 있습니다.

 

fetch를 사용할 때 하나 이상의 태그와 함께 캐시 항목을 태그로 표시할 수 있습니다. 그런 다음 revalidateTag를 호출하여 해당 태그와 연관된 모든 항목을 재유효화할 수 있습니다.

 

예를 들어, 다음 fetch 요청은 캐시 태그 "collection"을 추가합니다

 

app/page.tsx

export default async function Page() {
const res = await fetch('https://...', { next: { tags: ['collection'] } })
const data = await res.json()
// ...
}

 

Route Handler를 사용하는 경우 Next.js 앱만 알고 있는 비밀 토큰을 만들어야 합니다. 이 비밀은 권한없는 재유효화 시도를 방지하는 데 사용됩니다. 예를 들어 다음과 같은 URL 구조로 라우트에 액세스할 수 있습니다(수동 또는 웹훅을 사용하여):

// URL
// https://<your-site.com>/api/revalidate?tag=collection&secret=<token>

// app/api/revalidate/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { revalidateTag } from 'next/cache'

// 예: your-website.com/api/revalidate?tag=collection&secret=<token>에 대한 웹훅
export async function POST(request: NextRequest) {
    const secret = request.nextUrl.searchParams.get('secret')
    const tag = request.nextUrl.searchParams.get('tag')

      if (secret !== process.env.MY_SECRET_TOKEN) {
         return NextResponse.json({ message: '유효하지 않은 비밀' }, { status: 401 })
      }

      if (!tag) {
         return NextResponse.json({ message: '태그 파라미터 누락' }, { status: 400 })
      }

    revalidateTag(tag)

  return NextResponse.json({ revalidated: true, now: Date.now() })  
}

 

또는 revalidatePath를 사용하여 해당 경로와 관련된 모든 데이터를 재유효화할 수 있습니다.

// app/api/revalidate/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'

export async function POST(request: NextRequest) {
     const path = request.nextUrl.searchParams.get('path')
   if (!path) {
       return NextResponse.json({ message: '경로 파라미터 누락' }, { status: 400 })
   }
   revalidatePath(path)
 return NextResponse.json({ revalidated: true, now: Date.now() })
}
요청 기반 재유효화에 대해 자세히 알아보기.
https://nextjs.org/docs/app/building-your-application/caching#on-demand-revalidation

 

 에러 처리 및 재유효화

데이터 재유효화 시도 중에 에러가 발생하면 마지막으로 성공적으로 생성된 데이터는 캐시에서 계속 제공됩니다. 다음 요청에서 Next.js는 데이터를 다시 유효성 검사하려고 시도할 것입니다.

 

   데이터 캐싱에서 제외하는 방법

fetch 요청은 다음과 같은 경우에 캐시되지 않습니다.

- fetch 요청에 cache: 'no-store'가 추가된 경우.

- 개별 fetch 요청에 revalidate: 0 옵션이 추가된 경우.

- POST 메서드를 사용하는 Router Handler 내부에 있는 경우.

- 헤더 또는 쿠키 사용 후에 fetch 요청이 오는 경우.

- const dynamic = 'force-dynamic' 라우트 세그먼트 옵션을 사용한 경우, 기본적으로 캐시를 건너 뛰도록 fetchCache 라우트 세그먼트 옵션이 구성된 경우.


  개별 fetch 요청

개별 fetch 요청의 캐싱을 건너 뛰려면 fetch에서 cache 옵션을 'no-store'로 설정할 수 있습니다. 이렇게 하면 모든 요청마다 데이터를 동적으로 가져옵니다.

// layout.js / page.js
fetch('https://...', { cache: 'no-store' })
fetch API 참조에서 사용 가능한 캐시 옵션을 모두 볼 수 있습니다.
https://nextjs.org/docs/app/api-reference/functions/fetch

 여러 개의 fetch 요청

경로 세그먼트(예: 레이아웃 또는 페이지)에 여러 개의 fetch 요청이 있는 경우 세그먼트의 모든 데이터 요청의 캐싱 동작을 구성할 수 있습니다. Segment Config 옵션을 사용하여 데이터 요청의 캐싱 동작을 구성할 수 있습니다.

 

예를 들어, const dynamic = 'force-dynamic'을 사용하면 모든 데이터를 요청 시간에 가져 오도록하고 세그먼트를 동적으로 렌더링하도록 설정됩니다.

// layout.js / page.js

// 추가
export const dynamic = 'force-dynamic'

 

세그먼트 구성 옵션의 상세한 목록은 경로 세그먼트의 정적 및 동적 동작을 정밀하게 제어할 수 있도록 해주며 더 자세한 내용은 API 참조에서 확인할 수 있습니다.


  서버에서 서드파티 라이브러리를 사용하여 데이터 가져오기

서드파티 라이브러리(: 데이터베이스, CMS 또는 ORM 클라이언트)를 사용하는 경우 해당 요청의 캐싱 및 재유효화 동작을 Route Segment Config 옵션React의 캐시 함수를 사용하여 구성할 수 있습니다.

 

데이터가 캐시되는지 여부는 세그먼트가 정적으로 또는 동적으로 렌더링되는지에 따라 다릅니다. 세그먼트가 정적(기본값)인 경우 요청의 출력이 라우트 세그먼트의 일부로 캐시되고 재유효화됩니다. 세그먼트가 동적인 경우 요청의 출력은 캐시되지 않으며 세그먼트가 렌더링될 때마다 매번 다시 가져옵니다.

 

알아두어야 할 점:
Next.js는 개별 서드파티 요청의 캐싱 및 재유효화 동작을 구성하는 데 사용할 수있는 unstable_cache API에 대해 작업 중입니다.

 예시

revalidate 옵션은 3600으로 설정되어 있으므로 데이터는 최대 1시간마다 캐시되고 재유효화됩니다.

React 캐시 함수를 사용하여 데이터 요청을 메모이즈합니다.

// utils/get-item.ts

import { cache } from 'react'

export const revalidate = 3600 // 최대 1시간마다 재유효화

// cache 로 감싸진 요청은 메모리에 저장되어, 여러번 호출되어도 한 번만 호출된다.
// 요청 시 마다 사용되는 데이터는 메모리에 캐시되어 있는 데이터이다.
export const getItem = cache(async (id: string) => {
    const item = await db.item.findUnique({ id }) //디비에서 해당 아이디를 가진 데이터를 가져온다.
      return item
})

getItem 함수가 두 번 호출되어도 데이터베이스에는 하나의 쿼리만 실행됩니다.

 

개인추가 정리
- next.js에서 revalidate 설정 값의 단위는 ms(밀리초)입니다. 예를 들어, revalidate: 1000은 리베일데이트가 1초마다 발생한다는 것을 의미합니다.

- revalidate 설정 값은 리베일데이트가 발생하는 주기를 제어합니다. 리베일데이트는 페이지가 렌더링될 때마다 데이터가 변경되었는지 확인하는 과정입니다. 리베일데이트 주기가 짧을수록 데이터가 변경되었는지 확인하는 횟수가 많아지므로, 페이지의 최신 상태가 유지될 가능성이 높아집니다. 하지만 리베일데이트 주기가 짧으면 성능에 영향을 줄 수 있습니다.
따라서 revalidate 설정 값은 페이지의 최신 상태를 유지하면서 성능을 최적화할 수 있도록 적절하게 설정하는 것이 중요합니다.

다음은 revalidate 설정 값을 설정하는 몇 가지 예입니다.

  • revalidate: 0은 리베일데이트를 비활성화합니다.
  • revalidate: 1000은 리베일데이트가 1초마다 발생합니다.
  • revalidate: 5000은 리베일데이트가 5초마다 발생합니다.
  • revalidate: 10000은 리베일데이트가 10초마다 발생합니다.
// app/item/layout.tsx ==> 레이아웃 컴포넌트
import { getItem } from '@/utils/get-item'
 
export default async function Layout({
  params: { id },
}: {
  params: { id: string }
}) {
  const item = await getItem(id)
  // ...
}

// app/item/[id]/page.tsx ==> 페이지 컴포넌트
import { getItem } from '@/utils/get-item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  const item = await getItem(id)
  // ...
}
(개인정리) 위 예제는 레이아웃 컴포넌트에서 getItem 함수를 import 하여 호출하는 예시와 페이지 컴포넌트에서 getItem 함수를 import 하여 사용하는 예시

  Routes Handler 를 사용하여  클라이언트 컴포넌트에서 데이터 가져 오기

클라이언트 컴포넌트에서 데이터를 가져와야하는 경우 클라이언트에서 루트 핸들러를 호출할 수 있습니다. 루트 핸들러는 서버에서 실행되고 데이터를 클라이언트로 반환합니다. 이것은 API 토큰과 같은 민감한 정보를 클라이언트에 노출시키지 않아야 할 때 유용합니다.

 

예제를 보려면 Route Handler 문서를 참조하십시오.


  서버 컴포넌트 및 루트 핸들러

서버 컴포넌트는 서버에서 렌더링되므로 데이터를 가져 오려면 클라이언트에서 루트 핸들러를 호출할 필요가 없습니다. 대신 서버 컴포넌트 내에서 데이터를 직접 가져올 수 있습니다.


  서드파티 라이브러리를 사용하여 클라이언트에서 데이터 가져 오기

SWR 또는 React Query와 같은 서드파티 라이브러리를 사용하여 클라이언트에서 데이터를 가져올 수도 있습니다. 이러한 라이브러리는 요청을 메모이즈하고 캐싱, 재유효화 및 데이터 변이를 처리하기 위한 고유한 API를 제공합니다.

 

향후 API:
use는 함수에서 반환한 프로미스를 받아들이고 처리하는 React 함수입니다. fetch use로 래핑하는 것은 현재 클라이언트 컴포넌트에서 권장되지 않으며 여러 번 다시 렌더링 될 수 있습니다. React RFC에서 use에 대해 자세히 알아보세요.
https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md#usepromise

 

 

 

 

 

반응형