12P by xguru 5일전 | ★ favorite | 댓글 2개
  • Next.js의 미래 방향이 흥미로움
  • Server Actions에 대한 이슈가 있었지만 React 19의 useOptimistic, useFormStatus로 개선 가능성이 보임
    • Remix의 useFetcher 방식도 좋은 DX를 제공함
  • Next.js의 PPR(Partial Pre-rendering)과 새로운 granular 캐시 시스템이 특히 돋보임
  • 전반적으로 매우 긍정적인 인상을 받음

The Big Picture

  • next.config.js에서 새로운 캐시 시스템을 실험적으로 활성화할 수 있음
  • 캐시 프로필을 정의해 다양한 만료 시간과 재검증 주기를 설정할 수 있음
// next.config.js  
const config = {  
  experimental: {  
    // 새로운 캐싱 시스템 활성화. 이제 코드에서 `use cache` 사용 가능   
    dynamicIO: true,   
    // 선택사항: 캐시 프로파일 설정   
    cacheLife: {  
      blog: {  
        stale: 3600, // 클라이언트 캐시 유지: 1시간  
        revalidate: 900, // 서버에서 새로고침: 15분  
        expire: 86400, // 최대 수명: 1일  
      },  
    },  
  },  
};  

use cache 기본 사용법

  • 파일, 컴포넌트, 함수 수준에서 "use cache" 선언을 통해 캐싱 가능함
  • 코드 예시에서 use cache를 추가해 쉽게 캐시를 적용할 수 있음
  • cacheTag, revalidateTag 등을 활용해 원하는 시점에 캐시 무효화가 가능함
// 1. 파일 단위 캐싱  
"use cache";  
export default function Page() {  
  return <div>Cached Page</div>;  
}  
  
// 2. 컴포넌트 단위 캐싱  
export async function PriceDisplay() {  
  "use cache";  
  const price = await fetchPrice();  
  return <div>${price}</div>;  
}  
  
// 3. 함수 단위 캐싱  
export async function getData() {  
  "use cache";  
  return await db.query();  
}  

태그 기반 캐싱

import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache';  
  
// 특정 데이터 그룹 캐싱  
export async function ProductList() {  
  'use cache';  
  cacheTag('products');  
  const products = await fetchProducts();  
  return <div>{products}</div>;  
}  
  
// 데이터 변경 시 캐시 무효화  
export async function addProduct() {  
  'use server';  
  await db.products.add(...);  
  revalidateTag('products');  
}  

사용자 정의 Cache 프로필

  • unstable_cacheLife를 사용해 next.config.js에서 정의한 캐시 프로필을 불러올 수 있음
  • 코드 내부에서 선언된 프로필명(예: "blog")을 사용해 캐시 정책을 적용함
import { unstable_cacheLife as cacheLife } from "next/cache";  
  
export async function BlogPosts() {  
  "use cache";  
  cacheLife("blog"); // 미리 정의한 블로그 캐시 프로필 사용  
  return await fetchPosts();  
}  

중요하지만 간과할 수 있는 사항

캐시 키 자동 생성

  • 컴포넌트의 propsarguments가 자동으로 캐시 키에 포함됨
  • 직렬화 불가능한 값(함수 등)은 "수정 불가능한 참조" 형태로 처리됨
export async function UserCard({ id, onDelete }) {  
  "use cache";  
  // id는 캐시 키에 포함  
  // onDelete는 전달되지만 캐싱에는 영향을 주지 않음  
  const user = await fetchUser(id);  
  return <div onClick={onDelete}>{user.name}</div>;  
}  

동적 콘텐츠와 캐시 콘텐츠의 혼합

  • 캐시된 콘텐츠 내부에 동적 콘텐츠를 자식으로 전달해 혼합해 사용할 수 있음
  • cacheTag 배열을 지정해 여러 태그를 동시에 적용하고 무효화할 수 있음
export async function CachedWrapper({ children }) {  
  "use cache";  
  const header = await fetchHeader();  
  return (  
    <div>  
      <h1>{header}</h1>  
      {children} {/* 동적 콘텐츠는 그대로 유지 */}  
    </div>  
  );  
}  
export async function ProductPage({ id }) {  
  "use cache";  
  cacheTag(["products", `product-${id}`, "featured"]);  
  // 이 태그들 중 어떤 것을 사용해도 무효화 가능  
}  

캐싱 계층 구조

  • 최상위 레벨에서 "use cache"를 선언하면 해당 영역 전체가 캐시됨
  • 특정 부분(예: Suspense를 사용한 동적 섹션)은 캐싱 영역에서 제외할 수 있음
"use cache";  
export default async function Page() {  
  return (  
    <div>  
      <CachedHeader />  
      <div>  
        <Suspense fallback={<Loading />}>  
          <DynamicFeed /> {/* 동적 콘텐츠 */}  
        </Suspense>  
      </div>  
    </div>  
  );  
}  

타입 안전성

  • 캐시 키와 캐시 프로필 등 문자열을 상수로 관리해 매직 스트링 사용을 줄일 수 있음
  • React Query의 패턴처럼 태그를 생성해주는 방식을 사용하면 편리함
// 상수로 캐시 프로필 키를 관리  
export const CACHE_LIFE_KEYS = {  
  blog: "blog",  
} as const;  
  
const config = {  
  experimental: {  
    cacheLife: {  
      [CACHE_LIFE_KEYS.blog]: {  
        stale: 3600,  
        revalidate: 900,  
        expire: 86400,  
      },  
    },  
  },  
};  

캐싱 태그를 효율적으로 관리하는 방법

  • React Query 스타일의 태그 팩토리 패턴 적용
export const CACHE_TAGS = {  
  blog: {  
    all: ["blog"] as const,  
    list: () => [...CACHE_TAGS.blog.all, "list"] as const,  
    post: (id: string) => [...CACHE_TAGS.blog.all, "post", id] as const,  
    comments: (postId: string) =>  
      [...CACHE_TAGS.blog.all, "post", postId, "comments"] as const,  
  },  
} as const;  
  
// 캐싱 태그 설정  
function tagCache(tags: string[]) {  
  cacheTag(...tags);  
}  
  
// 사용 예제  
export async function BlogList() {  
  "use cache";  
  tagCache(CACHE_TAGS.blog.list());  
}  

캐시가 왜 필요한지 이해 불가. 고전적 방식에서 캐시따윈 필요 없었고, 캐시를 사용 해야할 상황이 극히 제한적인데, 쓸데없이 캐시가지고 목숨거는데?

next.js는 13 이후로 참쓸만해졌는데 최근엔 정말정말 맘에 듭니다. 풀스택 웹개발 기술 스택의 사실상 표준이 될 것 같습니다.