[이제와서 시작하는 Next.js 마스터하기 #4] 캐싱과 성능 최적화 완벽 가이드
[이제와서 시작하는 Next.js 마스터하기 #4] 캐싱과 성능 최적화 완벽 가이드
“웹사이트가 빠르다는 건 캐싱이 잘 되어 있다는 것!” - Next.js 16의 새로운 캐싱 시스템으로 10배 빠른 웹사이트를 만들어보세요!
🎯 이 글에서 배울 내용
- 캐싱이 무엇이고 왜 중요한지
- Next.js 16의 “use cache” directive
- fetch API 캐싱 전략
- revalidate로 데이터 갱신하기
- 캐싱 Best Practices
예상 소요 시간: 40분 사전 지식: #3 Server Components와 데이터 페칭
🤔 캐싱이 뭔가요? (쉽게 설명)
🍪 쿠키 통으로 이해하는 캐싱
캐싱 없이 (매번 새로 만듦):
1
2
3
4
5
1. 손님이 쿠키 요청
2. 오븐에 쿠키 굽기 (30분)
3. 쿠키 전달
4. 또 다른 손님이 같은 쿠키 요청
5. 다시 오븐에 쿠키 굽기 (30분) 😓
캐싱 사용 (한 번 만들어서 보관):
1
2
3
4
5
1. 손님이 쿠키 요청
2. 오븐에 쿠키 굽기 (30분)
3. 쿠키 통에 보관 📦
4. 또 다른 손님이 같은 쿠키 요청
5. 쿠키 통에서 꺼내서 전달 (즉시!) 😊
📊 캐싱의 장점
| 항목 | 캐싱 없음 | 캐싱 사용 |
|---|---|---|
| 응답 속도 | 느림 (매번 계산) | 빠름 (저장된 것 사용) |
| 서버 부하 | 높음 | 낮음 |
| 데이터베이스 요청 | 매번 | 한 번만 |
| 사용자 경험 | 😓 | 😊 |
🆕 Next.js 16의 “use cache” (신기능!)
1. 기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/products/page.js
'use cache'; // ← 이 한 줄로 전체 페이지 캐싱!
async function ProductsPage() {
// 이 데이터는 캐싱됩니다
const products = await fetch('https://api.example.com/products')
.then(r => r.json());
return (
<div>
<h1>상품 목록</h1>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
export default ProductsPage;
동작 방식:
- 첫 방문: 데이터 가져오기 + 결과 캐싱
- 두 번째 방문: 캐시된 결과 즉시 반환 ⚡
- 세 번째 방문: 여전히 캐시된 결과 ⚡
2. 함수 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
// lib/data.js
'use cache';
export async function getPopularProducts() {
// 이 함수의 결과가 캐싱됩니다
const products = await db.product.findMany({
where: { views: { gt: 1000 } },
orderBy: { views: 'desc' },
take: 10
});
return products;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/home/page.js
import { getPopularProducts } from '@/lib/data';
async function HomePage() {
// 캐시된 결과 사용
const popular = await getPopularProducts();
return (
<div>
<h2>인기 상품</h2>
{popular.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
3. 컴포넌트 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// components/PopularPosts.js
'use cache';
async function PopularPosts() {
const posts = await fetch('https://api.example.com/popular-posts')
.then(r => r.json());
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
export default PopularPosts;
⏰ Revalidate - 캐시 갱신하기
🤔 문제: 캐시가 영원히 유지되면?
1
2
3
4
5
6
7
8
9
'use cache';
async function NewsPage() {
const news = await fetch('https://api.example.com/news')
.then(r => r.json());
// 문제: 새 뉴스가 나와도 캐시 때문에 안 보임!
return <div>{/* news 표시 */}</div>;
}
✅ 해결: revalidate 설정
방법 1: 시간 기반 재검증
1
2
3
4
5
6
7
8
9
// app/news/page.js
async function NewsPage() {
// 60초마다 캐시 갱신
const news = await fetch('https://api.example.com/news', {
next: { revalidate: 60 }
}).then(r => r.json());
return <div>{/* news 표시 */}</div>;
}
동작 원리:
1
2
3
4
0초: 첫 방문 → API 호출 → 캐시 저장
10초: 두 번째 방문 → 캐시 반환 (빠름!)
59초: 세 번째 방문 → 캐시 반환 (빠름!)
61초: 네 번째 방문 → API 재호출 → 새 캐시 저장
방법 2: 태그 기반 재검증
1
2
3
4
5
6
7
8
9
// app/posts/page.js
async function PostsPage() {
// 'posts' 태그로 캐시 관리
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
}).then(r => r.json());
return <div>{/* posts 표시 */}</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/actions.js
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData) {
// 새 포스트 생성
await db.post.create({
data: {
title: formData.get('title'),
content: formData.get('content')
}
});
// 'posts' 태그가 붙은 모든 캐시 무효화
revalidateTag('posts');
}
방법 3: 경로 기반 재검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use server';
import { revalidatePath } from 'next/cache';
export async function updatePost(id, data) {
await db.post.update({
where: { id },
data
});
// 특정 경로만 재검증
revalidatePath('/blog');
revalidatePath(`/blog/${id}`);
}
🎯 fetch API 캐싱 전략
1. 기본값: 캐싱됨
1
2
3
4
5
6
7
async function Page() {
// 기본: 무한 캐싱
const data = await fetch('https://api.example.com/data')
.then(r => r.json());
return <div>{data.title}</div>;
}
2. 캐싱 비활성화 (항상 최신)
1
2
3
4
5
6
7
8
async function RealTimePage() {
// 매번 새로 가져오기
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
}).then(r => r.json());
return <div>{data.value}</div>;
}
사용 예시:
- 주식 가격
- 실시간 채팅
- 사용자별 데이터
3. 시간 기반 캐싱
1
2
3
4
5
6
7
8
async function WeatherPage() {
// 10분마다 갱신
const weather = await fetch('https://api.weather.com/current', {
next: { revalidate: 600 } // 600초 = 10분
}).then(r => r.json());
return <div>온도: {weather.temp}°C</div>;
}
추천 revalidate 값:
- 뉴스: 60초 (1분)
- 날씨: 600초 (10분)
- 블로그 포스트: 3600초 (1시간)
- 상품 정보: 86400초 (24시간)
4. 여러 fetch 조합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function DashboardPage() {
// 각각 다른 캐싱 전략
const [user, stats, realtime] = await Promise.all([
// 사용자 정보: 5분 캐싱
fetch('/api/user', {
next: { revalidate: 300 }
}),
// 통계: 1시간 캐싱
fetch('/api/stats', {
next: { revalidate: 3600 }
}),
// 실시간 데이터: 캐싱 안 함
fetch('/api/realtime', {
cache: 'no-store'
})
]).then(responses =>
Promise.all(responses.map(r => r.json()))
);
return (
<div>
<UserInfo data={user} />
<Stats data={stats} />
<RealtimeChart data={realtime} />
</div>
);
}
⚡ 성능 최적화 실전 패턴
1. 점진적 정적 재생성 (ISR)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/blog/[slug]/page.js
export const revalidate = 3600; // 1시간마다 재생성
async function BlogPostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// 빌드 시 생성할 페이지들
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({
slug: post.slug
}));
}
동작 방식:
- 빌드 시: 모든 포스트 정적 생성
- 요청 시: 캐시된 페이지 즉시 반환
- 1시간 후: 백그라운드에서 재생성
2. 조건부 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function ProductPage({ searchParams }) {
const { sort } = await searchParams;
// 정렬 옵션에 따라 다른 캐싱 전략
const cacheOption = sort === 'price'
? { revalidate: 3600 } // 가격순: 1시간
: { cache: 'no-store' }; // 최신순: 캐싱 안 함
const products = await fetch(
`https://api.example.com/products?sort=${sort}`,
cacheOption
).then(r => r.json());
return <ProductList products={products} />;
}
3. 캐시 워밍 (Cache Warming)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// scripts/warm-cache.js
async function warmCache() {
const popularPages = [
'/',
'/products',
'/about',
'/blog'
];
// 인기 페이지 미리 캐싱
for (const page of popularPages) {
await fetch(`https://mysite.com${page}`);
console.log(`✅ Warmed: ${page}`);
}
}
warmCache();
🔧 캐싱 디버깅
1. 캐시 확인하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app/test/page.js
async function TestPage() {
const start = Date.now();
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
}).then(r => r.json());
const duration = Date.now() - start;
return (
<div>
<p>데이터: {JSON.stringify(data)}</p>
<p>소요 시간: {duration}ms</p>
<p>
{duration < 50 ? '✅ 캐시됨!' : '⏳ API 호출함'}
</p>
</div>
);
}
2. 캐시 무효화 버튼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/admin/clear-cache/page.js
import { revalidatePath } from 'next/cache';
async function clearCache() {
'use server';
revalidatePath('/', 'layout');
return { success: true };
}
function ClearCachePage() {
return (
<form action={clearCache}>
<button type="submit">
전체 캐시 지우기
</button>
</form>
);
}
🔍 자주 묻는 질문 (FAQ)
Q1: 캐싱과 revalidate 중 뭘 써야 하나요?
답: 데이터 특성에 따라 선택하세요!
시간 기반 revalidate (추천):
1
2
3
4
// 정기적으로 업데이트되는 데이터
const data = await fetch('/api/news', {
next: { revalidate: 60 } // 1분마다
});
사용 예시: 뉴스, 날씨, 통계
캐싱 비활성화:
1
2
3
4
// 항상 최신이어야 하는 데이터
const data = await fetch('/api/realtime', {
cache: 'no-store'
});
사용 예시: 사용자 프로필, 장바구니, 실시간 데이터
Q2: "use cache"는 언제 사용하나요?
답: 전체 컴포넌트/함수 결과를 캐싱하고 싶을 때!
use cache (Next.js 16 신기능):
1
2
3
4
5
6
7
'use cache';
async function ExpensiveComponent() {
// 복잡한 계산이나 느린 데이터 페칭
const result = await heavyComputation();
return <div>{result}</div>;
}
장점:
- 컴포넌트 전체 캐싱
- 함수 레벨 캐싱
- 더 명시적이고 예측 가능
기존 fetch 캐싱과 병행 사용 가능:
1
2
3
4
5
6
7
8
'use cache';
async function Page() {
const data = await fetch('/api/data', {
next: { revalidate: 60 }
});
// ...
}
Q3: 개발 중에는 캐싱이 안 되는 것 같아요
답: 맞습니다! 개발 모드에서는 캐싱이 제한적입니다.
개발 모드 (npm run dev):
- 캐싱 거의 안 됨
- 빠른 개발을 위해
프로덕션 (npm run build && npm start):
- 캐싱 완전히 작동
- 실제 성능 확인 가능
테스트하려면:
1
2
3
4
5
# 프로덕션 빌드
npm run build
# 프로덕션 모드로 실행
npm start
Q4: 캐시를 수동으로 지울 수 있나요?
답: 네! 여러 방법이 있습니다.
방법 1: revalidatePath
1
2
3
4
5
6
7
'use server';
import { revalidatePath } from 'next/cache';
export async function clearBlogCache() {
revalidatePath('/blog');
}
방법 2: revalidateTag
1
2
3
4
5
6
7
'use server';
import { revalidateTag } from 'next/cache';
export async function clearPostsCache() {
revalidateTag('posts');
}
방법 3: 전체 재시작
1
2
# 개발 서버 재시작
# Ctrl + C → npm run dev
Q5: 캐싱 때문에 업데이트가 안 보여요!
답: 캐시 전략을 조정하세요!
문제 상황:
1
2
// 새 데이터를 추가했는데 안 보임
const posts = await fetch('/api/posts');
해결책 1: revalidate 짧게
1
2
3
const posts = await fetch('/api/posts', {
next: { revalidate: 10 } // 10초마다 갱신
});
해결책 2: 태그 사용
1
2
3
4
5
6
7
8
// 데이터 가져올 때
const posts = await fetch('/api/posts', {
next: { tags: ['posts'] }
});
// 데이터 추가할 때
await createPost(data);
revalidateTag('posts'); // 즉시 캐시 무효화
해결책 3: 캐싱 비활성화
1
2
3
const posts = await fetch('/api/posts', {
cache: 'no-store' // 항상 최신
});
💡 캐싱 Best Practices
1. 기본은 캐싱, 필요할 때만 비활성화
1
2
3
4
5
6
7
// ✅ 좋음: 대부분 캐싱
const staticData = await fetch('/api/static'); // 캐싱
// 필요할 때만 비활성화
const realtimeData = await fetch('/api/realtime', {
cache: 'no-store'
});
2. revalidate 값은 데이터 특성에 맞게
1
2
3
4
5
6
7
8
9
// 자주 변하는 데이터: 짧은 revalidate
const news = await fetch('/api/news', {
next: { revalidate: 60 } // 1분
});
// 거의 안 변하는 데이터: 긴 revalidate
const about = await fetch('/api/about', {
next: { revalidate: 86400 } // 24시간
});
3. 태그로 관련 캐시 그룹화
1
2
3
4
5
6
7
8
9
10
11
// 모든 블로그 관련 fetch에 'blog' 태그
const posts = await fetch('/api/posts', {
next: { tags: ['blog', 'posts'] }
});
const categories = await fetch('/api/categories', {
next: { tags: ['blog', 'categories'] }
});
// 블로그 업데이트 시 한 번에 무효화
revalidateTag('blog');
4. 사용자별 데이터는 캐싱 안 함
1
2
3
4
5
6
7
// ❌ 나쁨: 사용자 데이터 캐싱
const user = await fetch('/api/user'); // 잘못된 사용자 정보 보일 수 있음
// ✅ 좋음: 사용자 데이터는 캐싱 비활성화
const user = await fetch('/api/user', {
cache: 'no-store'
});
🎯 오늘 배운 내용 정리
✅ 핵심 개념
- 캐싱
- 한 번 계산한 결과 저장
- 빠른 응답 + 서버 부하 감소
- “use cache” (Next.js 16)
- 컴포넌트/함수 전체 캐싱
- 명시적이고 예측 가능
- Revalidate
- 시간 기반:
revalidate: 60 - 태그 기반:
revalidateTag('posts') - 경로 기반:
revalidatePath('/blog')
- 시간 기반:
- fetch 캐싱
- 기본: 캐싱됨
cache: 'no-store': 비활성화revalidate: N: N초마다 갱신
🚀 다음 단계
다음 포스트에서는:
- Server Actions 완벽 가이드
- 폼 처리와 데이터 변경
- Progressive Enhancement
를 배워보겠습니다!
📚 시리즈 네비게이션
이전 글
다음 글
- #5 Server Actions과 폼 처리 (다음 포스트에서 계속!)
🔗 참고 자료
“캐싱을 마스터하면 10배 빠른 웹사이트를 만들 수 있습니다!” - 성능 최적화의 핵심! ⚡
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.