메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
SonamuλŠ” HTTP Cache-Control 헀더λ₯Ό μžλ™μœΌλ‘œ μ„€μ •ν•˜μ—¬ λΈŒλΌμš°μ €μ™€ CDN의 캐싱 λ™μž‘μ„ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. API 응닡, 정적 파일, SSR νŽ˜μ΄μ§€ λ“± 각 λ¦¬μ†ŒμŠ€ νƒ€μž…λ³„λ‘œ μ μ ˆν•œ 캐싱 μ „λž΅μ„ μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

기본 ꡬ쑰

import { defineConfig, CachePresets } from "sonamu";

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        switch (req.type) {
          case "assets":
            return CachePresets.immutable;
          case "api":
            return CachePresets.noCache;
          case "ssr":
            return CachePresets.ssr;
          case "csr":
            return CachePresets.shortLived;
        }
      },
    },
  },
  // ...
});

cacheControlHandler

각 μš”μ²­μ— λŒ€ν•΄ Cache-Control 헀더λ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜λŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€. νƒ€μž…: (req: CacheControlRequest) => string | undefined
type CacheControlRequest = {
  type: "assets" | "api" | "ssr" | "csr";
  method: string;
  path: string;
};

μš”μ²­ νƒ€μž…

type:
  • "assets" - 정적 파일 (JS, CSS, 이미지 λ“±)
  • "api" - API μ—”λ“œν¬μΈνŠΈ
  • "ssr" - SSR νŽ˜μ΄μ§€
  • "csr" - CSR μ§„μž…μ  (index.html)
method: HTTP λ©”μ„œλ“œ (GET, POST, PUT, DELETE λ“±) path: μš”μ²­ 경둜 (/api/user/list, /assets/main.js λ“±)

CachePresets

Sonamuκ°€ μ œκ³΅ν•˜λŠ” 사전 μ •μ˜λœ 캐싱 μ „λž΅μž…λ‹ˆλ‹€.
import { CachePresets } from "sonamu";

μ‚¬μš© κ°€λŠ₯ν•œ Presets

CachePresets.noCache
Cache-Control: no-cache, no-store, must-revalidate
  • 항상 원본 μ„œλ²„μ—μ„œ μ΅œμ‹  버전 확인
  • μš©λ„: μ‹€μ‹œκ°„ 데이터, 인증이 ν•„μš”ν•œ API
CachePresets.shortLived (1λΆ„)
Cache-Control: public, max-age=60
  • 1λΆ„κ°„ μΊμ‹œ
  • μš©λ„: 자주 λ³€κ²½λ˜λŠ” 데이터, CSR μ§„μž…μ 
CachePresets.mediumLived (5λΆ„)
Cache-Control: public, max-age=300
  • 5λΆ„κ°„ μΊμ‹œ
  • μš©λ„: 보톡 주기둜 λ³€κ²½λ˜λŠ” 데이터
CachePresets.longLived (1μ‹œκ°„)
Cache-Control: public, max-age=3600
  • 1μ‹œκ°„ μΊμ‹œ
  • μš©λ„: 거의 λ³€κ²½λ˜μ§€ μ•ŠλŠ” 정적 데이터
CachePresets.immutable (1λ…„)
Cache-Control: public, max-age=31536000, immutable
  • 1λ…„κ°„ μΊμ‹œ, λ³€κ²½λ˜μ§€ μ•ŠμŒ
  • μš©λ„: ν•΄μ‹œκ°€ ν¬ν•¨λœ λΉŒλ“œ 파일
CachePresets.ssr (10초)
Cache-Control: public, max-age=10, stale-while-revalidate=60
  • 10μ΄ˆκ°„ μΊμ‹œ, λ°±κ·ΈλΌμš΄λ“œμ—μ„œ κ°±μ‹ 
  • μš©λ„: SSR νŽ˜μ΄μ§€

κΈ°λ³Έ μ˜ˆμ‹œ

λ‹¨μˆœ μ„€μ •

import { defineConfig, CachePresets } from "sonamu";

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        // λͺ¨λ“  APIλŠ” μΊμ‹œ μ—†μŒ
        if (req.type === "api") {
          return CachePresets.noCache;
        }

        // 정적 νŒŒμΌμ€ κΈ΄ μΊμ‹œ
        if (req.type === "assets") {
          return CachePresets.longLived;
        }

        // SSR νŽ˜μ΄μ§€λŠ” 짧은 μΊμ‹œ
        if (req.type === "ssr") {
          return CachePresets.ssr;
        }
      },
    },
  },
});

Assets μ„ΈλΆ„ν™”

