๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” Cache-Control ํŒจํ„ด์„ CachePresets์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ง์ ‘ ์„ค์ •์„ ์ž‘์„ฑํ•˜๋Š” ๋Œ€์‹  ํ”„๋ฆฌ์…‹์„ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๊ด€๋˜๊ณ  ๊ฒ€์ฆ๋œ ์บ์‹œ ์ „๋žต์„ ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CachePresets ๊ฐœ์š”

import { CachePresets } from "sonamu";

// API์—์„œ ์‚ฌ์šฉ
@api({
  httpMethod: 'GET',
  cacheControl: CachePresets.shortLived,
})

// SSR์—์„œ ์‚ฌ์šฉ
registerSSR({
  path: '/products',
  cacheControl: CachePresets.ssr,
});

์ „์ฒด ํ”„๋ฆฌ์…‹ ๋ชฉ๋ก

noStore

์บ์‹œ ์ €์žฅ ๊ธˆ์ง€

noCache

๋งค๋ฒˆ ์žฌ๊ฒ€์ฆ

shortLived

1๋ถ„ ์บ์‹œ

ssr

SSR ์ตœ์ ํ™” (10์ดˆ + SWR)

mediumLived

5๋ถ„ ์บ์‹œ

longLived

1์‹œ๊ฐ„ ์บ์‹œ

immutable

์˜๊ตฌ ์บ์‹œ (์ •์  ํŒŒ์ผ)

private

๊ฐœ์ธํ™” ๋ฐ์ดํ„ฐ

ํ”„๋ฆฌ์…‹ ์ƒ์„ธ

noStore

์บ์‹œ ์ €์žฅ์„ ์™„์ „ํžˆ ๊ธˆ์ง€ํ•ฉ๋‹ˆ๋‹ค.
CachePresets.noStore
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: no-store
{
  noStore: true
}

noCache

์บ์‹œ๋Š” ์ €์žฅํ•˜๋˜ ๋งค๋ฒˆ ์žฌ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
CachePresets.noCache
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: no-cache
{
  noCache: true
}
no-cache์˜ ์žฅ์ :
1์ฐจ ์š”์ฒญ: GET /api/data โ†’ 200 OK + ETag: "abc123"
2์ฐจ ์š”์ฒญ: GET /api/data (If-None-Match: "abc123")
โ†’ 304 Not Modified (body ์—†์Œ, ๋น ๋ฆ„)

shortLived

1๋ถ„ ์บ์‹œ - ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฐ์ดํ„ฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
CachePresets.shortLived
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: public, max-age=60
{
  visibility: 'public',
  maxAge: 60  // 60์ดˆ = 1๋ถ„
}

ssr

SSR ํŽ˜์ด์ง€ ์ตœ์ ํ™” - 10์ดˆ ์บ์‹œ + Stale-While-Revalidate 30์ดˆ
CachePresets.ssr
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: public, max-age=10, stale-while-revalidate=30
{
  visibility: 'public',
  maxAge: 10,  // 10์ดˆ ๋™์•ˆ Fresh
  staleWhileRevalidate: 30  // ์ดํ›„ 30์ดˆ ๋™์•ˆ Stale ์‚ฌ์šฉ
}

mediumLived

5๋ถ„ ์บ์‹œ - ๊ฑฐ์˜ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
CachePresets.mediumLived
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: public, max-age=300
{
  visibility: 'public',
  maxAge: 300  // 300์ดˆ = 5๋ถ„
}

longLived

1์‹œ๊ฐ„ ์บ์‹œ - ์ •์  ์ปจํ…์ธ ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
CachePresets.longLived
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: public, max-age=3600
{
  visibility: 'public',
  maxAge: 3600  // 3600์ดˆ = 1์‹œ๊ฐ„
}

immutable

์˜๊ตฌ ์บ์‹œ - ํ•ด์‹œ๊ฐ€ ํฌํ•จ๋œ ์ •์  ํŒŒ์ผ์šฉ
CachePresets.immutable
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: public, max-age=31536000, immutable
{
  visibility: 'public',
  maxAge: 31536000,  // 1๋…„ (์ดˆ ๋‹จ์œ„)
  immutable: true  // ์ ˆ๋Œ€ ๋ณ€๊ฒฝ ์•ˆ๋จ
}
immutable์˜ ์žฅ์ :
  • ์žฌ๊ฒ€์ฆ ์—†์ด ์บ์‹œ๋งŒ ์‚ฌ์šฉ (๊ฐ€์žฅ ๋น ๋ฆ„)
  • ํŒŒ์ผ์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ƒˆ ํ•ด์‹œ โ†’ ์ƒˆ ํŒŒ์ผ๋ช… โ†’ ์ž๋™ ์บ์‹œ ๊ฐฑ์‹ 

