API/Directives/use cache

김동현·2026년 3월 8일

next.js 공식문서 번역

목록 보기
63/79

use cache 디렉티브를 사용하면 특정 라우트(route), React 컴포넌트, 또는 일반 함수를 '캐시 가능(cacheable)'하게 지정할 수 있어요. 파일의 맨 위에 작성해서 해당 파일에서 export 되는 모든 것들을 캐시 하겠다고 선언할 수도 있고, 함수나 컴포넌트 내부의 맨 위에 인라인으로 작성해서 그 반환값만 캐시 하도록 설정할 수도 있답니다.

💡 알아두면 좋은 점:

  • 쿠키(cookies)나 헤더(headers)를 사용해야 한다면, 캐시되는 스코프 바깥에서 그 값들을 읽은 다음 인자(argument)로 전달하는 방식을 사용하세요. 이게 공식적으로 권장되는 패턴이랍니다.
  • 런타임 데이터를 처리할 때 기본 메모리 캐시만으로 부족하다면, 'use cache: remote'를 사용해 보세요. 플랫폼에서 제공하는 전용 캐시 핸들러를 연결할 수 있습니다. (다만 캐시를 확인하기 위해 네트워크 왕복이 필요하고 보통 플랫폼 이용 비용이 발생한다는 점은 기억해 두세요!)
  • 규정 준수(compliance) 문제나, 런타임 데이터를 인자로 넘기도록 코드를 리팩토링하기 어려운 상황이라면 'use cache: private' 문서를 참고해 보세요.

Usage (사용법)

use cache는 'Cache Components'라는 기능의 일부예요. 이 기능을 활성화하려면 먼저 next.config.ts (또는 .js) 파일에 cacheComponents 옵션을 추가해야 합니다.

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheComponents: true,
}

module.exports = nextConfig

설정을 마쳤다면, 이제 파일, 컴포넌트, 또는 함수 레벨에 use cache를 추가해 볼까요?

// File level (파일 레벨)
'use cache'

export default async function Page() {
  // ...
}

// Component level (컴포넌트 레벨)
export async function MyComponent() {
  'use cache'
  return <></>
}

// Function level (함수 레벨)
export async function getData() {
  'use cache'
  const data = await fetch('/api/data')
  return data
}

💡 알아두면 좋은 점: 파일 레벨에서 use cache를 사용할 때는, 해당 파일에서 export 하는 모든 함수가 반드시 async 비동기 함수여야만 합니다.


How use cache works (use cache는 어떻게 동작할까요?)

Cache keys (캐시 키)

캐시 항목의 '키(key)'는 입력값들을 직렬화(serialized)한 버전을 사용해서 생성되는데요, 여기에는 다음과 같은 4가지 요소가 포함됩니다.

  1. Build ID (빌드 ID) - 빌드할 때마다 고유하게 생성돼요. 즉, 새로 빌드하면 기존 캐시는 모두 무효화(invalidate) 된다는 뜻이죠.
  2. Function ID (함수 ID) - 코드베이스 내에서 해당 함수가 위치한 곳과 시그니처를 안전하게 해시(hash) 처리한 값이에요.
  3. Serializable arguments (직렬화 가능한 인자) - 컴포넌트의 props나 일반 함수의 인자들이 여기에 해당해요.
  4. HMR refresh hash (개발 환경 전용) - 개발 중 코드를 수정해서 핫 모듈 리플레이스먼트(HMR)가 일어날 때 캐시를 초기화하기 위한 해시값입니다.

👨‍🏫 강사의 추가 설명:
여기서 '직렬화(Serialization)'라는 단어가 조금 낯설 수 있어요. 쉽게 말해, 메모리에 떠 있는 자바스크립트 객체나 값들을 저장하거나 네트워크로 전송하기 위해 '문자열 형태' 등으로 변환하는 과정을 말해요. 캐시 키를 만들려면 이런 변환 과정이 필수적이랍니다. 기술 면접에서 "직렬화가 무엇인가요?"라는 질문을 받을 수 있으니 이 개념을 꼭 숙지해 두세요!

캐시된 함수가 바깥 스코프(outer scopes)에 있는 변수를 참조할 경우, Next.js는 똑똑하게도 그 변수들을 자동으로 캡처해서 인자처럼 묶어버립니다. 결국 그 외부 변수들도 캐시 키의 일부가 되는 거죠.

