๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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 ํ™œ์šฉ