ν•΄μ‹œκ°€ ν¬ν•¨λœ νŒŒμΌμ€ 영ꡬ μΊμ‹œ, κ·Έ μ™ΈλŠ” 일반 μΊμ‹œ:
export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        if (req.type === "assets") {
          // ν•΄μ‹œ 포함 파일 (예: main-a3f2b1c.js)
          if (req.path.match(/-[a-f0-9]+\./)) {
            return CachePresets.immutable;
          }
          // 일반 정적 파일
          return CachePresets.longLived;
        }

        return CachePresets.noCache;
      },
    },
  },
});

API κ²½λ‘œλ³„ 캐싱

GET μš”μ²­λ§Œ μ„ νƒμ μœΌλ‘œ μΊμ‹œ:
export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        if (req.type === "api") {
          // GET μš”μ²­λ§Œ 캐싱 κ³ λ €
          if (req.method === "GET") {
            // νŠΉμ • κ²½λ‘œλŠ” 짧은 μΊμ‹œ
            if (req.path.startsWith("/api/static-data")) {
              return CachePresets.shortLived;
            }

            // μ•½κ΄€ 등은 쀑간 μΊμ‹œ
            if (req.path.startsWith("/api/terms")) {
              return CachePresets.mediumLived;
            }
          }

          // κΈ°λ³Έ: μΊμ‹œ μ—†μŒ
          return CachePresets.noCache;
        }
      },
    },
  },
});

μ‹€μ „ μ˜ˆμ‹œ

ν‘œμ€€ μ›Ή μ•±

import { defineConfig, CachePresets } from "sonamu";

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        switch (req.type) {
          case "assets":
            // ν•΄μ‹œ 포함 파일: 영ꡬ μΊμ‹œ
            if (req.path.match(/-[a-f0-9]+\./)) {
              return CachePresets.immutable;
            }
            return CachePresets.longLived;

          case "api":
            // GET μš”μ²­λ§Œ 캐싱
            if (req.method === "GET") {
              // 정적 데이터
              if (req.path.startsWith("/api/static")) {
                return CachePresets.mediumLived;
              }
            }
            return CachePresets.noCache;

          case "ssr":
            // SSR νŽ˜μ΄μ§€: 10초 μΊμ‹œ
            return CachePresets.ssr;

          case "csr":
            // index.html: 1λΆ„ μΊμ‹œ
            return CachePresets.shortLived;
        }
      },
    },
  },
});

CDN μ΅œμ ν™”

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        switch (req.type) {
          case "assets":
            // CDN 엣지에 였래 μΊμ‹œ
            if (req.path.match(/-[a-f0-9]+\./)) {
              return CachePresets.immutable;
            }
            return CachePresets.longLived;

          case "api":
            // APIλŠ” CDN μΊμ‹œ μ•ˆ 함
            return CachePresets.noCache;

          case "ssr":
            // SSR은 CDN에 짧게 μΊμ‹œ
            return "public, max-age=30, stale-while-revalidate=120";

          case "csr":
            return CachePresets.shortLived;
        }
      },
    },
  },
});

개발 vs ν”„λ‘œλ•μ…˜

const isDev = process.env.NODE_ENV === "development";

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        // 개발 ν™˜κ²½: μΊμ‹œ λΉ„ν™œμ„±ν™”
        if (isDev) {
          return CachePresets.noCache;
        }

        // ν”„λ‘œλ•μ…˜: 일반적인 캐싱 μ „λž΅
        switch (req.type) {
          case "assets":
            if (req.path.match(/-[a-f0-9]+\./)) {
              return CachePresets.immutable;
            }
            return CachePresets.longLived;

          case "api":
            if (req.method === "GET") {
              return CachePresets.shortLived;
            }
            return CachePresets.noCache;

          case "ssr":
            return CachePresets.ssr;

          case "csr":
            return CachePresets.shortLived;
        }
      },
    },
  },
});

μ»€μŠ€ν…€ Cache-Control

Preset이 μ•„λ‹Œ 직접 헀더 값을 λ°˜ν™˜ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€:
export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        if (req.type === "api" && req.path.startsWith("/api/feed")) {
          // ν”Όλ“œλŠ” 5λΆ„ μΊμ‹œ + stale-while-revalidate
          return "public, max-age=300, stale-while-revalidate=600";
        }

        if (req.type === "assets" && req.path.endsWith(".woff2")) {
          // ν°νŠΈλŠ” 1λ…„ μΊμ‹œ
          return "public, max-age=31536000, immutable";
        }

        return CachePresets.noCache;
      },
    },
  },
});

Cache-Control μ΄ν•΄ν•˜κΈ°

μ£Όμš” λ””λ ‰ν‹°λΈŒ