async function Component({ userId }: { userId: string }) {
  const getData = async (filter: string) => {
    'use cache'
    // 캐시 키에는 클로저에서 가져온 userId와 인자로 받은 filter가 모두 포함됩니다.
    return fetch(`/api/users/${userId}/data?filter=${filter}`)
  }

  return getData('active')
}

위의 코드를 보면 getData 함수 안에서 바깥쪽의 userId를 참조하고 있고, filter는 인자로 받고 있죠? 이 두 값이 모두 getData 함수의 캐시 키를 만드는 데 사용돼요. 즉, userIdfilter의 조합이 달라지면 완전히 별개의 캐시 항목이 생성된다는 의미입니다.


Serialization (직렬화)

캐시되는 함수의 인자(arguments)와 반환값(return values)은 반드시 직렬화가 가능(serializable)해야 합니다.

전체적인 참고 자료는 아래 링크를 확인해 보세요:

💡 알아두면 좋은 점: 인자와 반환값은 서로 다른 직렬화 시스템을 사용해요. 인자에 적용되는 Server Component 직렬화 규칙이, 반환값에 적용되는 Client Component 규칙보다 훨씬 엄격하답니다. 쉽게 말해, JSX 엘리먼트를 반환값으로 내보내는 건 가능하지만, Pass-through(그대로 통과시키는) 패턴을 쓰지 않는 이상 인자로 JSX를 받아올 수는 없어요.

Supported types (지원하는 타입)

Arguments (인자):

  • 원시 타입 (Primitives): string, number, boolean, null, undefined
  • 순수 객체 (Plain objects): { key: value }
  • 배열 (Arrays): [1, 2, 3]
  • Dates, Maps, Sets, TypedArrays, ArrayBuffers
  • React elements (오직 pass-through 목적으로만 가능)

Return values (반환값):

  • 인자와 동일하며, 추가로 JSX elements 도 지원합니다.

Unsupported types (지원하지 않는 타입)

  • 클래스 인스턴스 (Class instances)
  • 함수 (Functions) - pass-through 목적 제외
  • Symbols, WeakMaps, WeakSets
  • URL 인스턴스
// Valid (유효함) - 원시 타입과 순수 객체 사용
async function UserCard({
  id,
  config,
}: {
  id: string
  config: { theme: string }
}) {
  'use cache'
  return <div>{id}</div>
}

// Invalid (유효하지 않음) - 클래스 인스턴스 사용
async function UserProfile({ user }: { user: UserClass }) {
  'use cache'
  // 에러 발생: 클래스 인스턴스는 직렬화할 수 없습니다.
  return <div>{user.name}</div>
}

Pass-through (non-serializable arguments) (그대로 통과시키기 - 직렬화 불가능한 인자)

직렬화가 불가능한 값이라도, 그 값을 함수 내부에서 들여다보거나 조작(introspect)하지만 않는다면 인자로 받을 수 있습니다. 이 방식을 통해 children이나 Server Actions를 활용한 컴포지션(composition) 패턴을 구현할 수 있어요.

async function CachedWrapper({ children }: { children: ReactNode }) {
  'use cache'
  // children을 읽거나 수정하지 말고, 그냥 그대로 렌더링에 넘겨주세요 (pass-through)
  return (
    <div className="wrapper">
      <header>Cached Header</header>
      {children}
    </div>
  )
}

// 사용 예시: children으로 동적인 컴포넌트가 들어갈 수 있습니다.
export default function Page() {
  return (
    <CachedWrapper>
      <DynamicComponent /> {/* 이 부분은 캐시되지 않고 그대로 통과됩니다 */}
    </CachedWrapper>
  )
}

마찬가지로 Server Actions도 캐시된 컴포넌트를 그냥 통과하게 만들 수 있습니다.

async function CachedForm({ action }: { action: () => Promise<void> }) {
  'use cache'
  // 여기서 action 함수를 호출하지 마세요 - 그냥 form에 그대로 전달만 하세요
  return <form action={action}>{/* ... */}</form>
}

Constraints (제약 사항)

캐시되는 함수들은 완전히 격리된 환경(isolated environment)에서 실행됩니다. 캐시가 항상 예측 가능하고 안전하게 동작하도록 보장하기 위해 다음과 같은 제약 사항들이 존재합니다.

