포스트

[이제와서 시작하는 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;

동작 방식:

  1. 첫 방문: 데이터 가져오기 + 결과 캐싱
  2. 두 번째 방문: 캐시된 결과 즉시 반환 ⚡
  3. 세 번째 방문: 여전히 캐시된 결과 ⚡

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. 요청 시: 캐시된 페이지 즉시 반환
  3. 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'
});

🎯 오늘 배운 내용 정리

✅ 핵심 개념

  1. 캐싱
    • 한 번 계산한 결과 저장
    • 빠른 응답 + 서버 부하 감소
  2. “use cache” (Next.js 16)
    • 컴포넌트/함수 전체 캐싱
    • 명시적이고 예측 가능
  3. Revalidate
    • 시간 기반: revalidate: 60
    • 태그 기반: revalidateTag('posts')
    • 경로 기반: revalidatePath('/blog')
  4. fetch 캐싱
    • 기본: 캐싱됨
    • cache: 'no-store': 비활성화
    • revalidate: N: N초마다 갱신

🚀 다음 단계

다음 포스트에서는:

  • Server Actions 완벽 가이드
  • 폼 처리와 데이터 변경
  • Progressive Enhancement

를 배워보겠습니다!


📚 시리즈 네비게이션

이전 글

다음 글


🔗 참고 자료


“캐싱을 마스터하면 10배 빠른 웹사이트를 만들 수 있습니다!” - 성능 최적화의 핵심! ⚡

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.