private

๊ฐœ์ธํ™” ๋ฐ์ดํ„ฐ - ์‚ฌ์šฉ์ž๋ณ„๋กœ ๋‹ค๋ฅธ ์‘๋‹ต
CachePresets.private
// ์ƒ์„ฑ๋˜๋Š” ํ—ค๋”: Cache-Control: private, no-cache
{
  visibility: 'private',
  noCache: true
}
private vs public:
  • private: ๋ธŒ๋ผ์šฐ์ €์—๋งŒ ์บ์‹ฑ (CDN์—๋Š” ์•ˆ ๋จ)
  • public: ๋ชจ๋“  ์บ์‹œ(๋ธŒ๋ผ์šฐ์ € + CDN)์— ์บ์‹ฑ

ํ”„๋ฆฌ์…‹ ๋น„๊ตํ‘œ

ํ”„๋ฆฌ์…‹ํ—ค๋”์šฉ๋„TTL
noStoreno-store๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ, Mutation์—†์Œ
noCacheno-cache๋งค๋ฒˆ ์žฌ๊ฒ€์ฆ ํ•„์š”์—†์Œ (์žฌ๊ฒ€์ฆ)
shortLivedpublic, max-age=60์ž์ฃผ ๋ณ€๊ฒฝ1๋ถ„
ssrpublic, max-age=10, stale-while-revalidate=30SSR ํŽ˜์ด์ง€10์ดˆ + SWR 30์ดˆ
mediumLivedpublic, max-age=300๊ฑฐ์˜ ๋ณ€๊ฒฝ ์•ˆ๋จ5๋ถ„
longLivedpublic, max-age=3600์ •์  ์ฝ˜ํ…์ธ 1์‹œ๊ฐ„
immutablepublic, max-age=31536000, immutableํ•ด์‹œ ํŒŒ์ผ1๋…„ (์˜๊ตฌ)
privateprivate, no-cache๊ฐœ์ธํ™” ๋ฐ์ดํ„ฐ์—†์Œ (private)

์ปค์Šคํ…€ ์„ค์ •

ํ”„๋ฆฌ์…‹์ด ๋งž์ง€ ์•Š์œผ๋ฉด ์ง์ ‘ ์„ค์ •์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// CDN ์ตœ์ ํ™” (๋ธŒ๋ผ์šฐ์ €: 1๋ถ„, CDN: 5๋ถ„)
@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 60,      // ๋ธŒ๋ผ์šฐ์ €
    sMaxAge: 300,    // CDN
  }
})
async getData() {
  return this.findMany({});
}

// Stale-If-Error ์ถ”๊ฐ€
@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 300,
    staleIfError: 86400,  // ์˜ค๋ฅ˜ ์‹œ 1์ผ๊ฐ„ Stale ์‚ฌ์šฉ
  }
})
async getCriticalData() {
  return this.getData();
}

์ „์—ญ ํ•ธ๋“ค๋Ÿฌ

๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ๋™์ ์œผ๋กœ Cache-Control์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// sonamu.config.ts
export const config: SonamuConfig = {
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        // API ์š”์ฒญ
        if (req.type === 'api') {
          if (req.method !== 'GET') {
            return CachePresets.noStore;  // Mutation์€ no-store
          }
          return CachePresets.shortLived;  // GET์€ 1๋ถ„ ์บ์‹œ
        }
        
        // ์ •์  ํŒŒ์ผ
        if (req.type === 'assets') {
          if (req.path.includes('-')) {  // ํ•ด์‹œ ํฌํ•จ
            return CachePresets.immutable;
          }
          return CachePresets.longLived;
        }
        
        // SSR
        if (req.type === 'ssr') {
          return CachePresets.ssr;
        }
        
        return undefined;  // ๊ธฐ๋ณธ๊ฐ’
      }
    }
  }
};

์‹ค์ „ ํ™œ์šฉ

API๋ณ„ ์ „๋žต