Runtime APIs (런타임 API)

캐시되는 함수나 컴포넌트 내부에서는 cookies(), headers(), 또는 searchParams 같은 런타임 API에 직접 접근할 수 없습니다. 대신, 캐시되는 스코프 바깥에서 미리 이런 값들을 읽어온 다음, 캐시 함수에 인자로 넘겨주어야 해요.

Runtime caching considerations (런타임 캐싱 시 고려사항)

use cache는 주로 정적인 껍데기(static shell) 안에 동적인 데이터를 포함시키기 위해 설계되었지만, LRU(Least Recently Used) 방식의 메모리 저장소를 이용해 런타임에 데이터를 캐싱하는 역할도 합니다.

이 런타임 캐시가 어떻게 동작하는지는 여러분이 애플리케이션을 어디에 배포하느냐(호스팅 환경)에 따라 달라집니다.

환경 (Environment)런타임 캐싱 동작 방식 (Runtime Caching Behavior)
Serverless (서버리스)보통 요청(request) 간에 캐시 항목이 유지되지 않아요 (각 요청이 완전히 다른 인스턴스에서 처리될 수 있거든요). 단, 빌드 타임에 만들어진 캐시는 정상적으로 작동합니다.
Self-hosted (직접 호스팅)서버가 계속 떠있기 때문에 요청 간에 캐시 항목이 유지됩니다. next.config.jscacheMaxMemorySize 옵션을 통해 캐시 크기를 조절할 수 있어요.

👨‍🏫 강사의 추가 설명:
포트폴리오 프로젝트를 Vercel 같은 서버리스 환경에 배포하실 계획이라면 이 차이를 명확히 이해하고 계셔야 합니다! 내 로컬(직접 호스팅 환경)에서는 런타임 캐시가 잘 유지되는데, 배포했더니 매번 새롭게 요청을 받아오는 것처럼 보인다면 바로 이 서버리스 환경의 특징 때문입니다.

기본 인메모리 캐시만으로 부족하다면, 플랫폼에서 제공하는 Redis나 KV 데이터베이스 같은 전용 캐시 핸들러를 연결할 수 있는 use cache: remote 사용을 고려해 보세요. 트래픽을 감당하기 어려운 백엔드 데이터 소스로 향하는 요청을 크게 줄여줍니다. (물론 저장소 비용이나 네트워크 지연, 플랫폼 수수료 등의 트레이드오프는 존재합니다.)

아주 드물게, 보안 규정을 준수해야 하거나 런타임 데이터를 인자로 넘기는 방식으로 코드를 도저히 리팩토링할 수 없는 상황이라면 use cache: private이 필요할 수도 있습니다.

React.cache isolation (React.cache의 격리)

React에서 제공하는 React.cacheuse cache 경계선 안쪽에서 완전히 격리된 채로 동작합니다. 즉, use cache가 선언된 함수 바깥에서 React.cache로 저장한 값은, use cache 함수 안에서는 보이지 않아요.

다시 말해, React.cache를 이용해서 use cache 스코프 안으로 데이터를 밀어 넣을 수 없다는 뜻입니다.

import { cache } from 'react'

const store = cache(() => ({ current: null as string | null }))

function Parent() {
  const shared = store()
  shared.current = 'value from parent'
  return <Child />
}

async function Child() {
  'use cache'
  const shared = store()
  // 여기서 shared.current는 'value from parent'가 아니라 null입니다!
  // use cache는 그 자신만의 완전히 독립된 React.cache 스코프를 가지기 때문이에요.
  return <div>{shared.current}</div>
}

👨‍🏫 강사의 팁:
"React.cache와 Next.js의 use cache(또는 fetch 캐시)의 차이점이 뭔가요?" 역시 면접 단골 질문입니다!
React.cache"하나의 사용자 요청(Request) 생명주기 내에서만" 데이터를 캐싱(메모이제이션)합니다. 반면, use cache"여러 사용자의 요청 간에, 그리고 빌드 전반에 걸쳐" 캐시가 영구적으로 유지될 수 있도록 설계된 기능이에요. 이 두 개가 격리된다는 건 너무나 당연하고도 중요한 설계 원칙이죠. 데이터를 넘기고 싶다면, 반드시 함수의 인자(props)를 사용하세요!


use cache at runtime (런타임에서의 use cache)

