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