메인 콘텐츠로 건너뛰기
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
    

다음 단계