서버(Server) 측면에서: 캐시 항목들은 서버의 메모리에 저장되며, cacheLife 설정에서 정의한 revalidate (재검증) 및 expire (만료) 시간을 엄격하게 따릅니다. next.config.js 파일에서 cacheHandlers를 구성하면 이 캐시 저장소를 원하는 대로 커스터마이징할 수 있어요.

클라이언트(Client) 측면에서: 서버 캐시에서 가져온 콘텐츠는 stale 시간 동안 브라우저의 메모리에 저장됩니다. 여기서 주의할 점은, 여러분이 어떻게 설정하든 간에 클라이언트 라우터는 최소 30초의 stale 시간을 강제로 적용한다는 거예요.

서버에서 클라이언트로 캐시 수명을 전달할 때는 x-nextjs-stale-time이라는 응답 헤더를 사용해서, 서버와 클라이언트 양쪽의 캐시 동작이 조화롭게 이루어지도록 합니다.


Revalidation (캐시 재검증)

기본적으로 use cache는 다음과 같은 설정이 적용된 default 프로필을 사용합니다.

  • stale (클라이언트 사이드 유지 시간): 5분
  • revalidate (서버 사이드 재검증 시간): 15분
  • expire (만료 시간): 시간 경과로 인한 만료 없음 (Never expires by time)
async function getData() {
  'use cache'
  // 암묵적으로 default 프로필을 사용하게 됩니다.
  return fetch('/api/data')
}

Customizing cache lifetime (캐시 수명 커스터마이징하기)

cacheLife 함수를 사용하면 캐시 지속 시간을 여러분의 입맛에 맞게 변경할 수 있습니다.

import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife('hours') // 내장된 'hours' 프로필을 사용합니다.
  return fetch('/api/data')
}

On-demand revalidation (수동으로 즉시 재검증하기)

특정 이벤트가 발생했을 때 즉시 캐시를 무효화하고 싶다면 cacheTag, updateTag, 또는 revalidateTag 함수를 사용하세요.

import { cacheTag } from 'next/cache'

async function getProducts() {
  'use cache'
  cacheTag('products') // 이 함수 결과물에 'products'라는 태그를 붙입니다.
  return fetch('/api/products')
}
'use server'

import { updateTag } from 'next/cache'

export async function updateProduct() {
  await db.products.update(...)
  updateTag('products') // 'products' 태그가 붙은 모든 캐시를 무효화합니다!
}

cacheLifecacheTag는 모두 클라이언트와 서버의 캐싱 레이어 전반에 걸쳐 통합적으로 작동해요. 즉, 코드 한 곳에서 캐시 규칙을 정의해 두면 애플리케이션 전체에 알아서 적용된다는 뜻이죠. 아주 편리하죠?


Examples (사용 예시)

Caching an entire route with use cache (라우트 전체를 캐시하기)

특정 라우트 전체를 사전 렌더링(pre-render) 하고 싶다면, layout 파일과 page 파일 양쪽 모두의 최상단에 use cache를 추가하세요. 이 두 영역은 애플리케이션 내에서 각기 다른 진입점(entry points)으로 취급되기 때문에, 서로 독립적으로 캐시가 생성됩니다.

'use cache'

export default async function Layout({ children }: { children: ReactNode }) {
  return <div>{children}</div>
}
'use cache'

export default async function Layout({ children }) {
  return <div>{children}</div>
}

그리고 page 파일 내부에서 임포트되어 중첩되는 모든 컴포넌트들은 해당 page에 연결된 캐시 결과물의 일부로 포함됩니다.

'use cache'

async function Users() {
  const users = await fetch('/api/users')
  // 유저 목록을 순회(loop)합니다
}

export default async function Page() {
  return (
    <main>
      <Users />
    </main>
  )
}
'use cache'

async function Users() {
  const users = await fetch('/api/users')
  // 유저 목록을 순회(loop)합니다
}

export default async function Page() {
  return (
    <main>
      <Users />
    </main>
  )
}

💡 알아두면 좋은 점:

  • 만약 use cachelayout이나 page 중 한 곳에만 추가한다면, 해당 라우트 세그먼트와 그 안에 임포트 된 컴포넌트들만 부분적으로 캐시 됩니다.

Caching a component's output with use cache (컴포넌트의 렌더링 결과물 캐시하기)

