> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cache Presets

> 사전 정의된 캐시 설정 활용하기

Sonamu는 자주 사용하는 Cache-Control 패턴을 **CachePresets**으로 제공합니다. 직접 설정을 작성하는 대신 프리셋을 사용하면 일관되고 검증된 캐시 전략을 쉽게 적용할 수 있습니다.

## CachePresets 개요

```typescript theme={null}
import { CachePresets } from "sonamu";

// API에서 사용
@api({
  httpMethod: 'GET',
  cacheControl: CachePresets.shortLived,
})

// SSR에서 사용
registerSSR({
  path: '/products',
  cacheControl: CachePresets.ssr,
});
```

## 전체 프리셋 목록

<CardGroup cols={2}>
  <Card title="noStore" icon="ban">
    캐시 저장 금지
  </Card>

  <Card title="noCache" icon="rotate">
    매번 재검증
  </Card>

  <Card title="shortLived" icon="clock">
    1분 캐시
  </Card>

  <Card title="ssr" icon="window">
    SSR 최적화 (10초 + SWR)
  </Card>

  <Card title="mediumLived" icon="hourglass-half">
    5분 캐시
  </Card>

  <Card title="longLived" icon="hourglass">
    1시간 캐시
  </Card>

  <Card title="immutable" icon="lock">
    영구 캐시 (정적 파일)
  </Card>

  <Card title="private" icon="user-lock">
    개인화 데이터
  </Card>
</CardGroup>

## 프리셋 상세

### noStore

**캐시 저장을 완전히 금지**합니다.

```typescript theme={null}
CachePresets.noStore
// 생성되는 헤더: Cache-Control: no-store
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      noStore: true
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 민감한 데이터 (개인정보, 결제 정보)
    * Mutation 요청 (POST, PUT, DELETE)
    * 실시간 데이터 (주식 가격, 경매)
    * 일회용 토큰, OTP
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 결제 API
    @api({
      httpMethod: 'POST',
      cacheControl: CachePresets.noStore,
    })
    async createPayment(data: PaymentSave) {
      return this.processPayment(data);
    }

    // 개인 정보 조회
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.noStore,
    })
    async getPrivateData(ctx: Context) {
      return this.getSensitiveData(ctx.user.id);
    }
    ```
  </Tab>
</Tabs>

### noCache

**캐시는 저장하되 매번 재검증**합니다.

```typescript theme={null}
CachePresets.noCache
// 생성되는 헤더: Cache-Control: no-cache
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      noCache: true
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 최신 상태 확인이 중요한 데이터
    * 자주 변경되지만 서버 부하는 줄이고 싶을 때
    * 조건부 요청 (ETag, Last-Modified) 활용
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 재고 정보 (자주 변경)
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.noCache,
    })
    async getInventory(productId: number) {
      return this.checkStock(productId);
    }
    ```
  </Tab>
</Tabs>

**no-cache의 장점**:

```
1차 요청: GET /api/data → 200 OK + ETag: "abc123"
2차 요청: GET /api/data (If-None-Match: "abc123")
→ 304 Not Modified (body 없음, 빠름)
```

### shortLived

**1분 캐시** - 자주 변경되는 데이터에 적합합니다.

```typescript theme={null}
CachePresets.shortLived
// 생성되는 헤더: Cache-Control: public, max-age=60
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'public',
      maxAge: 60  // 60초 = 1분
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 자주 변경되는 API 응답
    * 실시간성이 중요하지 않은 데이터
    * CSR fallback 페이지
    * 게시글 목록
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 게시글 목록
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.shortLived,
    })
    async getPosts(page: number) {
      return this.findMany({ page, num: 20 });
    }

    // CSR fallback
    registerSSR({
      path: '/*',  // catch-all
      cacheControl: CachePresets.shortLived,
    });
    ```
  </Tab>
</Tabs>

### ssr

**SSR 페이지 최적화** - 10초 캐시 + Stale-While-Revalidate 30초

