캐싱(Caching)

캐싱(Caching)

캐싱은 데이터 가져오기(fetching)나 다른 계산 결과를 저장해서, 같은 데이터를 요청할 때 다시 작업하지 않고 더 빠르게 응답해 주는 기술이에요. Next.js는 cacheComponents 옵션과 use cache 지시문을 중심으로 캐싱 모델을 제공해요. 이 글은 그 캐싱 모델의 기본 사용법을 정리할게요.

출처: https://nextjs.org/docs/app/getting-started/caching

Cache Components 활성화

Next config 파일에 cacheComponents 옵션을 추가해서 활성화해요.

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Cache Components를 쓰면 GET Route Handler도 페이지와 같은 프리렌더링 모델을 따라요. 이 모델을 안 쓰는 경우는 "Caching (Previous Model)" 가이드를 참고하면 돼요.

use cache 지시문 사용법

use cache 지시문은 async 함수와 컴포넌트의 반환값을 캐시해요. 두 레벨로 적용할 수 있어요.

  • 데이터 레벨(Data-level) — 데이터를 가져오거나 계산하는 함수를 캐시해요 (예: getProducts(), getUser(id)).
  • UI 레벨(UI-level) — 컴포넌트나 페이지 전체를 캐시해요 (예: async function BlogPosts()).

캐시 지시문은 결과에 수명(lifetime)을 부여하고, Next.js는 그 정보로 렌더링 최적화를 적용해요. 캐시된 결과는 정적 셸(static shell)의 일부가 되어 프리페치에 포함될 수 있어요. 모든 캐시 지시문에는 cacheLife를 함께 쓰는 걸 권장해요. 없으면 암시적 default 프로파일이 적용돼요.

인자와 부모 스코프에서 캡처한 값은 자동으로 캐시 키의 일부가 되어, 서로 다른 입력은 서로 다른 캐시 항목으로 분리돼요.

데이터 레벨 캐싱

데이터를 가져오는 async 함수를 캐시하려면 함수 본문 맨 위에 use cache 지시문을 추가해요.

import { cacheLife } from 'next/cache'

export async function getUsers() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM users')
}

같은 데이터를 여러 컴포넌트에서 쓰거나, 데이터를 UI와 독립적으로 캐시하고 싶을 때 유용해요.

UI 레벨 캐싱

컴포넌트·페이지·레이아웃 전체를 캐시하려면 컴포넌트 본문 맨 위에 지시문을 추가해요.

import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')

  const users = await db.query('SELECT * FROM users')

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

파일 맨 위에 "use cache"를 적으면 파일에 export된 모든 함수가 캐시돼요.

캐시하지 않는 데이터 스트리밍

매 요청마다 새 데이터가 필요한 API·DB·async 작업을 가져오는 컴포넌트에는 "use cache"를 쓰지 말아요. 대신 컴포넌트를 <Suspense>로 감싸고 fallback UI를 제공해요. fallback은 프리렌더된 셸과 함께 보내지고, async 작업은 요청 시점에 실행돼요.

import { Suspense } from 'react'

async function LatestPosts() {
  const data = await fetch('https://api.example.com/posts')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default function Page() {
  return (
    <>
      <h1>My Blog</h1>
      <Suspense fallback={<p>Loading posts...</p>}>
        <LatestPosts />
      </Suspense>
    </>
  )
}

<Suspense> 경계가 없으면 dev 오버레이가 blocking-route 인사이트를 표시해요.

런타임 API 다루기

cookies, headers, searchParams, params 같은 런타임 API는 사용자가 요청해야만 알 수 있는 정보가 필요해요. 이를 접근하는 컴포넌트는 <Suspense>로 감싸는 게 좋아요. 런타임 값이 필요한 캐시 함수에는 그 값을 추출해서 캐시 함수 인자로 전달하면, 그 값이 캐시 키의 일부가 돼요.

라우트가 프리렌더링에서 동적 파라미터를 모를 때 generateStaticParams로 빌드 타임에 특정 값을 프리렌더링하거나, ISR로 App Shell을 제공하면서 나머지는 백그라운드에서 해결할 수 있어요.

정적·캐시·스트리밍 콘텐츠

한 페이지에 정적 콘텐츠(자동 프리렌더), 캐시된 동적 콘텐츠(use cache로 정적 셸에 포함), 런타임 동적 콘텐츠(<Suspense>로 스트리밍)를 모두 섞을 수 있어요. 프리렌더링 중 정적 부분과 캐시된 부분은 정적 셸의 일부가 되고, 쿠키에 의존하는 UI는 요청 시점에 스트리밍돼요.

캐시가 저장되는 곳

캐시된 함수의 출력은 RSC 페이로드로 직렬화돼요. Next.js는 이를 HTML로 렌더링하고, 서버 또는 원격 저장소에 두거나 브라우저로 보내요. cacheLife가 각 복사본이 얼마나 신선하게 유지될지를 정해요.

  • 프리렌더된 HTML — 빌드 시 정적 셸로 저장되고, revalidateexpire가 재빌드 시점을 조절해요.
  • 공유 저장소 — 기본은 인스턴스별 인메모리 저장소예요. use cache: remote로 인스턴스 간 공유되는 내구성 있는 cache handler로 옮길 수 있어요.
  • 브라우저 — 클라이언트 내비게이션·프리페치용 RSC에 포함돼요.

모든 저장소는 단일 배포에 스코프돼요. 새 배포는 새로 시작하고, 캐시 키에 빌드 ID가 포함되므로 이전 캐시 항목은 이월되지 않아요.

더 알아보기