컴포넌트 내부에서 발생하는 fetch 요청이나 복잡한 계산 로직을 캐시하고 싶다면, 컴포넌트 레벨에서 use cache를 사용할 수 있습니다. 직렬화된 props의 값이 똑같다면, 이전에 만들어둔 캐시 결과물을 계속 재사용하게 됩니다.

export async function Bookings({ type = 'haircut' }: BookingsProps) {
  'use cache'
  async function getBookingsData() {
    const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`)
    return data
  }
  return //...
}

interface BookingsProps {
  type: string
}
export async function Bookings({ type = 'haircut' }) {
  'use cache'
  async function getBookingsData() {
    const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`)
    return data
  }
  return //...
}

Caching function output with use cache (일반 함수의 반환값 캐시하기)

use cache는 꼭 컴포넌트나 라우트에만 쓸 수 있는 건 아니에요. 어떤 비동기 함수(asynchronous function)에도 추가할 수 있습니다. 네트워크 요청을 캐시 하거나, 데이터베이스 쿼리 결과, 혹은 처리 속도가 느린 무거운 연산 결과를 캐시 할 때 아주 유용하겠죠.

export async function getData() {
  'use cache'

  const data = await fetch('/api/data')
  return data
}
export async function getData() {
  'use cache'

  const data = await fetch('/api/data')
  return data
}

Interleaving (인터리빙 - 캐시된 영역과 안 된 영역 교차시키기)

React에서 children이나 슬롯(slots)을 활용한 컴포지션은 유연한 컴포넌트를 만들기 위한 아주 유명하고 훌륭한 패턴이죠. use cache를 사용할 때도 평소처럼 UI를 조합해 나갈 수 있습니다. 반환되는 JSX 안에서 children이나 다른 슬롯으로 포함된 요소들은 캐시 된 컴포넌트의 캐시 항목에 아무런 영향을 주지 않고 그대로 쏙 통과(pass-through)하게 됩니다.

단, 캐시 되는 함수 내부에서 그 JSX 슬롯들을 직접 참조하거나 조작하지만 않는다면요! 렌더링 결과물 안에 단순히 자리만 잡고 있는 것은 캐시에 전혀 문제를 일으키지 않아요.

export default async function Page() {
  const uncachedData = await getData()
  return (
    // 컴포지션을 위한 슬롯들을 props로 전달합니다 (예: header, children)
    <CacheComponent header={<h1>Home</h1>}>
      {/* DynamicComponent가 children 슬롯으로 제공됩니다 */}
      <DynamicComponent data={uncachedData} />
    </CacheComponent>
  )
}

async function CacheComponent({
  header, // header: 하나의 슬롯으로, prop처럼 주입됩니다
  children, // children: 중첩 컴포지션을 위한 또 다른 슬롯입니다
}: {
  header: ReactNode
  children: ReactNode
}) {
  'use cache'
  const cachedData = await fetch('/api/cached-data')
  return (
    <div>
      {header}
      <PrerenderedComponent data={cachedData} />
      {children}
    </div>
  )
}
export default async function Page() {
  const uncachedData = await getData()
  return (
    // 컴포지션을 위한 슬롯들을 props로 전달합니다 (예: header, children)
    <CacheComponent header={<h1>Home</h1>}>
      {/* DynamicComponent가 children 슬롯으로 제공됩니다 */}
      <DynamicComponent data={uncachedData} />
    </CacheComponent>
  )
}

async function CacheComponent({
  header, // header: 하나의 슬롯으로, prop처럼 주입됩니다
  children, // children: 중첩 컴포지션을 위한 또 다른 슬롯입니다
}) {
  'use cache'
  const cachedData = await fetch('/api/cached-data')
  return (
    <div>
      {header}
      <PrerenderedComponent data={cachedData} />
      {children}
    </div>
  )
}

뿐만 아니라, 캐시 영역 안에서 함수를 직접 호출하지만 않는다면, 캐시 되는 컴포넌트를 거쳐서 Client Components 쪽으로 Server Actions를 부드럽게 넘겨줄 수도 있어요.

import ClientComponent from './ClientComponent'

export default async function Page() {
  const performUpdate = async () => {
    'use server'
    // 서버 측 업데이트 작업을 수행합니다
    await db.update(...)
  }

  return <CachedComponent performUpdate={performUpdate} />
}

