logo

DowanKim

6. 넥스트 캐싱 원리

2025년 10월 30일

포트폴리오 사이트

트러블슈팅 : Next.js 캐싱으로 인한 새 블로그 글이 반영되지 않는 문제

1. 문제 상황

블로그 시스템을 구현한 후, 새로운 블로그 글을 작성했는데 문제가 발생했습니다:

  • 로컬 개발 서버(npm run dev)에서는 새 글이 정상적으로 표시됨
  • 배포된 사이트(Vercel)에서는 새 글이 보이지 않음
  • Firebase 콘솔에서는 데이터가 정상적으로 저장되어 있음
  • 재배포 후에는 새 글이 표시됨

이상한 점:

  • Firebase에서 데이터를 가져오는데 왜 배포 환경에서만 안 보일까?
  • 로컬에서는 보이는데 배포 환경에서는 안 보이는 이유는?
  • 이게 말이되나...?

2. Next.js App Router의 캐싱 시스템 이해하기

2.1 Next.js가 캐싱을 하는 이유

Next.js는 성능 최적화를 위해 여러 레벨에서 캐싱을 사용합니다:

  1. 빠른 응답 속도: 한 번 렌더링한 결과를 재사용
  2. 서버 부하 감소: 동일한 요청에 대해 재계산 불필요
  3. 비용 절감: 서버 리소스 사용량 감소

2.2 Next.js App Router의 캐싱 레이어

Next.js 14 App Router는 다음 4가지 캐싱 레이어를 사용합니다:

┌─────────────────────────────────────────┐
│  1. Request Memoization (요청 메모이제이션) │
│     - 같은 요청 내에서 동일한 함수 호출 결과 캐싱
└─────────────────────────────────────────┘
           ↓
┌─────────────────────────────────────────┐
│  2. Data Cache (데이터 캐시)              │
│     - fetch() 호출 결과를 캐싱
│     - 기본적으로 영구 캐시 (무기한)
└─────────────────────────────────────────┘
           ↓
┌─────────────────────────────────────────┐
│  3. Full Route Cache (전체 라우트 캐시)    │
│     - 렌더링된 페이지 전체를 캐싱
│     - 정적 페이지는 빌드 시 생성
└─────────────────────────────────────────┘
           ↓
┌─────────────────────────────────────────┐
│  4. Router Cache (라우터 캐시)            │
│     - 클라이언트 사이드 라우팅 결과 캐싱
│     - 브라우저 메모리에 저장
└─────────────────────────────────────────┘

각 레이어를 자세히 살펴보겠습니다.

2.3 1단계: Request Memoization (요청 메모이제이션)

같은 요청 내에서 동일한 함수를 여러 번 호출해도 한 번만 실행합니다.

예시:

