ReactNextCentral

데이터 가져오기, 캐싱 및 재검증

Published on
Next.js 애플리케이션에서 데이터를 가져오고 캐시하고 재검증하는 방법을 알아보세요.
Table of Contents

데이터 가져오기는 모든 애플리케이션의 핵심 부분입니다. 이 페이지에서는 리액트와 Next.js에서 데이터를 어떻게 가져오고 캐시하고 재검증할 수 있는지 설명합니다.

데이터를 가져올 수 있는 네 가지 방법이 있습니다.

  1. 서버에서 fetch를 사용하여
  2. 서버에서 서드파티 라이브러리를 사용하여
  3. 클라이언트에서 라우트 핸들러를 통해
  4. 클라이언트에서 서드파티 라이브러리를 사용하여.

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

Next.js는 서버에서 각 fetch 요청에 대한 캐싱재검증 동작을 구성할 수 있도록 기본 fetch 웹 API를 확장합니다. 리액트는 리액트 컴포넌트 트리를 렌더링하는 동안 fetch 요청을 자동으로 메모이즈합니다.

async/await와 함께 서버 컴포넌트에서 fetch를 사용, 라우트 핸들러서버 액션에서 fetch를 사용할 수 있습니다.

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>
}
JavaScript
app/page.js
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.js는 cookiesheaders와 같이 서버 컴포넌트에서 데이터를 가져올 때 필요할 수 있는 유용한 함수를 제공합니다. 이들은 요청 시간 정보에 의존하기 때문에 라우트가 동적으로 렌더링됩니다.
  • 라우트 핸들러에서는 라우트 핸들러가 리액트 컴포넌트 트리의 일부가 아니므로 fetch 요청이 메모이즈되지 않습니다.
  • TypeScript와 함께 서버 컴포넌트에서 async/await를 사용하려면 TypeScript 5.1.3 이상 및 @types/react 18.2.8 이상이 필요합니다.

데이터 캐싱

캐싱은 모든 요청에 대해 데이터 소스에서 다시 가져오지 않아도 되도록 데이터를 저장합니다. 기본적으로 Next.js는 서버의 데이터 캐시fetch의 반환 값을 자동으로 캐시합니다. 이는 데이터를 빌드 시간 또는 요청 시간에 가져와 캐싱하고 각 데이터 요청에 재사용할 수 있음을 의미합니다.

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

POST 방법을 사용하는 fetch 요청도 자동으로 캐시됩니다. 그러나 라우트 핸들러 내부에서 POST 방법을 사용하는 경우 캐시되지 않습니다.

데이터 캐시란?

데이터 캐시는 지속적인 HTTP 캐시입니다. 플랫폼에 따라 캐시는 자동으로 확장되며 여러 지역간에 공유될 수 있습니다. 데이터 캐시에 대한 자세한 내용을 알아보세요.

데이터 재검증

재검증은 데이터 캐시를 제거하고 최신 데이터를 다시 가져오는 과정입니다. 데이터가 변경될 때 최신 정보를 표시하려면 유용합니다.

캐시된 데이터는 두 가지 방법으로 재검증될 수 있습니다.

  1. 시간 기반 재검증: 일정 시간이 경과한 후 데이터를 자동으로 재검증합니다. 데이터가 자주 변경되지 않고 최신성이 그렇게 중요하지 않은 경우 유용합니다.
  2. 요청 기반 재검증: 이벤트(예: 폼 제출)를 기반으로 데이터를 수동으로 재검증합니다. 요청 기반 재검증은 한 번에 데이터 그룹을 재검증하기 위해 태그 기반 또는 경로 기반 접근법을 사용할 수 있습니다. Headless CMS의 내용이 업데이트될 때처럼 최신 데이터가 최대한 빨리 표시되도록 하려면 유용합니다.

시간 기반 재검증

타임 인터벌로 데이터를 재검증하려면 fetchnext.revalidate 옵션을 사용하여 리소스의 캐시 수명(초 단위)을 설정할 수 있습니다.

fetch('https://...', { next: { revalidate: 3600 } })

또는 라우트 세그먼트의 모든 fetch 요청을 재검증하려면 세그먼트 구성 옵션을 사용할 수 있습니다.

layout.js
export const revalidate = 3600 // 최대 1시간마다 재검증

정적으로 렌더링된 라우트에 여러 개의 fetch 요청이 있고 각 요청에 다른 재검증 빈도가 있는 경우 모든 요청에 가장 낮은 시간이 사용됩니다. 동적으로 렌더링된 라우트의 경우 각 fetch 요청은 독립적으로 재검증됩니다.

시간 기반 재검증에 대한 자세한 내용을 알아보세요.

요청 기반 재검증

라우트 핸들러나 서버 액션 내에서 경로(revalidatePath) 또는 캐시 태그(revalidateTag)를 기반으로 데이터를 요청에 따라 재검증할 수 있습니다.

Next.js에는 라우트 간의 fetch 요청을 무효화하기 위한 캐시 태깅 시스템이 있습니다.

  1. fetch를 사용할 때 캐시 항목을 하나 이상의 태그로 태그 지정할 수 있는 옵션이 있습니다.
  2. 그런 다음, 해당 태그와 연관된 모든 항목을 재검증하기 위해 revalidateTag를 호출할 수 있습니다.

