전 Next.js 의 미래가 마음에 들어요 : 정말 멋있어지고 있어요

3 weeks ago 3

  • 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(); }

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

캐시 키 자동 생성

  • 컴포넌트의 props와 arguments가 자동으로 캐시 키에 포함됨
  • 직렬화 불가능한 값(함수 등)은 "수정 불가능한 참조" 형태로 처리됨
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()); }

Read Entire Article