class ProductModelClass extends BaseModel {
  // ๋ชฉ๋ก: ์ž์ฃผ ๋ณ€๊ฒฝ โ†’ ์งง์€ ์บ์‹œ
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.shortLived,
  })
  async findAll() {
    return this.findMany({});
  }
  
  // ์ƒ์„ธ: ๋œ ๋ณ€๊ฒฝ โ†’ ์ค‘๊ฐ„ ์บ์‹œ
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.mediumLived,
  })
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
  
  // ์นดํ…Œ๊ณ ๋ฆฌ: ๊ฑฐ์˜ ๋ณ€๊ฒฝ ์•ˆ๋จ โ†’ ๊ธด ์บ์‹œ
  @api({
    httpMethod: 'GET',
    cacheControl: CachePresets.longLived,
  })
  async getCategories() {
    return this.categories;
  }
  
  // ์ƒ์„ฑ/์ˆ˜์ •: Mutation โ†’ no-store
  @api({
    httpMethod: 'POST',
    cacheControl: CachePresets.noStore,
  })
  async create(data: ProductSave) {
    return this.saveOne(data);
  }
}

SSR ํŽ˜์ด์ง€

// ํ™ˆํŽ˜์ด์ง€: SSR ์ตœ์ ํ™”
registerSSR({
  path: '/',
  cacheControl: CachePresets.ssr,
  Component: HomePage,
});

// ์ƒํ’ˆ ๋ชฉ๋ก: ์งง์€ ์บ์‹œ
registerSSR({
  path: '/products',
  cacheControl: CachePresets.shortLived,
  Component: ProductList,
});

// ์•ฝ๊ด€: ๊ธด ์บ์‹œ
registerSSR({
  path: '/terms',
  cacheControl: CachePresets.longLived,
  Component: Terms,
});

์ฃผ์˜์‚ฌํ•ญ

ํ”„๋ฆฌ์…‹ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ํ”„๋ฆฌ์…‹์€ ์ถœ๋ฐœ์ : ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•„์š”
    // โœ… ์ƒํ™ฉ์— ๋งž๊ฒŒ ์กฐ์ •
    cacheControl: {
      ...CachePresets.shortLived,
      maxAge: 120,  // 1๋ถ„ โ†’ 2๋ถ„์œผ๋กœ ์กฐ์ •
    }
    
  2. GET๋งŒ ์บ์‹ฑ: POST, PUT, DELETE๋Š” noStore ์‚ฌ์šฉ
    // โŒ ์ž˜๋ชป๋œ ์˜ˆ
    @api({
      httpMethod: 'POST',
      cacheControl: CachePresets.shortLived,
    })
    
    // โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
    @api({
      httpMethod: 'POST',
      cacheControl: CachePresets.noStore,
    })
    
  3. ๊ฐœ์ธ ๋ฐ์ดํ„ฐ๋Š” private: CDN ์บ์‹ฑ ๋ฐฉ์ง€
    // โŒ ์œ„ํ—˜: CDN์— ๊ฐœ์ธ ๋ฐ์ดํ„ฐ ์บ์‹ฑ
    @api({
      cacheControl: CachePresets.shortLived,  // public
    })
    async getMyData() { ... }
    
    // โœ… ์•ˆ์ „
    @api({
      cacheControl: CachePresets.private,
    })
    async getMyData() { ... }
    
  4. immutable์€ ํ•ด์‹œ ํŒŒ์ผ๋งŒ: ํŒŒ์ผ๋ช…์ด ๋ฐ”๋€Œ์–ด์•ผ ๊ฐฑ์‹ ๋จ
    // โŒ ์ž˜๋ชป๋œ ์‚ฌ์šฉ
    // main.js (ํ•ด์‹œ ์—†์Œ) โ†’ ๋‚ด์šฉ ๋ณ€๊ฒฝํ•ด๋„ ์บ์‹œ ์œ ์ง€
    cacheControl: CachePresets.immutable
    
    // โœ… ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ
    // main-abc123.js (ํ•ด์‹œ ์žˆ์Œ) โ†’ ๋‚ด์šฉ ๋ณ€๊ฒฝ ์‹œ ํŒŒ์ผ๋ช…๋„ ๋ณ€๊ฒฝ
    cacheControl: CachePresets.immutable
    

๋‹ค์Œ ๋‹จ๊ณ„