내 상황을 클로드가 정리해줌
# Next.js revalidatePath: 'page' vs 'layout' 타입의 차이와 함정
## 상황
블로그에 새 포스트를 작성한 후 `revalidatePath`로 캐시를 무효화했지만, 포스트 목록에 새 글이 나타나지 않는 문제가 발생했습니다.
**환경:**
- Next.js 15 (App Router)
- `generateStaticParams`를 사용한 정적 페이지 생성
- ISR(Incremental Static Regeneration) 환경
**코드 구조:**
```typescript
// app/[page]/page.tsx
export const generateStaticParams = async () => {
const totalCount = await getTotalPostCount();
const totalPage = Math.ceil(totalCount / POST_COUNT_PER_PAGE);
return Array.from({ length: totalPage }, (_, idx) => ({
page: String(idx + 1),
}));
};
export default async function HomePage({ params }: HomePageProps) {
const { page: stringifyPage } = await params;
const totalPageCount = await getTotalPostCount().then((r) =>
Math.ceil(r / POST_COUNT_PER_PAGE)
);
// ...
}
초기 무효화 코드:
const revalidatePathAfterUploadPost = ({ categoryName, postId, method }) => {
revalidatePath('/[page]', 'page');
if (categoryName) {
revalidatePath(`/category/${categoryName}/[page]`, 'page');
}
if (method === 'PATCH') {
revalidatePath(`/post/${postId}`, 'page');
}
};
내가 생각한 동작
revalidatePath('/[page]', 'page')를 호출하면:
- /1, /2, /3 등 모든 페이지 경로가 무효화됨
- 페이지가 다시 렌더링되면서 getTotalPostCount()가 재실행됨
- 새로운 포스트 개수가 반영되어 목록에 표시됨
예상 흐름:
포스트 생성
→ revalidatePath('/[page]', 'page')
→ 페이지 재생성
→ getTotalPostCount() 재실행
→ 새 포스트 표시 ✅
실제 동작
실제로는 새 포스트가 목록에 나타나지 않았고, 빌드 시점의 포스트 목록만 계속 보였습니다.
문제 원인: type: 'page'는 Full Route Cache(페이지 HTML)만 무효화하고, Data Cache(데이터 fetching 결과)는 무효화하지 않았기 때문입니다.
실제 흐름:
포스트 생성
→ revalidatePath('/[page]', 'page')
→ Full Route Cache 무효화 (페이지 HTML)
→ 페이지 재생성 시도
→ getTotalPostCount()는 캐시된 결과 사용 (10개)
→ 실제로는 11개인데 10개로 표시 ❌
Next.js 캐싱 계층 구조
┌──────────────────────────────────────┐
│ Router Cache (클라이언트 측) │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ Full Route Cache (서버) │ ← revalidatePath('page') 영향
│ - 렌더링된 HTML │
│ - RSC Payload │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ Data Cache (서버) │ ← revalidatePath('layout') 영향
│ - getTotalPostCount() 결과 │
│ - fetch() 결과 │
│ - DB 쿼리 결과 │
└──────────────────────────────────────┘
'page' vs 'layout' 타입 비교
타입 Full Route Cache Data Cache 사용 케이스
실제 동작 비교
page만 무효화:
revalidatePath('/[page]', 'page');
// 캐시 상태:
Cache Layers:
┌─────────────────────────┐
│ Data Cache (유지됨) │
│ getTotalPostCount = 10 │ ← 캐시된 값 그대로!
└─────────────────────────┘
↓
┌─────────────────────────┐
│ Route Cache (무효화) 🔄 │
│ /[page] HTML 재생성 │ ← HTML만 새로 만듦
└─────────────────────────┘
결과: HTML은 새로 만들어지지만,
캐시된 데이터(10개)를 사용
✅ layout 무효화:
revalidatePath('/', 'layout');
// 캐시 상태:
Cache Layers:
┌─────────────────────────┐
│ Data Cache (무효화) 🔄 │
│ getTotalPostCount 재실행 │ ← 새로 계산!
│ = 11 (업데이트됨) │
└─────────────────────────┘
↓
┌─────────────────────────┐
│ Route Cache (무효화) 🔄 │
│ /[page] HTML 재생성 │ ← HTML도 새로 만듦
└─────────────────────────┘
결과: 데이터부터 새로 가져와서
최신 값(11개)으로 페이지 생성
결론
해결 방법
const revalidatePathAfterUploadPost = ({ categoryName, postId, method }) => {
// ✅ layout 타입으로 Data Cache까지 무효화
revalidatePath('/', 'layout');
if (categoryName) {
revalidatePath(`/category/${categoryName}`, 'layout');
}
if (method === 'PATCH' && postId) {
revalidatePath(`/post/${postId}`, 'page');
}
};
핵심 교훈
- 데이터가 변경되면 반드시 type: 'layout' 사용
- 새 포스트 추가, 수정, 삭제
- 데이터베이스 변경이 발생하는 모든 경우
- type: 'page'는 UI만 변경될 때 사용
- 스타일 변경
- 레이아웃 조정
- 데이터는 그대로인 경우
- 경로 계층 구조 이해 필요
- revalidatePath('/', 'layout')는: ✅ /, /[page], /1, /2 무효화 ❌ /category/tech 무효화 안함 (독립적인 route segment) → 각 route segment는 명시적으로 무효화 필요!
- type: 'layout'이 하위 페이지를 포함하므로 중복 호출 불필요
- // ❌ 불필요한 중복 revalidatePath('/[page]', 'page'); revalidatePath('/', 'layout'); // ✅ layout만으로 충분 revalidatePath('/', 'layout'); // /[page]도 자동 포함됨
참고: Next.js 공식 문서
type (optional) 'page' or 'layout' string to change the type of path to revalidate.
- If path contains a dynamic segment, this parameter is required.
- Use a specific URL when you want to refresh a single page.
- Use a route pattern plus type to refresh multiple URLs.
문서에는 명확히 나와있지 않지만, 'layout'은 Data Cache까지 무효화하는 반면, 'page'는 Full Route Cache만 무효화한다는 점이 가장 중요한 차이점입니다.
'FrontEnd > Next.js' 카테고리의 다른 글
| app router 기준 렌더링 로직을 비교해보기 (0) | 2025.12.02 |
|---|---|
| Next.js로 SSE 환경 만들기 (app router에서 better-sse 사용 불가 이유) (0) | 2025.11.23 |
| Next.js 로 블로그 개발하면서 겪었던 트러블 슈팅 모음 (0) | 2025.10.01 |
| 아이템 리스트 페이지의 서버 컴포넌트를 최적화 시켜보기(fetch 캐싱 옵션) (0) | 2025.09.19 |
| 서버 컴포넌트에서 fetch와 캐싱 동작: SSR, SSG, ISR 관점에서 정리 (0) | 2025.09.13 |