```typescript theme={null}
CachePresets.ssr
// 생성되는 헤더: Cache-Control: public, max-age=10, stale-while-revalidate=30
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'public',
      maxAge: 10,  // 10초 동안 Fresh
      staleWhileRevalidate: 30  // 이후 30초 동안 Stale 사용
    }
    ```
  </Tab>

  <Tab title="작동 방식" icon="diagram-project">
    **0\~10초**: 신선한 캐시

    * 사용자: 캐시에서 즉시 응답

    **10\~40초**: Stale 상태

    * 사용자: Stale 캐시 즉시 반환
    * CDN: 백그라운드에서 서버 요청 → 캐시 갱신

    **40초 이후**: 만료

    * 서버 요청 필요
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * SSR 페이지 (상품 상세, 블로그 글)
    * 동적 콘텐츠이지만 약간의 지연 허용
    * CDN 효율 극대화
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 상품 상세 페이지
    registerSSR({
      path: '/products/:id',
      cacheControl: CachePresets.ssr,
      Component: ProductDetail,
    });

    // 블로그 글
    registerSSR({
      path: '/blog/:slug',
      cacheControl: CachePresets.ssr,
      Component: BlogPost,
    });
    ```
  </Tab>
</Tabs>

### mediumLived

**5분 캐시** - 거의 변경되지 않는 데이터에 적합합니다.

```typescript theme={null}
CachePresets.mediumLived
// 생성되는 헤더: Cache-Control: public, max-age=300
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'public',
      maxAge: 300  // 300초 = 5분
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 설정 데이터 (약관, 정책)
    * 카테고리 목록
    * 통계 데이터 (일 단위 갱신)
    * FAQ, 공지사항
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 카테고리 트리
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.mediumLived,
    })
    async getCategoryTree() {
      return this.buildTree();
    }

    // 약관
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.mediumLived,
    })
    async getTerms() {
      return this.findOne(['type', 'terms']);
    }
    ```
  </Tab>
</Tabs>

### longLived

**1시간 캐시** - 정적 컨텐츠에 적합합니다.

```typescript theme={null}
CachePresets.longLived
// 생성되는 헤더: Cache-Control: public, max-age=3600
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'public',
      maxAge: 3600  // 3600초 = 1시간
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 거의 변경되지 않는 정적 콘텐츠
    * 이미지, 폰트 (해시 없는 파일)
    * 참조 데이터 (국가 목록, 통화 코드)
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 국가 목록
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.longLived,
    })
    async getCountries() {
      return this.findMany({});
    }

    // 정적 설정
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.longLived,
    })
    async getConfig() {
      return this.config;
    }
    ```
  </Tab>
</Tabs>

### immutable

**영구 캐시** - 해시가 포함된 정적 파일용

```typescript theme={null}
CachePresets.immutable
// 생성되는 헤더: Cache-Control: public, max-age=31536000, immutable
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'public',
      maxAge: 31536000,  // 1년 (초 단위)
      immutable: true  // 절대 변경 안됨
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 해시가 포함된 파일명
      * `bundle-abc123.js`
      * `styles-def456.css`
      * `image-789xyz.png`
    * 내용이 변경되면 파일명도 변경되는 경우
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 빌드된 정적 파일
    server.get('/assets/:filename', (req, reply) => {
      const filename = req.params.filename;
      
      // 해시 포함 파일은 immutable
      if (/\-[a-f0-9]{8,}\.(js|css|png)$/.test(filename)) {
        applyCacheHeaders(reply, CachePresets.immutable);
      }
      
      return reply.sendFile(filename);
    });
    ```
  </Tab>
</Tabs>

**immutable의 장점**:

* 재검증 없이 캐시만 사용 (가장 빠름)
* 파일이 변경되면 새 해시 → 새 파일명 → 자동 캐시 갱신

### private

**개인화 데이터** - 사용자별로 다른 응답

```typescript theme={null}
CachePresets.private
// 생성되는 헤더: Cache-Control: private, no-cache
```

<Tabs>
  <Tab title="설정" icon="gear">
    ```typescript theme={null}
    {
      visibility: 'private',
      noCache: true
    }
    ```
  </Tab>

  <Tab title="사용 시기" icon="lightbulb">
    * 로그인 사용자별 데이터
    * 마이페이지, 프로필
    * 장바구니, 주문 내역
    * 맞춤 추천
  </Tab>

  <Tab title="예제" icon="code">
    ```typescript theme={null}
    // 내 프로필
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.private,
    })
    async getMyProfile(ctx: Context) {
      return this.findOne(['id', ctx.user.id]);
    }

    // 내 주문 목록
    @api({
      httpMethod: 'GET',
      cacheControl: CachePresets.private,
    })
    async getMyOrders(ctx: Context) {
      return this.findMany({ 
        where: [['user_id', ctx.user.id]] 
      });
    }
    ```
  </Tab>
</Tabs>

**private vs public**:

* `private`: 브라우저에만 캐싱 (CDN에는 안 됨)
* `public`: 모든 캐시(브라우저 + CDN)에 캐싱

## 프리셋 비교표

| 프리셋           | 헤더                                              | 용도                | TTL           |
| ------------- | ----------------------------------------------- | ----------------- | ------------- |
| `noStore`     | `no-store`                                      | 민감한 데이터, Mutation | 없음            |
| `noCache`     | `no-cache`                                      | 매번 재검증 필요         | 없음 (재검증)      |
| `shortLived`  | `public, max-age=60`                            | 자주 변경             | 1분            |
| `ssr`         | `public, max-age=10, stale-while-revalidate=30` | SSR 페이지           | 10초 + SWR 30초 |
| `mediumLived` | `public, max-age=300`                           | 거의 변경 안됨          | 5분            |
| `longLived`   | `public, max-age=3600`                          | 정적 콘텐츠            | 1시간           |
| `immutable`   | `public, max-age=31536000, immutable`           | 해시 파일             | 1년 (영구)       |
| `private`     | `private, no-cache`                             | 개인화 데이터           | 없음 (private)  |

## 커스텀 설정

프리셋이 맞지 않으면 직접 설정을 작성할 수 있습니다.

```typescript theme={null}
// 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을 설정할 수 있습니다.

