6. 넥스트 캐싱 원리
2025년 10월 30일
트러블슈팅 : Next.js 캐싱으로 인한 새 블로그 글이 반영되지 않는 문제
1. 문제 상황
블로그 시스템을 구현한 후, 새로운 블로그 글을 작성했는데 문제가 발생했습니다:
- 로컬 개발 서버(
npm run dev)에서는 새 글이 정상적으로 표시됨 - 배포된 사이트(Vercel)에서는 새 글이 보이지 않음
- Firebase 콘솔에서는 데이터가 정상적으로 저장되어 있음
- 재배포 후에는 새 글이 표시됨
이상한 점:
- Firebase에서 데이터를 가져오는데 왜 배포 환경에서만 안 보일까?
- 로컬에서는 보이는데 배포 환경에서는 안 보이는 이유는?
- 이게 말이되나...?
2. Next.js App Router의 캐싱 시스템 이해하기
2.1 Next.js가 캐싱을 하는 이유
Next.js는 성능 최적화를 위해 여러 레벨에서 캐싱을 사용합니다:
- 빠른 응답 속도: 한 번 렌더링한 결과를 재사용
- 서버 부하 감소: 동일한 요청에 대해 재계산 불필요
- 비용 절감: 서버 리소스 사용량 감소
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 (전체 라우트 캐시)
렌더링된 페이지 전체를 캐싱합니다.
동작 방식:
-
정적 페이지 (Static)
- 빌드 시점에 HTML 생성
- 모든 사용자에게 동일한 HTML 제공
- 예:
/about,/contact
-
동적 페이지 (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 왜 로컬에서는 보이고 배포 환경에서는 안 보일까?
시나리오:
-
로컬 개발 서버 (
npm run dev)사용자 요청 → 서버 컴포넌트 실행 → Firebase에서 데이터 가져오기 → 렌더링- 캐싱이 최소화되어 매번 새로 실행
- 새 블로그 글이 즉시 반영됨
-
배포 환경 (Vercel)
사용자 요청 → 캐시 확인 → 캐시된 HTML 반환 (새 데이터 없음!)- Full Route Cache에 빌드 시점의 HTML이 저장됨
- 새 블로그 글이 반영되지 않음
4.2 Next.js가 페이지를 정적으로 렌더링하는 조건
Next.js는 다음 조건을 만족하면 페이지를 정적으로 렌더링합니다:
-
동적 함수를 사용하지 않음
cookies(),headers(),searchParams등을 사용하지 않음
-
dynamic설정이 없음- 기본값은
'auto'(자동 감지)
- 기본값은
-
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에게 "이 페이지는 항상 동적으로 렌더링해야 한다"고 명시적으로 알려줍니다.
동작 과정:
-
설정 없을 때 (기본값
'auto')사용자 요청 ↓ Next.js: "동적 함수가 없네? 정적으로 렌더링하자" ↓ 빌드 시점에 HTML 생성 ↓ 캐시된 HTML 반환 (새 데이터 없음) -
dynamic = 'force-dynamic'설정 후사용자 요청 ↓ Next.js: "force-dynamic 설정이 있네? 무조건 동적으로 렌더링하자" ↓ 서버 컴포넌트 실행 ↓ Firebase에서 최신 데이터 가져오기 ↓ 새로운 HTML 생성 및 반환
효과:
- Full Route Cache 비활성화
- 매 요청마다 새로 렌더링
- 항상 최신 데이터 반영
6.2 export const revalidate = 0의 의미
이 설정은 캐시 재검증 시간을 0초로 설정합니다.
동작 과정:
-
revalidate없을 때 (기본값: 무기한 캐시)첫 번째 요청 ↓ 데이터 가져오기 → 렌더링 → HTML 생성 ↓ 캐시에 저장 (무기한) ↓ 두 번째 요청 ↓ 캐시된 HTML 반환 (새 데이터 없음) -
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 비활성화
왜 둘 다 필요한가?
-
dynamic = 'force-dynamic'만 사용- Full Route Cache는 비활성화됨
- 하지만 Data Cache는 여전히 작동할 수 있음
fetch()를 사용하는 경우 Data Cache가 적용됨
-
revalidate = 0만 사용- Data Cache는 비활성화됨
- 하지만 Full Route Cache는 여전히 작동할 수 있음
- Next.js가 정적으로 판단하면 빌드 시점에 HTML 생성
-
둘 다 사용
- 모든 캐싱 레이어를 우회
- 항상 최신 데이터를 가져옴
- 블로그 글처럼 자주 변경되는 콘텐츠에 적합
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 (전체 라우트 캐시)
렌더링된 페이지 전체를 캐싱합니다.
동작 방식:
-
정적 페이지
빌드 시점 ↓ 페이지 컴포넌트 실행 ↓ HTML 생성 ↓ .next/server/app/page.html에 저장 ↓ 배포 시 이 HTML 파일 제공 -
동적 페이지 (
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> </> ) }
동작:
- 사용자가
/blog로 접근 - Next.js가
dynamic = 'force-dynamic'설정 확인 - 서버에서
BlogPage컴포넌트 실행 getAllTags(),getAllProjects()호출 → Firebase에서 최신 데이터 가져오기- HTML 생성 및 반환
- 캐시하지 않음 (
revalidate = 0) - 다음 요청 시 다시 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> </> ) }
동작:
- 사용자가
/blog/포트폴리오-사이트로 접근 - Next.js가 동적 라우트임을 인식
dynamic = 'force-dynamic'설정 확인- 서버에서
TagPage컴포넌트 실행 getProjectByTag(),getPostsByTagPaginated()호출 → Firebase에서 최신 데이터 가져오기- 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> </> ) }
동작:
- 사용자가
/blog/포트폴리오-사이트/nextjs-전환하기로 접근 - Next.js가 동적 라우트임을 인식
dynamic = 'force-dynamic'설정 확인- 서버에서
BlogPostPage컴포넌트 실행 getPostBySlug(),getPostsByTag()호출 → Firebase에서 최신 데이터 가져오기- 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 대시보드에서:
- 프로젝트 → Functions 탭
- 각 요청의 로그 확인
- 함수 실행 시간 확인
확인 사항:
- 매 요청마다 함수가 실행되는지
- 실행 시간이 적절한지
- 에러가 없는지
14. 마무리
Next.js 캐싱 문제에 대해 알아보았습니다.
- 문제 원인 파악: Next.js의 기본 캐싱 동작 이해
- 해결 방법 적용:
dynamic = 'force-dynamic',revalidate = 0설정 - 동작 원리 이해: 각 캐싱 레이어의 역할과 동작 방식
- 성능 vs 최신성 트레이드오프: 블로그 특성에 맞는 선택
결과:
- 새 블로그 글이 즉시 반영됨 (재배포 불필요)
- Firebase에서 항상 최신 데이터를 가져옴
- 로컬과 배포 환경의 동작이 일치함
이 설정으로 블로그 시스템이 실시간으로 콘텐츠를 반영할 수 있게 되었습니다. 성능상 손해가 있을 수 있지만, 상식적으로 방금 내가 글을 썼는데 이 글을 못보는건 말이 안된다고 생각했습니다. 또한 생각보다 넥스트 캐싱에 대한 원리를 깊게 파고들 수 있는 계기가 되었습니다.