public
  • CDNκ³Ό 쀑간 ν”„λ‘μ‹œμ—μ„œλ„ μΊμ‹œ κ°€λŠ₯
  • 정적 파일, 곡개 API에 적합
private
  • λΈŒλΌμš°μ €λ§Œ μΊμ‹œ κ°€λŠ₯
  • κ°œμΈν™”λœ 데이터에 적합
no-cache
  • μΊμ‹œλŠ” ν•˜λ˜ 맀번 원본 검증 ν•„μš”
  • 쑰건뢀 μš”μ²­ (304 Not Modified) κ°€λŠ₯
no-store
  • 어디에도 μΊμ‹œν•˜μ§€ μ•ŠμŒ
  • λ―Όκ°ν•œ 데이터에 μ‚¬μš©
max-age=N
  • N초 λ™μ•ˆ μ‹ μ„ ν•œ(fresh) μƒνƒœ
  • 이 κΈ°κ°„ λ™μ•ˆμ€ μž¬κ²€μ¦ 없이 μΊμ‹œ μ‚¬μš©
immutable
  • μ ˆλŒ€ λ³€κ²½λ˜μ§€ μ•ŠμŒ
  • λΈŒλΌμš°μ €κ°€ μž¬κ²€μ¦μ„ κ±΄λ„ˆλœ€
  • ν•΄μ‹œ 기반 νŒŒμΌμ— 졜적
stale-while-revalidate=N
  • 만료 ν›„ N초 λ™μ•ˆ stale 버전 λ°˜ν™˜
  • λ°±κ·ΈλΌμš΄λ“œμ—μ„œ κ°±μ‹ 
  • SSR νŽ˜μ΄μ§€μ— 유용

μ˜ˆμ‹œ μ‘°ν•©

// 영ꡬ μΊμ‹œ (λΉŒλ“œ 파일)
"public, max-age=31536000, immutable";

// 짧은 μΊμ‹œ + λ°±κ·ΈλΌμš΄λ“œ κ°±μ‹  (SSR)
"public, max-age=10, stale-while-revalidate=60";

// λΈŒλΌμš°μ €λ§Œ 짧게 μΊμ‹œ (인증 API)
"private, max-age=60";

// μΊμ‹œ μ•ˆ 함 (μ‹€μ‹œκ°„ 데이터)
"no-cache, no-store, must-revalidate";

undefined λ°˜ν™˜

헀더λ₯Ό μ„€μ •ν•˜μ§€ μ•ŠμœΌλ €λ©΄ undefinedλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€:
export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        // λ‚΄λΆ€ APIλŠ” Cache-Control 헀더 μ—†μŒ
        if (req.path.startsWith("/api/internal")) {
          return undefined;
        }

        return CachePresets.noCache;
      },
    },
  },
});

μ£Όμ˜μ‚¬ν•­

1. 인증이 ν•„μš”ν•œ API

// ❌ λ‚˜μœ 예: 인증 APIλ₯Ό public μΊμ‹œ
if (req.path.startsWith("/api/user/profile")) {
  return "public, max-age=300"; // λ‹€λ₯Έ μ‚¬μš©μž 데이터 λ…ΈμΆœ μœ„ν—˜!
}

// βœ… 쒋은 예: private λ˜λŠ” no-cache
if (req.path.startsWith("/api/user/profile")) {
  return "private, max-age=60"; // λ˜λŠ” CachePresets.noCache
}

2. POST/PUT/DELETEλŠ” μΊμ‹œ μ•ˆ 함

if (req.type === "api") {
  // GET만 캐싱
  if (req.method === "GET") {
    return CachePresets.shortLived;
  }
  // POST, PUT, DELETEλŠ” 항상 no-cache
  return CachePresets.noCache;
}

3. ν•΄μ‹œ 기반 파일 감지

// 일반적인 λΉŒλ“œ λ„κ΅¬μ˜ ν•΄μ‹œ νŒ¨ν„΄
const hashPatterns = [
  /-[a-f0-9]{8,}\./, // Vite: main-a3f2b1c4.js
  /\.[a-f0-9]{20}\./, // Webpack: main.a3f2b1c4d5e6f7a8b9c0.js
];

if (req.type === "assets") {
  if (hashPatterns.some((p) => p.test(req.path))) {
    return CachePresets.immutable;
  }
}

λ‹€μŒ 단계

Cache-Control 섀정을 μ™„λ£Œν–ˆλ‹€λ©΄:
  • cache - μ„œλ²„ μ‚¬μ΄λ“œ 캐싱 (BentoCache)
  • compress - 응닡 μ••μΆ• μ„€μ •
  • advanced-features/ssr - SSRμ—μ„œ Cache-Control ν™œμš©