```typescript theme={null}
// 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별 전략

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  // 목록: 자주 변경 → 짧은 캐시
  @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 페이지

```typescript theme={null}
// 홈페이지: SSR 최적화
registerSSR({
  path: '/',
  cacheControl: CachePresets.ssr,
  Component: HomePage,
});

// 상품 목록: 짧은 캐시
registerSSR({
  path: '/products',
  cacheControl: CachePresets.shortLived,
  Component: ProductList,
});

// 약관: 긴 캐시
registerSSR({
  path: '/terms',
  cacheControl: CachePresets.longLived,
  Component: Terms,
});
```

## 주의사항

<Warning>
  **프리셋 사용 시 주의사항**:

  1. **프리셋은 출발점**: 상황에 맞게 커스터마이징 필요
     ```typescript theme={null}
     // ✅ 상황에 맞게 조정
     cacheControl: {
       ...CachePresets.shortLived,
       maxAge: 120,  // 1분 → 2분으로 조정
     }
     ```

  2. **GET만 캐싱**: POST, PUT, DELETE는 `noStore` 사용
     ```typescript theme={null}
     // ❌ 잘못된 예
     @api({
       httpMethod: 'POST',
       cacheControl: CachePresets.shortLived,
     })

     // ✅ 올바른 예
     @api({
       httpMethod: 'POST',
       cacheControl: CachePresets.noStore,
     })
     ```

  3. **개인 데이터는 private**: CDN 캐싱 방지
     ```typescript theme={null}
     // ❌ 위험: CDN에 개인 데이터 캐싱
     @api({
       cacheControl: CachePresets.shortLived,  // public
     })
     async getMyData() { ... }

     // ✅ 안전
     @api({
       cacheControl: CachePresets.private,
     })
     async getMyData() { ... }
     ```

  4. **immutable은 해시 파일만**: 파일명이 바뀌어야 갱신됨
     ```typescript theme={null}
     // ❌ 잘못된 사용
     // main.js (해시 없음) → 내용 변경해도 캐시 유지
     cacheControl: CachePresets.immutable

     // ✅ 올바른 사용
     // main-abc123.js (해시 있음) → 내용 변경 시 파일명도 변경
     cacheControl: CachePresets.immutable
     ```
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="Cache-Control이란?" icon="circle-question" href="/ko/advanced-features/cache-control/what-is-cache-control">
    HTTP 캐싱 기본 개념
  </Card>

  <Card title="API에서 사용하기" icon="code" href="/ko/advanced-features/cache-control/using-in-apis">
    @api 데코레이터에 적용
  </Card>

  <Card title="SSR에서 사용하기" icon="window" href="/ko/advanced-features/cache-control/using-in-ssr">
    SSR 페이지에 적용
  </Card>
</CardGroup>