async function CachedComponent({
  performUpdate,
}: {
  performUpdate: () => Promise<void>
}) {
  'use cache'
  // 여기서 performUpdate를 절대 호출하지 마세요!
  return <ClientComponent action={performUpdate} />
}
import ClientComponent from './ClientComponent'

export default async function Page() {
  const performUpdate = async () => {
    'use server'
    // 서버 측 업데이트 작업을 수행합니다
    await db.update(...)
  }

  return <CachedComponent performUpdate={performUpdate} />
}

async function CachedComponent({ performUpdate }) {
  'use cache'
  // 여기서 performUpdate를 절대 호출하지 마세요!
  return <ClientComponent action={performUpdate} />
}
'use client'

export default function ClientComponent({
  action,
}: {
  action: () => Promise<void>
}) {
  return <button onClick={action}>Update</button>
}
'use client'

export default function ClientComponent({ action }) {
  return <button onClick={action}>Update</button>
}

Troubleshooting (문제 해결하기)

Debugging cache behavior (캐시 동작 디버깅하기)

Verbose logging (상세 로그 켜기)

캐시와 관련된 상세한 로그를 확인하고 싶다면 환경 변수 NEXT_PRIVATE_DEBUG_CACHE=1을 설정해 주세요.

NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
# 프로덕션 환경에서는 아래처럼 실행합니다
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start

💡 알아두면 좋은 점: 이 환경 변수를 켜면 ISR(Incremental Static Regeneration)이나 다른 캐싱 메커니즘의 로그도 함께 출력됩니다. 자세한 내용은 올바른 프로덕션 동작 확인하기(Verifying correct production behavior) 문서를 참고해 보세요.

Console log replays (콘솔 로그 리플레이)

개발 환경에서 캐시 된 함수 내부에 console.log를 찍어두면, 캐시를 반환할 때 로그 앞에 Cache라는 접두사가 붙어서 출력됩니다. 캐시 히트 여부를 확인하기 편하겠죠?

Build Hangs (Cache Timeout) (빌드 멈춤 현상 - 캐시 타임아웃)

가끔 빌드가 중간에 멈춰버리는 현상(Build hangs)이 발생할 수 있는데요. 이는 use cache 경계선 밖에서 만들어진 '동적인 데이터'나 '런타임 데이터'로 결정되는(resolve) 비동기 Promise에 접근하려고 할 때 발생합니다. 캐시 되는 함수 입장에서는 빌드 중에는 도저히 결과값을 알 수 없는 데이터를 한없이 기다리게 되고, 결국 50초가 지나면 타임아웃 에러를 발생시키는 거죠.

빌드 타임아웃이 나면 터미널에서 이런 에러 메시지를 보시게 될 거예요:

Error: Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".
(에러: 사전 렌더링 중 캐시를 채우는 데 시간이 초과되었습니다. "use cache" 내부에서 params, searchParams, cookies() 또는 동적 데이터와 같은 요청 전용 인수를 사용했기 때문일 가능성이 높습니다.)

이런 상황은 주로 다음 세 가지 경우에 흔히 발생합니다.
1. 이런 Promise 자체를 컴포넌트의 props로 넘겼을 때
2. 클로저(closure)를 통해 접근할 때
3. Map 같은 공유 저장소에서 꺼내올 때

💡 알아두면 좋은 점: 참고로 use cache 안에서 cookies()headers()직접 호출하면 타임아웃이 아니라 즉시 별도의 명확한 에러(different error)를 뿜어냅니다. 타임아웃은 'Promise를 기다릴 때' 발생하는 거예요.

런타임 데이터 Promise를 props로 넘길 때의 문제점:

import { cookies } from 'next/headers'
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dynamic />
    </Suspense>
  )
}

async function Dynamic() {
  const cookieStore = cookies()
  return <Cached promise={cookieStore} /> // 여기서 빌드가 멈춥니다! (Build hangs)
}

async function Cached({ promise }: { promise: Promise<unknown> }) {
  'use cache'
  const data = await promise // 빌드 중인데 런타임 데이터가 올 때까지 하염없이 기다립니다...
  return <p>..</p>
}

이럴 때는 Dynamic 컴포넌트 안에서 cookies 객체를 완전히 await 해서 풀어낸 다음, 진짜 필요한 쿠키의 '값(value)'만 Cached 컴포넌트로 넘겨주는 방식을 사용해야 합니다.

