Files
nuxt-deep/docs/curriculum/07-caching-strategy.md

6.0 KiB

7. 최적화 캐시 / 재호출 전략

캐시 레이어 구조

Nuxt의 캐시는 여러 레이어에서 작동한다.

브라우저 캐시 (HTTP Cache-Control)
    ↑
CDN / Edge 캐시
    ↑
Nitro 서버 캐시 (cachedEventHandler, cachedFunction)
    ↑
useFetch / useAsyncData 캐시 (payload 기반)

1. useFetch / useAsyncData 캐시

기본 캐시 동작 (payload)

<script setup>
// 서버에서 가져온 데이터는 payload에 저장됨
// 클라이언트에서 같은 키로 요청하면 재요청 없이 payload 사용
const { data } = await useFetch('/api/products', {
  key: 'products-list'  // 명시적 캐시 키 (기본값: URL + params 조합)
})
</script>

stale-while-revalidate 패턴

"캐시된 데이터를 즉시 보여주면서 백그라운드에서 최신 데이터를 가져오는" 전략이다.

<script setup>
const { data, refresh } = await useFetch('/api/products', {
  // 1. 캐시된 데이터로 즉시 렌더링
  // 2. 백그라운드에서 새 데이터 요청
  // 3. 새 데이터 도착 시 자동으로 UI 업데이트
  getCachedData(key, nuxtApp) {
    // nuxtApp.payload.data에 이미 캐시된 데이터가 있으면 사용
    return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
  }
})
</script>

수동 refresh와 자동 watch

<script setup>
const page = ref(1)
const category = ref('tent')

const { data, pending, refresh } = await useFetch('/api/products', {
  query: { page, category },
  watch: [page, category],  // 값이 바뀌면 자동으로 재요청
})

// 수동으로 재요청
async function handleRefresh() {
  await refresh()
}
</script>

invalidation — 캐시 무효화 타이밍

<script setup>
const { data: products, refresh } = await useFetch('/api/products')

// 상품 등록 후 목록 갱신
async function createProduct(newProduct) {
  await $fetch('/api/products', {
    method: 'POST',
    body: newProduct
  })
  // 캐시 무효화: 수동 refresh
  await refresh()
}

// 또는 clearNuxtData로 특정 키 캐시 삭제
function invalidateCache() {
  clearNuxtData('products-list')
}
</script>

2. Nitro 서버 캐시

서버 API 응답 자체를 캐시한다. DB 부하를 크게 줄일 수 있다.

cachedEventHandler

// server/api/products.ts
export default cachedEventHandler(async (event) => {
  // DB에서 데이터 조회
  const products = await db.from('products').select('*')
  return products
}, {
  maxAge: 60 * 60,     // 1시간 캐시
  name: 'products',    // 캐시 키 이름
  getKey: (event) => {
    // 쿼리 파라미터에 따라 다른 캐시 키 사용
    const query = getQuery(event)
    return `products-${query.category}-${query.page}`
  },
  // 캐시 무효화 조건
  shouldBypassCache: (event) => {
    return getHeader(event, 'Cache-Control') === 'no-cache'
  }
})

cachedFunction

// server/utils/db.ts
export const getCachedProducts = cachedFunction(
  async (category: string) => {
    return await db.from('products').select('*').eq('category', category)
  },
  {
    maxAge: 60 * 10,  // 10분 캐시
    name: 'get-products',
    getKey: (category) => category,
  }
)

3. routeRules 캐시 (ISR)

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // ISR: 첫 요청 후 60초간 캐시, 만료 시 백그라운드에서 재생성
    '/products/**': { isr: 60 },

    // SSG: 빌드 시 생성, 변경 없음
    '/blog/**': { prerender: true },

    // CDN 캐시 헤더 설정
    '/api/public/**': {
      headers: {
        'Cache-Control': 's-maxage=3600, stale-while-revalidate=86400'
      }
    },

    // 캐시 완전 비활성화
    '/api/realtime/**': {
      headers: { 'Cache-Control': 'no-store' }
    }
  }
})

4. 컴포넌트 레벨 캐시

lazy 로딩 + 조건부 요청

<script setup>
const isTabActive = ref(false)

// isTabActive가 true가 될 때만 데이터 요청
const { data } = await useFetch('/api/analytics', {
  immediate: false,  // 즉시 요청하지 않음
})

watch(isTabActive, (active) => {
  if (active) refresh()
})
</script>

keep-alive로 컴포넌트 캐시

<!-- 페이지 전환해도 컴포넌트 상태 유지 -->
<NuxtPage :keepalive="{ include: ['ProductList', 'Dashboard'] }" />
// pages/products/index.vue
defineOptions({
  name: 'ProductList'  // keepalive에서 참조하는 이름
})

5. 실전 캐시 전략 예시

상품 목록 (자주 안 바뀜)

// nuxt.config.ts
routeRules: {
  '/products': { isr: 3600 }  // 1시간마다 재생성
}

// server/api/products.ts
export default cachedEventHandler(handler, {
  maxAge: 60 * 60,  // Nitro도 1시간 캐시
})

사용자 전용 데이터 (캐시 불가)

<script setup>
// server: false로 서버 캐시 우회
// 사용자별 데이터는 서버에서 캐시하면 안 됨
const { data: myOrders } = await useFetch('/api/my/orders', {
  server: false,  // 클라이언트에서만 요청
  headers: useRequestHeaders(['cookie'])  // 인증 쿠키 전달
})
</script>

실시간 데이터 (폴링)

<script setup>
const { data, refresh } = await useFetch('/api/stock', {
  server: false
})

// 30초마다 자동 갱신
let interval: ReturnType<typeof setInterval>

onMounted(() => {
  interval = setInterval(refresh, 30_000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>

요약

전략 사용 시점 구현 방법
Payload 캐시 SSR 초기 로딩 중복 방지 useFetch 기본 동작
수동 refresh 변경 후 최신 데이터 필요 refresh() 호출
watch 자동 재요청 필터/페이지 변경 시 watch 옵션
Nitro 서버 캐시 DB 부하 절감 cachedEventHandler
ISR 준정적 콘텐츠 routeRules: { isr: N }
keep-alive 페이지 전환 시 상태 유지 <NuxtPage keepalive>
폴링 실시간 데이터 setInterval + refresh