본문 바로가기
FrontEnd/Next.js

revalidatePath로 캐시 무효화 시 마주칠 수 있는 상황

by 위그든씨 2025. 10. 2.

내 상황을 클로드가 정리해줌

# 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');
    }
};

핵심 교훈

  1. 데이터가 변경되면 반드시 type: 'layout' 사용
    • 새 포스트 추가, 수정, 삭제
    • 데이터베이스 변경이 발생하는 모든 경우
  2. type: 'page'는 UI만 변경될 때 사용
    • 스타일 변경
    • 레이아웃 조정
    • 데이터는 그대로인 경우
  3. 경로 계층 구조 이해 필요
  4. revalidatePath('/', 'layout')는: ✅ /, /[page], /1, /2 무효화 ❌ /category/tech 무효화 안함 (독립적인 route segment) → 각 route segment는 명시적으로 무효화 필요!
  5. type: 'layout'이 하위 페이지를 포함하므로 중복 호출 불필요
  6. // ❌ 불필요한 중복 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만 무효화한다는 점이 가장 중요한 차이점입니다.