export default async function BlogPage() { const tags = await getAllTags() // 첫 번째 호출 const projects = await getAllProjects() // 같은 컴포넌트 내에서 다시 호출 const tags2 = await getAllTags() // 두 번째 호출이지만 실제로는 실행되지 않음! // tags와 tags2는 같은 참조 (메모이제이션됨) }

동작 원리:

  • Next.js가 같은 요청 내에서 getAllTags()를 감지
  • 첫 번째 호출 결과를 메모리에 저장
  • 두 번째 호출 시 저장된 결과를 반환

장점:

  • 불필요한 중복 호출 방지
  • 성능 향상

2.4 2단계: Data Cache (데이터 캐시)

fetch() 호출 결과를 캐싱합니다. 기본적으로 영구 캐시입니다.

예시:

// Next.js가 자동으로 캐싱 const response = await fetch('https://api.example.com/data') const data = await response.json()

주의사항:

  • Firebase SDK를 직접 사용하면 Data Cache가 적용되지 않음
  • fetch()를 사용해야 Data Cache가 작동

우리 프로젝트의 경우:

// Firebase SDK 직접 사용 const snapshot = await getDocs(collection(db, 'posts')) // → Data Cache 적용 안 됨!

2.5 3단계: Full Route Cache (전체 라우트 캐시)

렌더링된 페이지 전체를 캐싱합니다.

동작 방식:

  1. 정적 페이지 (Static)

    • 빌드 시점에 HTML 생성
    • 모든 사용자에게 동일한 HTML 제공
    • 예: /about, /contact
  2. 동적 페이지 (Dynamic)

    • 요청 시점에 HTML 생성
    • 사용자별로 다른 HTML 가능
    • 예: /blog/[tag], /blog/[tag]/[slug]

문제:

  • Next.js는 기본적으로 페이지를 정적으로 렌더링하려고 시도
  • 동적 데이터를 사용해도 정적으로 렌더링될 수 있음

2.6 4단계: Router Cache (라우터 캐시)

클라이언트 사이드 라우팅 결과를 브라우저 메모리에 캐싱합니다.

동작:

  • 사용자가 /blog로 이동
  • Next.js가 서버에서 HTML을 가져와서 렌더링
  • 결과를 브라우저 메모리에 저장
  • 같은 페이지로 다시 이동하면 캐시된 결과 사용

3. 개발 모드 vs 프로덕션 모드의 차이

3.1 개발 모드 (npm run dev)

npm run dev

특징:

  • 캐싱이 최소화됨
  • 매 요청마다 새로 렌더링
  • Hot Module Replacement (HMR) 지원
  • 개발 편의성 우선

동작:

// 개발 모드에서는 export default async function BlogPage() { const tags = await getAllTags() // 매번 새로 실행 // ... }

결과:

  • 새 블로그 글이 즉시 반영됨
  • 개발 중에는 문제가 없어 보임

3.2 프로덕션 모드 (npm run build + npm run start)

npm run build npm run start

특징:

  • 캐싱이 적극적으로 활용됨
  • 성능 최적화 우선
  • 빌드 시점에 최대한 많은 페이지를 정적으로 생성

동작:

// 프로덕션 모드에서는 export default async function BlogPage() { const tags = await getAllTags() // 캐시된 결과 사용 가능 // ... }

문제:

  • 빌드 시점의 데이터가 캐시됨
  • 새 블로그 글이 반영되지 않음

4. 문제의 원인 분석

4.1 왜 로컬에서는 보이고 배포 환경에서는 안 보일까?

시나리오:

  1. 로컬 개발 서버 (npm run dev)

    사용자 요청 → 서버 컴포넌트 실행 → Firebase에서 데이터 가져오기 → 렌더링
    
    • 캐싱이 최소화되어 매번 새로 실행
    • 새 블로그 글이 즉시 반영됨
  2. 배포 환경 (Vercel)

    사용자 요청 → 캐시 확인 → 캐시된 HTML 반환 (새 데이터 없음!)
    
    • Full Route Cache에 빌드 시점의 HTML이 저장됨
    • 새 블로그 글이 반영되지 않음

4.2 Next.js가 페이지를 정적으로 렌더링하는 조건

Next.js는 다음 조건을 만족하면 페이지를 정적으로 렌더링합니다:

  1. 동적 함수를 사용하지 않음

    • cookies(), headers(), searchParams 등을 사용하지 않음
  2. dynamic 설정이 없음

    • 기본값은 'auto' (자동 감지)
  3. revalidate 설정이 없음

    • 기본값은 무기한 캐시

우리 프로젝트의 경우:

// app/blog/page.tsx export default async function BlogPage() { const tags = await getAllTags() // Firebase에서 데이터 가져오기 // ... }

문제:

  • 동적 함수를 사용하지 않아서 Next.js가 정적으로 판단
  • 빌드 시점에 한 번만 실행되어 HTML 생성
  • 이후 요청에서는 캐시된 HTML만 반환

5. 해결 방법: 동적 렌더링 설정

5.1 dynamic 설정 이해하기

dynamic은 페이지의 렌더링 방식을 제어하는 설정입니다.

가능한 값:

  • 'auto' (기본값): Next.js가 자동으로 결정
  • 'force-dynamic': 항상 동적으로 렌더링 (캐싱 안 함)
  • 'force-static': 항상 정적으로 렌더링 (강제 캐싱)
  • 'error': 동적 렌더링이 필요하면 에러 발생

5.2 revalidate 설정 이해하기

revalidate는 캐시 재검증 시간을 설정합니다.

가능한 값:

  • false 또는 0: 캐시하지 않음 (항상 최신 데이터)
  • 숫자 (초): 해당 시간 동안 캐시, 이후 재검증
  • 예: revalidate: 60 → 60초 동안 캐시

5.3 구현 코드

각 블로그 페이지에 다음 설정을 추가합니다:

블로그 목록 페이지 (app/blog/page.tsx)

import Link from 'next/link' import { getAllTags, getAllProjects } from '@/lib/blog' import Header from '@/components/Home/Header/Header' // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function BlogPage() { const tags = await getAllTags() const projects = await getAllProjects() // ... }

태그별 페이지 (app/blog/[tag]/page.tsx)

import { notFound } from 'next/navigation' import { getProjectByTag, getPostsByTagPaginated } from '@/lib/blog' // ... // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function TagPage({ params }: PageProps) { const decodedTag = decodeURIComponent(params.tag) const project = await getProjectByTag(decodedTag) const initialResult = await getPostsByTagPaginated(decodedTag, 12) // ... }

블로그 글 상세 페이지 (app/blog/[tag]/[slug]/page.tsx)

import { getPostBySlug, getPostsByTag } from '@/lib/blog' // ... // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function BlogPostPage({ params }: PageProps) { const decodedSlug = decodeURIComponent(params.slug) const post = await getPostBySlug(decodedSlug) // ... }

6. 각 설정의 동작 원리

6.1 export const dynamic = 'force-dynamic'의 의미

이 설정은 Next.js에게 "이 페이지는 항상 동적으로 렌더링해야 한다"고 명시적으로 알려줍니다.

동작 과정:

  1. 설정 없을 때 (기본값 'auto')

    사용자 요청
    ↓
    Next.js: "동적 함수가 없네? 정적으로 렌더링하자"
    ↓
    빌드 시점에 HTML 생성
    ↓
    캐시된 HTML 반환 (새 데이터 없음)
    
  2. dynamic = 'force-dynamic' 설정 후

    사용자 요청
    ↓
    Next.js: "force-dynamic 설정이 있네? 무조건 동적으로 렌더링하자"
    ↓
    서버 컴포넌트 실행
    ↓
    Firebase에서 최신 데이터 가져오기
    ↓
    새로운 HTML 생성 및 반환
    

효과:

  • Full Route Cache 비활성화
  • 매 요청마다 새로 렌더링
  • 항상 최신 데이터 반영

6.2 export const revalidate = 0의 의미

이 설정은 캐시 재검증 시간을 0초로 설정합니다.

동작 과정:

  1. revalidate 없을 때 (기본값: 무기한 캐시)

    첫 번째 요청
    ↓
    데이터 가져오기 → 렌더링 → HTML 생성
    ↓
    캐시에 저장 (무기한)
    ↓
    두 번째 요청
    ↓
    캐시된 HTML 반환 (새 데이터 없음)
    
  2. revalidate = 0 설정 후

    첫 번째 요청
    ↓
    데이터 가져오기 → 렌더링 → HTML 생성
    ↓
    캐시에 저장하지 않음 (revalidate: 0)
    ↓
    두 번째 요청
    ↓
    다시 데이터 가져오기 → 렌더링 → HTML 생성
    ↓
    항상 최신 데이터 반환
    

효과:

  • Data Cache 비활성화
  • Request Memoization은 여전히 작동 (같은 요청 내에서만)
  • 매 요청마다 최신 데이터 가져오기

6.3 두 설정을 함께 사용하는 이유

export const dynamic = 'force-dynamic' // Full Route Cache 비활성화 export const revalidate = 0 // Data Cache 비활성화

왜 둘 다 필요한가?

  1. dynamic = 'force-dynamic'만 사용

    • Full Route Cache는 비활성화됨
    • 하지만 Data Cache는 여전히 작동할 수 있음
    • fetch()를 사용하는 경우 Data Cache가 적용됨
  2. revalidate = 0만 사용

    • Data Cache는 비활성화됨
    • 하지만 Full Route Cache는 여전히 작동할 수 있음
    • Next.js가 정적으로 판단하면 빌드 시점에 HTML 생성
  3. 둘 다 사용

    • 모든 캐싱 레이어를 우회
    • 항상 최신 데이터를 가져옴
    • 블로그 글처럼 자주 변경되는 콘텐츠에 적합

7. 캐싱 레이어별 상세 설명

7.1 Request Memoization (요청 메모이제이션)

같은 요청 내에서 동일한 함수 호출을 메모이제이션합니다.

예시:

export default async function BlogPage() { // 같은 요청 내에서 여러 번 호출 const tags1 = await getAllTags() const tags2 = await getAllTags() const tags3 = await getAllTags() // 실제로는 getAllTags()가 한 번만 실행됨 // tags1, tags2, tags3는 모두 같은 참조 }

동작 원리:

// Next.js 내부 동작 (의사 코드) const requestMemo = new Map() async function memoizedCall(fn, key) { if (requestMemo.has(key)) { return requestMemo.get(key) // 캐시된 결과 반환 } const result = await fn() requestMemo.set(key, result) // 결과 캐싱 return result }

장점:

  • 중복 호출 방지
  • 성능 향상

단점:

  • 같은 요청 내에서만 작동
  • 요청이 끝나면 캐시도 사라짐

7.2 Data Cache (데이터 캐시)

fetch() 호출 결과를 캐싱합니다.

예시:

// Next.js가 자동으로 캐싱 const response = await fetch('https://api.example.com/data', { cache: 'force-cache' // 기본값 })

주의사항:

  • Firebase SDK를 직접 사용하면 Data Cache가 적용되지 않음
  • 우리 프로젝트는 Firebase SDK를 직접 사용하므로 Data Cache 영향 없음

하지만 revalidate = 0을 설정하면:

  • 향후 fetch()를 사용할 때도 캐싱되지 않음
  • 일관된 동작 보장

7.3 Full Route Cache (전체 라우트 캐시)

렌더링된 페이지 전체를 캐싱합니다.

동작 방식:

  1. 정적 페이지

    빌드 시점
    ↓
    페이지 컴포넌트 실행
    ↓
    HTML 생성
    ↓
    .next/server/app/page.html에 저장
    ↓
    배포 시 이 HTML 파일 제공
    
  2. 동적 페이지 (dynamic = 'force-dynamic')

    사용자 요청
    ↓
    페이지 컴포넌트 실행
    ↓
    HTML 생성
    ↓
    캐시하지 않음 (매번 새로 생성)
    ↓
    사용자에게 전송
    

7.4 Router Cache (라우터 캐시)

클라이언트 사이드 라우팅 결과를 브라우저 메모리에 캐싱합니다.

동작:

사용자가 /blog로 이동
↓
Next.js가 서버에서 HTML 가져오기
↓
브라우저 메모리에 저장
↓
사용자가 다른 페이지로 이동 후 /blog로 다시 이동
↓
캐시된 HTML 사용 (서버 요청 없음)

특징:

  • 클라이언트 사이드에서만 작동
  • 브라우저를 닫으면 사라짐
  • dynamic = 'force-dynamic' 설정과는 무관 (서버 사이드 설정)

8. 개발 모드 vs 프로덕션 모드 상세 비교

8.1 개발 모드의 캐싱 동작

npm run dev

특징:

  • 모든 캐싱이 최소화됨
  • 매 요청마다 새로 렌더링
  • 개발 편의성 우선

동작:

// 개발 모드 export default async function BlogPage() { console.log('페이지 렌더링 시작') // 매 요청마다 출력됨 const tags = await getAllTags() console.log('태그 가져오기 완료') // 매 요청마다 실행됨 return <div>...</div> }

결과:

  • 새 블로그 글이 즉시 반영됨
  • 개발 중에는 문제가 없어 보임

8.2 프로덕션 모드의 캐싱 동작

npm run build npm run start

특징:

  • 캐싱이 적극적으로 활용됨
  • 성능 최적화 우선

동작 (설정 없을 때):

// 프로덕션 모드 (설정 없음) export default async function BlogPage() { console.log('페이지 렌더링 시작') // 빌드 시점에만 출력됨 const tags = await getAllTags() console.log('태그 가져오기 완료') // 빌드 시점에만 실행됨 return <div>...</div> }

결과:

  • 빌드 시점의 데이터만 표시됨
  • 새 블로그 글이 반영되지 않음

동작 (설정 후):

// 프로덕션 모드 (dynamic = 'force-dynamic', revalidate = 0) export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function BlogPage() { console.log('페이지 렌더링 시작') // 매 요청마다 출력됨 const tags = await getAllTags() console.log('태그 가져오기 완료') // 매 요청마다 실행됨 return <div>...</div> }

결과:

  • 매 요청마다 최신 데이터 가져오기
  • 새 블로그 글이 즉시 반영됨

9. 실제 동작 시나리오

9.1 설정 없을 때의 동작

시나리오: 새 블로그 글 작성 후 배포 환경에서 확인

1. 빌드 시점 (npm run build)
   ↓
   Next.js: "동적 함수가 없네? 정적으로 렌더링하자"
   ↓
   BlogPage 컴포넌트 실행
   ↓
   getAllTags() 호출 → Firebase에서 데이터 가져오기
   ↓
   (이 시점에는 새 블로그 글이 없음)
   ↓
   HTML 생성 및 캐시에 저장
   ↓
   배포 완료

2. 사용자 요청 (배포 후)
   ↓
   Next.js: "캐시된 HTML이 있네? 그걸 반환하자"
   ↓
   캐시된 HTML 반환 (새 블로그 글 없음)
   ↓
   ❌ 새 블로그 글이 보이지 않음

3. 새 블로그 글 작성 (Firebase에 저장)
   ↓
   Firebase: "새 글이 저장되었습니다"
   ↓
   하지만 Next.js는 여전히 캐시된 HTML 사용
   ↓
   ❌ 여전히 새 블로그 글이 보이지 않음

4. 재배포 (npm run build 다시 실행)
   ↓
   BlogPage 컴포넌트 다시 실행
   ↓
   getAllTags() 호출 → Firebase에서 최신 데이터 가져오기
   ↓
   (이제 새 블로그 글이 포함됨)
   ↓
   새로운 HTML 생성
   ↓
   ✅ 새 블로그 글이 보임

문제점:

  • 새 블로그 글을 작성할 때마다 재배포가 필요함
  • 실시간성이 떨어짐

9.2 설정 후의 동작

시나리오: dynamic = 'force-dynamic', revalidate = 0 설정 후

1. 빌드 시점 (npm run build)
   ↓
   Next.js: "force-dynamic 설정이 있네? 동적으로 렌더링하자"
   ↓
   HTML을 생성하지 않음 (동적 페이지로 표시)
   ↓
   배포 완료

2. 사용자 요청 (배포 후)
   ↓
   Next.js: "동적 페이지네? 지금 렌더링하자"
   ↓
   BlogPage 컴포넌트 실행
   ↓
   getAllTags() 호출 → Firebase에서 최신 데이터 가져오기
   ↓
   HTML 생성 및 반환
   ↓
   ✅ 항상 최신 데이터 반환

3. 새 블로그 글 작성 (Firebase에 저장)
   ↓
   Firebase: "새 글이 저장되었습니다"
   ↓
   (아무것도 하지 않아도 됨)

4. 사용자 요청 (새 글 작성 후)
   ↓
   Next.js: "동적 페이지네? 지금 렌더링하자"
   ↓
   BlogPage 컴포넌트 실행
   ↓
   getAllTags() 호출 → Firebase에서 최신 데이터 가져오기
   ↓
   (이제 새 블로그 글이 포함됨)
   ↓
   새로운 HTML 생성 및 반환
   ↓
   ✅ 새 블로그 글이 즉시 보임 (재배포 불필요!)

장점:

  • 새 블로그 글이 즉시 반영됨
  • 재배포 불필요
  • 실시간성 확보

10. 성능 vs 최신성 트레이드오프

10.1 캐싱의 장단점

캐싱 사용 시:

장점:

  • 빠른 응답 속도 (캐시된 결과 즉시 반환)
  • 서버 부하 감소 (재계산 불필요)
  • 비용 절감 (서버 리소스 사용량 감소)

단점:

  • 오래된 데이터 표시 가능
  • 새 콘텐츠가 즉시 반영되지 않음
  • 재배포가 필요할 수 있음

캐싱 비활성화 시:

장점:

  • 항상 최신 데이터 표시
  • 새 콘텐츠가 즉시 반영됨
  • 재배포 불필요

단점:

  • 응답 속도가 느려질 수 있음 (매번 렌더링)
  • 서버 부하 증가 (매번 재계산)
  • 비용 증가 (서버 리소스 사용량 증가)

10.2 우리 프로젝트에서의 선택

블로그 시스템의 특성:

  • 콘텐츠가 자주 변경됨 (새 글 작성)
  • 최신성이 중요함 (새 글이 즉시 반영되어야 함)
  • 재배포 없이 콘텐츠 업데이트가 필요함

선택:

export const dynamic = 'force-dynamic' // 캐싱 비활성화 export const revalidate = 0 // 항상 최신 데이터

결과:

  • 최신성 우선
  • 성능은 약간 희생하지만 블로그 특성상 허용 가능

10.3 다른 페이지는 어떻게 할까?

포트폴리오 메인 페이지 (app/page.tsx):

// 설정 없음 (기본값 사용) export default function PortfolioPage() { return ( <> <Header /> <Home /> <About /> {/* ... */} </> ) }

이유:

  • 정적 콘텐츠 (자기소개, 경력 등)
  • 자주 변경되지 않음
  • 캐싱을 사용해도 문제없음
  • 성능 최적화 유지

블로그 페이지:

// 동적 렌더링 설정 export const dynamic = 'force-dynamic' export const revalidate = 0

이유:

  • 동적 콘텐츠 (Firebase에서 가져옴)
  • 자주 변경됨 (새 글 작성)
  • 최신성이 중요함
  • 캐싱 비활성화 필요

11. 실제 코드에서의 적용

11.1 블로그 목록 페이지 (app/blog/page.tsx)

import Link from 'next/link' import { getAllTags, getAllProjects } from '@/lib/blog' import Header from '@/components/Home/Header/Header' // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function BlogPage() { // 이 함수는 매 요청마다 실행됨 const tags = await getAllTags() const projects = await getAllProjects() // 프로젝트 정보가 있는 태그와 없는 태그를 구분 const projectMap = new Map(projects.map(p => [p.tag, p])) const projectTags = tags.map(tag => ({ tag, project: projectMap.get(tag) || null, })) return ( <> <Header /> <section className="min-h-screen bg-[#050a13] text-white pt-32 px-4 pb-16"> {/* ... */} </section> </> ) }

동작:

  1. 사용자가 /blog로 접근
  2. Next.js가 dynamic = 'force-dynamic' 설정 확인
  3. 서버에서 BlogPage 컴포넌트 실행
  4. getAllTags(), getAllProjects() 호출 → Firebase에서 최신 데이터 가져오기
  5. HTML 생성 및 반환
  6. 캐시하지 않음 (revalidate = 0)
  7. 다음 요청 시 다시 1번부터 반복

11.2 태그별 페이지 (app/blog/[tag]/page.tsx)

import { getProjectByTag, getPostsByTagPaginated } from '@/lib/blog' // ... // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function TagPage({ params }: PageProps) { const decodedTag = decodeURIComponent(params.tag) // 매 요청마다 Firebase에서 최신 데이터 가져오기 const project = await getProjectByTag(decodedTag) const initialResult = await getPostsByTagPaginated(decodedTag, 12) const initialPosts = initialResult.posts if (initialPosts.length === 0) { notFound() } return ( <> <Header /> <section className="min-h-screen bg-[#050a13] text-white pt-32 px-4 pb-16"> {/* ... */} </section> </> ) }

동작:

  1. 사용자가 /blog/포트폴리오-사이트로 접근
  2. Next.js가 동적 라우트임을 인식
  3. dynamic = 'force-dynamic' 설정 확인
  4. 서버에서 TagPage 컴포넌트 실행
  5. getProjectByTag(), getPostsByTagPaginated() 호출 → Firebase에서 최신 데이터 가져오기
  6. HTML 생성 및 반환

11.3 블로그 글 상세 페이지 (app/blog/[tag]/[slug]/page.tsx)

import { getPostBySlug, getPostsByTag } from '@/lib/blog' // ... // 항상 동적으로 렌더링하여 최신 데이터를 가져오도록 설정 export const dynamic = 'force-dynamic' export const revalidate = 0 export default async function BlogPostPage({ params }: PageProps) { const decodedSlug = decodeURIComponent(params.slug) const decodedTag = decodeURIComponent(params.tag) // 매 요청마다 Firebase에서 최신 데이터 가져오기 const post = await getPostBySlug(decodedSlug) if (!post) { notFound() } // 같은 태그의 모든 포스트 가져오기 (이전/다음 포스트 찾기용) const allPosts = await getPostsByTag(decodedTag) // ... return ( <> <Header /> <article className="min-h-screen bg-[#050a13] text-white pt-32 px-4 pb-16"> {/* ... */} </article> </> ) }

동작:

  1. 사용자가 /blog/포트폴리오-사이트/nextjs-전환하기로 접근
  2. Next.js가 동적 라우트임을 인식
  3. dynamic = 'force-dynamic' 설정 확인
  4. 서버에서 BlogPostPage 컴포넌트 실행
  5. getPostBySlug(), getPostsByTag() 호출 → Firebase에서 최신 데이터 가져오기
  6. HTML 생성 및 반환

12. 최적화 방법?

12.1 성능 고려사항

모든 페이지에 force-dynamic을 적용하면:

  • 서버 부하 증가
  • 응답 속도 저하 가능
  • 비용 증가

권장사항:

  • 동적 콘텐츠가 자주 변경되는 페이지만 적용
  • 정적 콘텐츠는 기본 캐싱 유지

12.2 ISR (Incremental Static Regeneration) 고려

만약 성능과 최신성의 균형이 필요하다면:

// ISR 사용 예시 export const revalidate = 60 // 60초마다 재검증 export default async function BlogPage() { const tags = await getAllTags() // ... }

동작:

  • 첫 요청: 데이터 가져와서 HTML 생성 및 캐싱
  • 60초 동안: 캐시된 HTML 반환 (빠름)
  • 60초 후: 백그라운드에서 재검증 (새 데이터 확인)
  • 새 데이터가 있으면: 캐시 업데이트
  • 새 데이터가 없으면: 기존 캐시 유지

제 프로젝트에서는:

  • 블로그 글은 즉시 반영되어야 함
  • revalidate = 0 사용 (캐싱 안 함)

12.3 부분적 캐싱 (Selective Caching)

일부 데이터만 캐싱하고 싶다면:

// 예시: 프로젝트 목록은 캐싱, 블로그 글은 캐싱 안 함 export default async function BlogPage() { // 프로젝트 목록은 자주 변경되지 않으므로 캐싱 가능 const projects = await getAllProjects() // 캐싱 가능 // 블로그 글은 자주 변경되므로 캐싱 안 함 const tags = await getAllTags() // 캐싱 안 함 }

하지만:

  • dynamic = 'force-dynamic' 설정 시 모든 데이터가 캐싱되지 않음
  • 세밀한 제어가 어려움

13. 디버깅 방법

13.1 캐싱 상태 확인

개발 중에 캐싱 상태를 확인하려면:

export default async function BlogPage() { console.log('페이지 렌더링 시간:', new Date().toISOString()) const tags = await getAllTags() console.log('태그 개수:', tags.length) // ... }

확인 방법:

  • 개발 모드: 매 요청마다 콘솔에 출력됨
  • 프로덕션 모드 (설정 없음): 빌드 시점에만 출력됨
  • 프로덕션 모드 (설정 후): 매 요청마다 출력됨

13.2 Vercel 로그 확인

Vercel 대시보드에서:

  1. 프로젝트 → Functions 탭
  2. 각 요청의 로그 확인
  3. 함수 실행 시간 확인

확인 사항:

  • 매 요청마다 함수가 실행되는지
  • 실행 시간이 적절한지
  • 에러가 없는지

14. 마무리

Next.js 캐싱 문제에 대해 알아보았습니다.

  1. 문제 원인 파악: Next.js의 기본 캐싱 동작 이해
  2. 해결 방법 적용: dynamic = 'force-dynamic', revalidate = 0 설정
  3. 동작 원리 이해: 각 캐싱 레이어의 역할과 동작 방식
  4. 성능 vs 최신성 트레이드오프: 블로그 특성에 맞는 선택

결과:

  • 새 블로그 글이 즉시 반영됨 (재배포 불필요)
  • Firebase에서 항상 최신 데이터를 가져옴
  • 로컬과 배포 환경의 동작이 일치함

이 설정으로 블로그 시스템이 실시간으로 콘텐츠를 반영할 수 있게 되었습니다. 성능상 손해가 있을 수 있지만, 상식적으로 방금 내가 글을 썼는데 이 글을 못보는건 말이 안된다고 생각했습니다. 또한 생각보다 넥스트 캐싱에 대한 원리를 깊게 파고들 수 있는 계기가 되었습니다.