데이터 공유를 위한 Map 저장소를 쓸 때의 문제점:

// 문제점: Map이 동적인 Promise를 저장하고 있고, 캐시된 코드가 이걸 가져다 쓰려고 합니다.
import { Suspense } from 'react'

const cache = new Map<string, Promise<string>>()

export default function Page() {
  return (
    <>
      <Suspense fallback={<div>Loading...</div>}>
        <Dynamic id="data" />
      </Suspense>
      <Cached id="data" />
    </>
  )
}

async function Dynamic({ id }: { id: string }) {
  // 동적인 Promise를 전역 Map 저장소에 세팅합니다
  cache.set(
    id,
    fetch(`https://api.example.com/${id}`).then((r) => r.text())
  )
  return <p>Dynamic</p>
}

async function Cached({ id }: { id: string }) {
  'use cache'
  return <p>{await cache.get(id)}</p> // 여기서 빌드 멈춤! 동적인 Promise를 꺼내와서 기다리기 때문이죠.
}

이런 문제를 피하려면 Next.js에 내장된 fetch() 중복 제거(deduplication) 기능을 그대로 활용하거나, 캐시 되는 컨텍스트와 안 되는 컨텍스트를 위한 Map 저장소를 아예 분리해서 사용하는 것이 좋습니다.

👨‍🏫 강사의 추가 설명:
포트폴리오를 만들 때 데이터를 전역 상태나 서버 모듈에 임시로 저장해두는 패턴을 많이 쓰시는데요, use cache를 도입할 때는 위와 같이 Promise 처리에 굉장히 주의해야 합니다. 디버깅이 까다로운 부분이니 눈여겨봐 두세요!


Platform Support (플랫폼 지원 현황)

배포 환경에 따라 use cache 기능 지원 여부가 달라집니다.

Deployment Option (배포 옵션)Supported (지원 여부)
Node.js server (Node.js 서버)Yes (지원함)
Docker container (도커 컨테이너)Yes (지원함)
Static export (정적 파일 추출)No (지원 안 함)
Adapters (어댑터)Platform-specific (플랫폼에 따라 다름)

Next.js를 직접 호스팅 할 때 캐시를 구성하는 방법은 캐싱 및 ISR 설정하기(configure caching) 문서를 참고해 보세요.


Version History (버전 업데이트 내역)

Version (버전)Changes (변경 사항)
v16.0.0"use cache" 기능이 'Cache Components' 피처와 함께 정식 활성화되었습니다.
v15.0.0"use cache" 기능이 실험적(experimental) 피처로 처음 소개되었습니다.

캐싱과 관련된 다른 API 레퍼런스들도 함께 살펴보시면 이해가 훨씬 깊어질 거예요.

  • use cache: private
    • 런타임 요청 API에 접근하는 함수를 캐싱할 때 "use cache: private" 디렉티브를 사용하는 방법을 배울 수 있습니다.
  • cacheComponents
    • Next.js에서 cacheComponents 플래그를 활성화하는 방법을 알아봅니다.
  • cacheLife
    • Next.js의 cacheLife 환경 설정 방법을 알아봅니다.
  • cacheHandlers
    • use cache 디렉티브를 위한 커스텀 캐시 핸들러를 구성하는 방법을 확인해 보세요.
  • cacheTag
    • cacheTag 함수를 이용해 애플리케이션의 캐시 무효화(invalidation)를 관리하는 방법을 배웁니다.
  • cacheLife (Function)
    • 캐시된 함수나 컴포넌트의 캐시 만료 시간을 명시적으로 설정하기 위한 cacheLife 함수 사용법을 알아봅니다.
  • revalidateTag
    • 특정 태그의 캐시를 즉시 재검증하는 revalidateTag 함수의 API 레퍼런스입니다.

전체 문서의 맥락을 살펴보고 싶다면 사이트맵(/docs/sitemap.md)을 확인해 주세요.
사용 가능한 모든 문서의 인덱스 목록은 LLM용 인덱스(/docs/llms.txt)에서 볼 수 있습니다.

혹시 공부하시다가 더 궁금한 점이 있거나 다른 파트의 번역이 필요하시면 언제든 말씀해 주세요! 👨‍🏫

profile
프론트에_가까운_풀스택_개발자

0개의 댓글