@api 데코레이터에 cacheControl 옵션을 추가하여 API 응답에 Cache-Control 헤더를 설정할 수 있습니다.
기본 사용법
@api 데코레이터에 추가
복사
import { BaseModel, api, CachePresets } from "sonamu";
class ProductModelClass extends BaseModel {
@api({
httpMethod: 'GET',
cacheControl: CachePresets.shortLived, // 1분 캐시
})
async findAll() {
return this.findMany({});
}
}
복사
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Type: application/json
[{"id": 1, "name": "Product A"}, ...]
설정 방법
- 프리셋 사용
- 커스텀 설정
- 전역 핸들러
가장 간단한 방법사용 가능한 프리셋:
복사
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived, // 5분 캐시
})
async getCategories() {
return this.categories;
}
noStore: 캐시 금지noCache: 매번 재검증shortLived: 1분mediumLived: 5분longLived: 1시간private: 개인화 데이터
세밀한 제어가 필요할 때
복사
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public',
maxAge: 120, // 2분
staleWhileRevalidate: 300, // 5분간 Stale 사용
}
})
async getData() {
return this.findMany({});
}
모든 API에 일괄 적용
복사
// sonamu.config.ts
export const config: SonamuConfig = {
server: {
apiConfig: {
cacheControlHandler: (req) => {
if (req.type !== 'api') return undefined;
// GET 요청만 캐싱
if (req.method === 'GET') {
return CachePresets.shortLived;
}
// POST, PUT, DELETE는 no-store
return CachePresets.noStore;
}
}
}
};
우선순위
Cache-Control 설정은 다음 순서로 적용됩니다:- @api 데코레이터의 cacheControl (가장 우선)
- cacheControlHandler 반환값
- 기본값 (Cache-Control 헤더 없음)
예시
복사
// sonamu.config.ts
cacheControlHandler: (req) => {
if (req.type === 'api' && req.method === 'GET') {
return CachePresets.shortLived; // 기본 1분
}
}
// Model
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived, // 5분으로 오버라이드
})
async getData() { ... }
mediumLived 적용 (데코레이터가 우선)
HTTP 메서드별 전략
GET 요청
조회 API는 캐싱 가능합니다.- 자주 변경
- 가끔 변경
- 거의 변경 안됨
복사
// 게시글 목록 (자주 추가됨)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.shortLived, // 1분
})
async getPosts(page: number) {
return this.findMany({ page, num: 20 });
}
복사
// 상품 정보 (가끔 수정)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived, // 5분
})
async getProduct(id: number) {
return this.findOne(['id', id]);
}
복사
// 카테고리 (거의 변경 안됨)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.longLived, // 1시간
})
async getCategories() {
return this.categories;
}
POST, PUT, DELETE 요청
Mutation 요청은 캐싱하면 안 됩니다.복사
// ✅ 올바른 예
@api({
httpMethod: 'POST',
cacheControl: CachePresets.noStore, // 캐시 금지
})
async create(data: ProductSave) {
return this.saveOne(data);
}
@api({
httpMethod: 'PUT',
cacheControl: CachePresets.noStore,
})
async update(id: number, data: Partial<ProductSave>) {
return this.updateOne(['id', id], data);
}
@api({
httpMethod: 'DELETE',
cacheControl: CachePresets.noStore,
})
async delete(id: number) {
return this.deleteOne(['id', id]);
}
Mutation 요청에 캐싱을 설정하면 안 되는 이유:
- 같은 POST 요청을 여러 번 보내면 중복 생성 발생
- 브라우저가 이전 응답을 재사용 → 실제로는 생성되지 않은 것처럼 보임
복사
// ❌ 절대 이렇게 하지 마세요
@api({
httpMethod: 'POST',
cacheControl: { maxAge: 60 }, // 위험!
})
async create(data: ProductSave) { ... }
데이터 특성별 전략
공개 데이터 (public)
모든 사용자에게 동일한 응답입니다.복사
class ProductModelClass extends BaseModel {
// 상품 목록
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public', // CDN 캐싱 가능
maxAge: 60,
}
})
async findAll() {
return this.findMany({});
}
}
- 브라우저 + CDN에서 캐싱
- 네트워크 트래픽 최소화
- 서버 부하 감소
개인 데이터 (private)
사용자별로 다른 응답입니다.복사
class OrderModelClass extends BaseModel {
// 내 주문 내역
@api({
httpMethod: 'GET',
cacheControl: CachePresets.private, // 브라우저만 캐싱
})
async getMyOrders(ctx: Context) {
return this.findMany({
where: [['user_id', ctx.user.id]]
});
}
}
- 브라우저에만 캐싱 (CDN 불가)
- 다른 사용자에게 노출 방지
민감한 데이터 (no-store)
캐싱하면 안 되는 데이터입니다.복사
class PaymentModelClass extends BaseModel {
// 결제 정보
@api({
httpMethod: 'GET',
cacheControl: CachePresets.noStore, // 캐시 금지
})
async getPaymentInfo(id: number) {
return this.findOne(['id', id]);
}
}
실전 예제
1. 전자상거래 API
복사
class ProductModelClass extends BaseModel {
// 상품 목록 (자주 변경, public)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.shortLived, // 1분
})
async findAll(page: number) {
return this.findMany({ page, num: 20 });
}
// 상품 상세 (가끔 변경, public)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived, // 5분
})
async findById(id: number) {
return this.findOne(['id', id]);
}
// 카테고리 (거의 안 변함, public)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.longLived, // 1시간
})
async getCategories() {
return this.categories;
}
// 재고 (실시간, no-cache)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.noCache, // 매번 재검증
})
async getStock(productId: number) {
return this.checkStock(productId);
}
// 장바구니 (개인, private)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.private, // 브라우저만
})
async getMyCart(ctx: Context) {
return this.getCart(ctx.user.id);
}
// 주문 생성 (Mutation, no-store)
@api({
httpMethod: 'POST',
cacheControl: CachePresets.noStore, // 캐시 금지
})
async createOrder(data: OrderSave) {
return this.saveOne(data);
}
}
2. 블로그 API
복사
class PostModelClass extends BaseModel {
// 게시글 목록 (자주 추가, 5분)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived,
})
async findAll(page: number) {
return this.findMany({ page, num: 10 });
}
// 게시글 상세 (가끔 수정, 10분)
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public',
maxAge: 600, // 10분
staleWhileRevalidate: 1800, // 30분간 Stale 사용
}
})
async findById(id: number) {
return this.findOne(['id', id]);
}
// 인기 게시글 (1시간)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.longLived,
})
async getPopular() {
return this.findPopular();
}
}
3. 사용자 API
복사
class UserModelClass extends BaseModel {
// 공개 프로필 (public, 5분)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.mediumLived,
})
async getPublicProfile(username: string) {
return this.findOne(['username', username]);
}
// 내 프로필 (private, 재검증)
@api({
httpMethod: 'GET',
cacheControl: CachePresets.private,
})
async getMyProfile(ctx: Context) {
return this.findOne(['id', ctx.user.id]);
}
// 프로필 업데이트 (no-store)
@api({
httpMethod: 'PUT',
cacheControl: CachePresets.noStore,
})
async updateProfile(ctx: Context, data: Partial<UserSave>) {
return this.updateOne(['id', ctx.user.id], data);
}
}
CDN 최적화
s-maxage 활용
브라우저와 CDN에 다른 TTL을 적용할 수 있습니다.복사
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public',
maxAge: 60, // 브라우저: 1분
sMaxAge: 300, // CDN: 5분
}
})
async getData() {
return this.findMany({});
}
- 브라우저: 1분마다 CDN에 요청
- CDN: 5분마다 서버에 요청
- 서버 부하 최소화
Stale-While-Revalidate
CDN에서 Stale 응답을 즉시 반환하면서 백그라운드 갱신:복사
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public',
maxAge: 60,
staleWhileRevalidate: 300, // 5분간 Stale 허용
}
})
async getProducts() {
return this.findMany({});
}
stale-while-revalidate를 지원합니다.
Vary 헤더
요청 헤더에 따라 다른 캐시를 사용합니다.복사
@api({
httpMethod: 'GET',
cacheControl: {
visibility: 'public',
maxAge: 300,
vary: ['Accept-Language'], // 언어별 캐시 분리
}
})
async getProducts(ctx: Context) {
const locale = ctx.locale; // 'ko' 또는 'en'
return this.getLocalizedProducts(locale);
}
복사
GET /api/products (Accept-Language: ko) → 한국어 응답 캐싱
GET /api/products (Accept-Language: en) → 영어 응답 캐싱
전역 핸들러
모든 API에 일괄적으로 Cache-Control을 적용할 수 있습니다.복사
// sonamu.config.ts
import { CachePresets, type SonamuConfig } from "sonamu";
export const config: SonamuConfig = {
server: {
apiConfig: {
cacheControlHandler: (req) => {
// API 요청만 처리
if (req.type !== 'api') {
return undefined;
}
// HTTP 메서드별 처리
if (req.method !== 'GET') {
return CachePresets.noStore; // Mutation은 no-store
}
// API 경로별 처리
if (req.path.includes('/admin/')) {
return CachePresets.noStore; // 관리자 API는 no-store
}
if (req.path.includes('/private/')) {
return CachePresets.private; // 개인 API는 private
}
// 기본값: 1분 캐시
return CachePresets.shortLived;
}
}
}
};
API 객체 활용
복사
cacheControlHandler: (req) => {
if (req.type !== 'api') return undefined;
// ExtendedApi 정보 활용
if (req.api) {
const { modelName, methodName } = req.api;
// 특정 Model만 긴 캐시
if (modelName === 'ConfigModel') {
return CachePresets.longLived;
}
// 특정 메서드는 캐시 금지
if (methodName.startsWith('create') || methodName.startsWith('update')) {
return CachePresets.noStore;
}
}
return CachePresets.shortLived;
}
BentoCache와 함께 사용
서버 캐시와 HTTP 캐시를 조합하면 최대 성능을 얻을 수 있습니다.복사
@cache({ ttl: "10m", tags: ["product"] }) // 서버 캐시: 10분
@api({
httpMethod: 'GET',
cacheControl: { maxAge: 60 } // HTTP 캐시: 1분
})
async getProducts() {
return this.findMany({});
}
- 0~60초: 브라우저 캐시 (서버 요청 없음)
- 60초~10분: 서버 요청하지만 BentoCache 사용 (DB 조회 없음)
- 10분 이후: DB 조회 → 다시 캐싱
주의사항
API Cache-Control 사용 시 주의사항:
-
GET만 캐싱: POST, PUT, DELETE는 반드시
noStore사용복사// ❌ 위험 @api({ httpMethod: 'POST', cacheControl: { maxAge: 60 } }) // ✅ 안전 @api({ httpMethod: 'POST', cacheControl: CachePresets.noStore }) -
개인 데이터는 private: CDN 캐싱 방지
복사
// ❌ 위험: CDN에 개인 데이터 캐싱 @api({ cacheControl: { visibility: 'public', maxAge: 60 } }) async getMyOrders(ctx: Context) { ... } // ✅ 안전 @api({ cacheControl: CachePresets.private }) async getMyOrders(ctx: Context) { ... } -
자주 변경되는 데이터는 짧은 TTL: 오래된 데이터 방지
복사
// ❌ 재고가 실시간인데 1시간 캐싱 @api({ cacheControl: CachePresets.longLived }) async getStock() { ... } // ✅ 짧은 TTL 또는 no-cache @api({ cacheControl: CachePresets.noCache }) async getStock() { ... } -
인증 필요한 API는 조심: Authorization 헤더 고려
복사
@api({ cacheControl: { visibility: 'private', // private 필수 maxAge: 60, vary: ['Authorization'], // 인증 토큰별 캐시 분리 } }) async getMyData(ctx: Context) { ... }