예를 들어, 아래 fetch 요청은 collection 캐시 태그를 추가합니다.

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

라우트 핸들러를 사용하는 경우 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: 'Invalid secret' }, { status: 401 }
    )
  }

  if (!tag) {
    return NextResponse.json(
      { message: 'Missing tag param' }, { 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: 'Missing path param' }, 
      { status: 400 }
    )
  }

  revalidatePath(path)

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

요청 기반 재검증에 대한 자세한 내용을 알아보세요.

오류 처리 및 재검증

데이터를 재검증하려고 시도하는 동안 오류가 발생하면 마지막으로 성공적으로 생성된 데이터는 계속해서 캐시에서 제공됩니다. 다음 요청에서 Next.js는 데이터를 다시 재검증하려고 시도합니다.

데이터 캐싱 선택 해제하기

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

  • fetch 요청에 cache: 'no-store'가 추가된 경우.
  • 개별 fetch 요청에 revalidate: 0 옵션이 추가된 경우.
  • POST 메소드를 사용하는 라우터 핸들러 내의 fetch 요청인 경우.
  • headerscookies의 사용 이후의 fetch 요청인 경우.
  • const dynamic = 'force-dynamic' 라우트 세그먼트 옵션을 사용한 경우.
  • fetchCache 라우트 세그먼트 옵션이 기본적으로 캐시를 건너뛰도록 설정된 경우.
  • fetch 요청이 Authorization 또는 Cookie 헤더를 사용하고 컴포넌트 트리에서 그 위에 캐시되지 않은 요청이 있는 경우.

개별 fetch 요청

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

layout.js
fetch('https://...', { cache: 'no-store' })

fetch API 참조에서 사용 가능한 모든 cache 옵션을 확인하세요.

여러 fetch 요청

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

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

layout.js
// 추가
export const dynamic = 'force-dynamic'

세그먼트 설정 옵션의 광범위한 목록이 있으며 라우트 세그먼트의 정적 및 동적 동작을 세밀하게 제어할 수 있습니다. 자세한 내용은 API 참조를 참조하세요.


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

fetch를 지원하거나 노출하지 않는 서드 파티 라이브러리(예: 데이터베이스, CMS, ORM 클라이언트)를 사용하는 경우, 라우트 세그먼트 설정 옵션 및 리액트의 cache 함수를 사용하여 해당 요청의 캐싱 및 재검증 동작을 구성할 수 있습니다.

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

Next.js는 개별 서드파티 요청의 캐싱 및 재검증 동작을 구성하기 위한 API, unstable_cache,를 개발 중입니다.

예제

utils/get-item.ts
import { cache } from 'react'

export const revalidate = 3600 // 데이터를 최대 매시간마다 재검증합니다.

export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})
JavaScript
"utils/get-item.js"
import { cache } from 'react'

export const revalidate = 3600 // 데이터를 최대 매시간마다 재검증합니다.

export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id })
  return item
})
  • revalidate 옵션은 3600으로 설정되어 있습니다. 이는 데이터가 캐시되고 최대 매시간마다 재검증됨을 의미합니다.
  • 리액트의 cache 함수는 데이터 요청을 메모이제이션하기 위해 사용됩니다.

아래 getItem 함수가 두 번 호출되더라도 데이터베이스에는 한 번의 쿼리만 수행됩니다.

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)
  // ...
}
JavaScript
app/item/layout.js
import { getItem } from '@/utils/get-item'

export default async function Layout({ params: { id } }) {
  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)
  // ...
}
JavaScript
app/item/[id]/page.js
import { getItem } from '@/utils/get-item'

export default async function Page({ params: { id } }) {
  const item = await getItem(id)
  // ...
}

클라이언트에서 라우트 핸들러로 데이터 가져오기

클라이언트 컴포넌트에서 데이터를 가져올 필요가 있다면 클라이언트에서 라우트 핸들러를 호출할 수 있습니다. 라우트 핸들러는 서버에서 실행되어 클라이언트에 데이터를 반환합니다. 이는 API 토큰과 같은 민감한 정보를 클라이언트에 노출하지 않고자 할 때 유용합니다.

예제는 라우트 핸들러 문서에서 확인하세요.

서버 컴포넌트와 라우트 핸들러

서버 컴포넌트는 서버에서 렌더링되기 때문에 데이터를 가져오기 위해 서버 컴포넌트에서 라우트 핸들러를 호출할 필요가 없습니다. 대신 서버 컴포넌트 내부에서 데이터를 직접 가져올 수 있습니다.


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

SWR 또는 리액트 Query와 같은 서드 파티 라이브러리를 사용하여 클라이언트에서 데이터를 가져올 수도 있습니다. 이 라이브러리들은 요청 메모이제이션, 캐싱, 재검증 및 데이터 변경에 대한 자체 API를 제공합니다.

미래의 API:

use는 함수가 반환하는 promise를 수락하고 처리하는 리액트 함수입니다. 클라이언트 컴포넌트에서 fetchuse로 감싸는 것은 현재 권장되지 않으며 여러 번의 리렌더링을 유발할 수 있습니다. 리액트 RFC에서 use에 대해 자세히 알아보세요.