Cloudflare 작업자의 캐시 API 및 무효화 전략
Workers의 Cache API를 사용하면 TTL을 세부적으로 제어할 수 있습니다. 오래된 재검증 및 키별 무효화: 빌드 방법 알아보기 고성능 글로벌 분산 캐싱 레이어.
엣지 캐싱: 기존 CDN을 넘어
기존 CDN은 표준 HTTP 헤더를 따라 정적 자산을 캐시합니다. Cloudflare 작업자 개념을 훨씬 더 발전시킵니다. 캐시 API, 귀하의 코드 TypeScript는 캐시된 내용과 기간을 정밀하게 제어합니다. 캐시가 무효화되는 방법과 캐시가 오래되었을 때 적용할 대체 논리.
결과는 하나 프로그래밍 가능한 CDN: 규칙을 구성하는 대신 대시보드에서 정적으로, 즉각적으로 결정하는 비즈니스 로직을 작성하세요. 어떤 TTL과 어떤 캐시 키를 사용하여 응답을 캐시할 수 있는지입니다. 이러한 유연성 이는 REST API, 사용자 정의 페이지 및 반동적 콘텐츠에 특히 유용합니다.
무엇을 배울 것인가
- Cloudflare Workers Cache API의 작동 방식 및 브라우저 캐시 API와의 차이점
- 캐싱 전략: 캐시 우선, 네트워크 우선, 오래된 재검증 중
- 사용자 정의 캐시 키: 사용자, 언어, 버전별로 캐시를 분리합니다.
- Cloudflare Zone Purge API를 사용하여 URL, 태그, 접두사 무효화
- 다양한 헤더: Accept-Encoding, Accept-Language를 위한 차별화된 캐시
- 고급 패턴: KV를 사용한 캐시 워밍, 유예 기간 및 회로 차단기
- 흔히 발생하는 실수와 이를 방지하는 방법
Cache API: 브라우저와의 기본 사항 및 차이점
Workers에 노출된 Cache API는 다음과 동일한 인터페이스를 따릅니다. 서비스 워커 캐시 API 브라우저이지만 중요한 차이점이 있습니다. Workers에서 캐시는 다음과 같습니다. 분산 모든 Cloudflare PoP에서: 프랑크푸르트의 작업자가 응답을 캐시하면, 그 대답은 런던이나 암스테르담에서는 자동으로 제공되지 않습니다. 모든 PoP에는 자신의 로컬 캐시.
// Accesso alla cache nel Worker
// In Workers esiste un unico "default" cache namespace
const cache = caches.default;
// Oppure cache named (isolata per nome, utile per namespace logici)
const apiCache = await caches.open('api-v2');
// Le operazioni fondamentali:
// cache.match(request) -> Response | undefined
// cache.put(request, response) -> void
// cache.delete(request) -> boolean
캐시 API: 중요한 제약조건
- HTTP 요청만(브라우저와 같은 임의의 URL은 아님)
- 다음을 사용하여 응답을 캐시할 수 없습니다.
Vary: * - 캐시는 PoP별로 이루어집니다. 다음과 같은 자동 데이터 센터 간 무효화가 없습니다.
cache.delete() - 상태 206(부분 콘텐츠)의 응답은 캐시할 수 없습니다.
- 적용되는 최대 TTL은 31일입니다.
전략 1: 대체 네트워크를 사용한 캐시 우선
데이터가 거의 변경되지 않는 API에 대한 가장 일반적인 전략: 캐시에서 제공 사용 가능한 경우 그렇지 않으면 소스로 이동하여 캐시를 채웁니다.
// worker.ts - Cache-First Strategy
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Solo richieste GET sono cacheable
if (request.method !== 'GET') {
return fetch(request);
}
const cache = caches.default;
// 1. Controlla la cache
let response = await cache.match(request);
if (response) {
// Cache HIT: aggiungi header diagnostico e restituisci
const headers = new Headers(response.headers);
headers.set('X-Cache-Status', 'HIT');
return new Response(response.body, {
status: response.status,
headers,
});
}
// 2. Cache MISS: fetch dall'origin
response = await fetch(request);
// 3. Clona la risposta (il body e un ReadableStream, consumabile una sola volta)
const responseToCache = response.clone();
// 4. Metti in cache con ctx.waitUntil() per non bloccare la risposta al client
ctx.waitUntil(
cache.put(request, responseToCache)
);
// 5. Restituisci la risposta originale con header diagnostico
const headers = new Headers(response.headers);
headers.set('X-Cache-Status', 'MISS');
return new Response(response.body, {
status: response.status,
headers,
});
},
};
interface Env {}
전략 2: 무효화 중 재검증
전략 재검증하는 동안 오래된 그것은 최선을 다하는 사람이다 데이터 신선도와 인지된 속도 사이의 절충: 클라이언트는 언제나 백그라운드에서 실행 중이더라도 캐시로부터 즉각적인 응답 작업자는 다음 요청을 위해 캐시를 업데이트합니다.
// Stale-While-Revalidate implementato manualmente
// (Cloudflare supporta anche il header standard, ma questa versione offre più controllo)
const CACHE_TTL = 60; // Secondi prima che la cache sia "fresh"
const STALE_TTL = 300; // Secondi aggiuntivi in cui la cache e "stale but usable"
interface CacheMetadata {
cachedAt: number;
ttl: number;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method !== 'GET') return fetch(request);
const cache = caches.default;
// Crea una Request con una custom cache key che include i metadata
const cacheKey = new Request(request.url, {
headers: request.headers,
});
const cached = await cache.match(cacheKey);
if (cached) {
const cachedAt = parseInt(cached.headers.get('X-Cached-At') ?? '0');
const age = (Date.now() - cachedAt) / 1000;
if (age < CACHE_TTL) {
// FRESH: servi dalla cache senza revalidazione
return addCacheHeaders(cached, 'FRESH', age);
}
if (age < CACHE_TTL + STALE_TTL) {
// STALE: servi dalla cache ma revalida in background
ctx.waitUntil(revalidate(cacheKey, cache));
return addCacheHeaders(cached, 'STALE', age);
}
}
// MISS o troppo vecchio: fetch sincrono
return fetchAndCache(cacheKey, cache, ctx);
},
};
async function revalidate(cacheKey: Request, cache: Cache): Promise<void> {
const fresh = await fetch(cacheKey.url);
if (fresh.ok) {
const toCache = addTimestamp(fresh);
await cache.put(cacheKey, toCache);
}
}
async function fetchAndCache(
cacheKey: Request,
cache: Cache,
ctx: ExecutionContext
): Promise<Response> {
const response = await fetch(cacheKey.url);
if (response.ok) {
const toCache = addTimestamp(response.clone());
ctx.waitUntil(cache.put(cacheKey, toCache));
}
const headers = new Headers(response.headers);
headers.set('X-Cache-Status', 'MISS');
return new Response(response.body, { status: response.status, headers });
}
function addTimestamp(response: Response): Response {
const headers = new Headers(response.headers);
headers.set('X-Cached-At', String(Date.now()));
// Cache-Control: max-age elevato per far "sopravvivere" la risposta in cache
headers.set('Cache-Control', 'public, max-age=86400');
return new Response(response.body, { status: response.status, headers });
}
function addCacheHeaders(response: Response, status: string, age: number): Response {
const headers = new Headers(response.headers);
headers.set('X-Cache-Status', status);
headers.set('Age', String(Math.floor(age)));
return new Response(response.body, { status: response.status, headers });
}
interface Env {}
사용자 정의 캐시 키: 상황별 캐시 분리
기본적으로 캐시 키는 요청의 전체 URL입니다. 그러나 종종 당신은 필요합니다
언어, API 버전, 사용자 또는 장치 계층으로 구분된 캐시입니다. 해결책은
하나 만들어 맞춤 캐시 키 객체로서 Request
짧은 URL로.
// Cache differenziata per lingua e versione API
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
const url = new URL(request.url);
// Estrai parametri rilevanti per la cache key
const lang = request.headers.get('Accept-Language')?.split(',')[0]?.split('-')[0] ?? 'en';
const apiVersion = url.searchParams.get('v') ?? 'v1';
const tier = request.headers.get('X-User-Tier') ?? 'free';
// Costruisci una URL sintetica come cache key
// Non deve essere una URL reale, solo identificativa
const cacheKeyUrl = new URL(request.url);
cacheKeyUrl.searchParams.set('_ck_lang', lang);
cacheKeyUrl.searchParams.set('_ck_v', apiVersion);
// Non includiamo 'tier' nella key se vuoi condividere la cache tra tier
const cacheKey = new Request(cacheKeyUrl.toString(), {
method: 'GET',
// Importante: non copiare headers di autenticazione nella cache key
// altrimenti ogni utente avrebbe la propria cache entry
});
// Cerca nella cache con la custom key
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// Fetch dall'origin passando la richiesta originale (con auth headers)
const originResponse = await fetch(request);
if (originResponse.ok && isCacheable(originResponse)) {
const toCache = setCacheHeaders(originResponse.clone(), 300);
ctx.waitUntil(cache.put(cacheKey, toCache));
}
return originResponse;
},
};
function isCacheable(response: Response): boolean {
// Non mettere in cache risposte con dati personali o Set-Cookie
if (response.headers.has('Set-Cookie')) return false;
if (response.headers.get('Cache-Control')?.includes('private')) return false;
if (response.headers.get('Cache-Control')?.includes('no-store')) return false;
return true;
}
function setCacheHeaders(response: Response, maxAge: number): Response {
const headers = new Headers(response.headers);
headers.set('Cache-Control', `public, max-age=${maxAge}, s-maxage=${maxAge}`);
// Rimuovi header che potrebbero impedire il caching
headers.delete('Set-Cookie');
return new Response(response.body, { status: response.status, headers });
}
interface Env {}
캐시 헤더: s-maxage, 재검증 중 오래된 것, 오류가 있는 경우 오래된 것
Cloudflare는 다음을 존중합니다. 표준 캐시 제어 헤더 그리고 그것을 확장 의미. 다음 지침을 이해하는 것이 중요합니다.
| 지령 | 의미 | Esempio |
|---|---|---|
max-age=N |
브라우저 및 CDN용 TTL(N초) | max-age=300 |
s-maxage=N |
CDN/프록시 전용 TTL(Cloudflare의 최대 기간 재정의) | s-maxage=3600, max-age=60 |
stale-while-revalidate=N |
재검증하는 동안 오래된 서비스를 제공하는 추가 시간(초) | s-maxage=60, stale-while-revalidate=300 |
stale-if-error=N |
원본이 오류를 반환하는 경우 오래된 서비스를 제공하는 데 걸리는 시간(초) | stale-if-error=86400 |
no-store |
어떤 상황에서도 캐시하지 마세요 | 민감한 데이터의 경우 |
private |
CDN이 아닌 캐시 브라우저만 해당 | 인증된 응답의 경우 |
// Esempio: API con s-maxage e stale-while-revalidate via header
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Routing con TTL differenziati per tipo di risorsa
if (url.pathname.startsWith('/api/products')) {
return fetchWithCacheHeaders(request, {
sMaxAge: 300, // 5 minuti fresh in CDN
staleWhileRevalidate: 3600, // 1 ora stale accettabile
staleIfError: 86400, // 1 giorno stale in caso di errore origin
});
}
if (url.pathname.startsWith('/api/user')) {
// Dati utente: non cacheare in CDN
return fetchWithCacheHeaders(request, {
sMaxAge: 0,
private: true,
});
}
if (url.pathname.startsWith('/static')) {
// Asset statici: cache aggressiva
return fetchWithCacheHeaders(request, {
sMaxAge: 31536000, // 1 anno
immutable: true,
});
}
return fetch(request);
},
};
interface CacheOptions {
sMaxAge?: number;
staleWhileRevalidate?: number;
staleIfError?: number;
private?: boolean;
immutable?: boolean;
}
async function fetchWithCacheHeaders(
request: Request,
options: CacheOptions
): Promise<Response> {
const response = await fetch(request);
const headers = new Headers(response.headers);
let cacheControl = '';
if (options.private) {
cacheControl = 'private, no-store';
} else {
const parts: string[] = ['public'];
if (options.sMaxAge !== undefined) parts.push(`s-maxage=${options.sMaxAge}`);
if (options.staleWhileRevalidate) parts.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
if (options.staleIfError) parts.push(`stale-if-error=${options.staleIfError}`);
if (options.immutable) parts.push('immutable');
cacheControl = parts.join(', ');
}
headers.set('Cache-Control', cacheControl);
return new Response(response.body, { status: response.status, headers });
}
interface Env {}
무효화: URL, 태그 및 접두사 제거
cache.delete(request) 작업자가 실행되는 로컬 PoP에서만 캐시를 삭제하세요.
캐시를 무효화하려면 전 세계 모든 PoP에 걸쳐, API를 사용해야 합니다.
Cloudflare 영역 퍼지 REST. 이것이 콘텐츠 관리를 위한 올바른 메커니즘입니다.
그리고 배포하세요.
// Invalidation globale tramite Cloudflare API
// Da usare tipicamente da un webhook CMS o da un Worker admin
interface PurgeOptions {
files?: string[]; // URL specifici
tags?: string[]; // Cache-Tag headers
prefixes?: string[]; // URL prefix
hosts?: string[]; // Tutti gli URL di un host
}
async function purgeCloudflareCache(
zoneId: string,
apiToken: string,
options: PurgeOptions
): Promise<void> {
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(options),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Purge failed: ${JSON.stringify(error)}`);
}
}
// Worker che funge da webhook per invalidazione CMS
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
// Verifica il secret del webhook
const secret = request.headers.get('X-Webhook-Secret');
if (secret !== env.WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const body = await request.json() as WebhookPayload;
// Invalida le URL specifiche aggiornate dal CMS
if (body.type === 'post.updated') {
await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
files: [
`https://example.com/blog/${body.slug}`,
`https://example.com/api/posts/${body.id}`,
`https://example.com/sitemap.xml`,
],
});
}
// Invalida per tag (richiede Cache-Tag header sulle risposte origin)
if (body.type === 'category.updated') {
await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
tags: [`category-${body.categorySlug}`],
});
}
return new Response(JSON.stringify({ purged: true }), {
headers: { 'Content-Type': 'application/json' },
});
},
};
interface WebhookPayload {
type: string;
id?: string;
slug?: string;
categorySlug?: string;
}
interface Env {
ZONE_ID: string;
CF_API_TOKEN: string;
WEBHOOK_SECRET: string;
}
캐시 태그: 의미 체계 무효화
I 캐시 태그 이는 무효화를 위한 가장 강력한 메커니즘입니다.
선택적. 헤더를 추가하여 작동합니다. Cache-Tag 답변에 :
각 응답에는 여러 개의 태그가 있을 수 있으며 모든 URL을 무효화할 수 있습니다.
단일 API 호출로 태그와 연결됩니다.
// Origin server o Worker che aggiunge Cache-Tag alle risposte
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const response = await fetch(request);
const headers = new Headers(response.headers);
// Aggiungi tag semantici basati sul contenuto
const tags: string[] = [];
// Tag per tipo di risorsa
if (url.pathname.startsWith('/api/products')) {
const productId = url.pathname.split('/')[3];
tags.push('products'); // Invalida tutti i prodotti
if (productId) tags.push(`product-${productId}`); // Invalida questo prodotto specifico
}
if (url.pathname.startsWith('/api/categories')) {
const catId = url.pathname.split('/')[3];
tags.push('categories');
if (catId) tags.push(`category-${catId}`);
}
// Tag per versione dell'API
const apiVersion = url.pathname.split('/')[2];
if (apiVersion?.startsWith('v')) {
tags.push(`api-${apiVersion}`);
}
if (tags.length > 0) {
// Cache-Tag: lista separata da virgole, max 16KB
headers.set('Cache-Tag', tags.join(','));
}
return new Response(response.body, { status: response.status, headers });
},
};
// Esempio di invalidazione per tag dopo un aggiornamento:
// POST /api/admin/purge
// { "tags": ["product-123", "categories"] }
// Invalida tutte le URL che hanno Cache-Tag: product-123 o categories
interface Env {}
고급 패턴: KV를 L2로 사용하는 캐시
Cache API에는 중요한 제한 사항이 있습니다. 프로그래밍 방식으로 액세스할 수 없습니다. 임의 읽기/쓰기의 경우 HTTP 요청에만 해당됩니다. 더 많은 패턴을 보려면 복잡한(예: 조정 무효화, 회로 차단기 또는 개체 캐시) 비 HTTP), 사용 L2 캐시로서의 작업자 KV.
// Cache a due livelli: Cache API (L1, HTTP) + KV (L2, programmabile)
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const cacheKey = buildCacheKey(url);
// L1: Cache API (piu veloce, locale al PoP)
const l1Cache = caches.default;
const l1Hit = await l1Cache.match(request);
if (l1Hit) {
return addHeader(l1Hit, 'X-Cache', 'L1-HIT');
}
// L2: KV Store (globale, programmabile, con TTL gestito da KV)
const kvCached = await env.API_CACHE.getWithMetadata<CacheMetadata>(cacheKey);
if (kvCached.value !== null) {
const { value, metadata } = kvCached;
// Ricostruisci una Response dalla stringa KV
const cachedResponse = new Response(value, {
headers: {
'Content-Type': metadata?.contentType ?? 'application/json',
'Cache-Control': 'public, max-age=60',
'X-Cache': 'L2-HIT',
'X-Cached-At': String(metadata?.cachedAt ?? 0),
},
});
// Popola anche L1 per richieste successive nello stesso PoP
ctx.waitUntil(l1Cache.put(request, cachedResponse.clone()));
return cachedResponse;
}
// MISS su entrambi i livelli: fetch dall'origin
const originResponse = await fetch(request);
if (originResponse.ok) {
const body = await originResponse.text();
const contentType = originResponse.headers.get('Content-Type') ?? 'application/json';
const metadata: CacheMetadata = {
cachedAt: Date.now(),
contentType,
url: url.toString(),
};
// Salva in KV con TTL di 5 minuti
ctx.waitUntil(
env.API_CACHE.put(cacheKey, body, {
expirationTtl: 300,
metadata,
})
);
// Salva anche in L1
const toL1 = new Response(body, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=60',
'X-Cache': 'MISS',
},
});
ctx.waitUntil(l1Cache.put(request, toL1));
return new Response(body, {
headers: {
'Content-Type': contentType,
'X-Cache': 'MISS',
},
});
}
return addHeader(originResponse, 'X-Cache', 'BYPASS-ERROR');
},
};
function buildCacheKey(url: URL): string {
// Normalizza l'URL per la cache key (rimuovi query params non semantici)
const params = new URLSearchParams(url.searchParams);
params.delete('utm_source');
params.delete('utm_medium');
params.delete('_t'); // timestamp di cache-busting
params.sort(); // ordine deterministico
return `${url.pathname}?${params.toString()}`;
}
function addHeader(response: Response, key: string, value: string): Response {
const headers = new Headers(response.headers);
headers.set(key, value);
return new Response(response.body, { status: response.status, headers });
}
interface CacheMetadata {
cachedAt: number;
contentType: string;
url: string;
}
interface Env {
API_CACHE: KVNamespace;
}
캐시 워밍: 캐시 미리 채우기
Il 캐시 워밍업 캐시를 먼저 미리 채우는 관행입니다. 실제 요청이 도착하면 배포 후 콜드 캐시 문제가 제거됩니다. 대규모 무효화. Cron Trigger를 통해 예약된 작업자로 구현됩니다.
// wrangler.toml - Cron Trigger per cache warming
// [triggers]
// crons = ["*/15 * * * *"] # Ogni 15 minuti
// worker.ts - Cache Warming Worker
const URLS_TO_WARM = [
'https://api.example.com/products?featured=true',
'https://api.example.com/categories',
'https://api.example.com/homepage',
'https://api.example.com/navigation',
];
export default {
// Scheduled handler per Cron Trigger
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
console.log(`Cache warming started at ${new Date(event.scheduledTime).toISOString()}`);
const results = await Promise.allSettled(
URLS_TO_WARM.map(url => warmUrl(url))
);
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Cache warming complete: ${succeeded} success, ${failed} failed`);
},
async fetch(request: Request): Promise<Response> {
return new Response('Cache Warmer Worker', { status: 200 });
},
};
async function warmUrl(url: string): Promise<void> {
// Forza bypass della cache aggiungendo header speciale
// (da gestire lato Worker principale con whitelist IP o secret)
const response = await fetch(url, {
headers: {
'Cache-Control': 'no-cache', // Forza revalidazione
'X-Cache-Warm': 'true',
},
});
if (!response.ok) {
throw new Error(`Failed to warm ${url}: ${response.status}`);
}
}
interface Env {}
interface ScheduledEvent {
scheduledTime: number;
cron: string;
}
캐시 디버깅 및 모니터링
Cloudflare는 캐시 상태를 이해하기 위해 응답으로 진단 헤더를 공개합니다.
가장 중요한 것은 CF-Cache-Status:
| CF-캐시-상태 | 의미 |
|---|---|
HIT |
Cloudflare 캐시에서 제공 |
MISS |
캐시되지 않음, 원본에서 요청됨 |
EXPIRED |
캐시되었지만 TTL이 만료되었습니다. 원본에 요청하세요. |
STALE |
오래된 것 제공(재검증하는 동안 오래된 것) |
BYPASS |
캐시 우회(쿠키, 인증 헤더 등) |
DYNAMIC |
캐시 불가능(동적 응답) |
REVALIDATED |
원본으로 검증된 캐시(304 Not Modified) |
// Worker che logga le metriche di cache su KV Analytics
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const response = await fetch(request);
const cacheStatus = response.headers.get('CF-Cache-Status') ?? 'UNKNOWN';
// Logga la metrica in background
ctx.waitUntil(
logCacheMetric(env, {
url: request.url,
status: cacheStatus,
timestamp: Date.now(),
country: request.cf?.country ?? 'unknown',
datacenter: request.cf?.colo ?? 'unknown',
})
);
return response;
},
};
async function logCacheMetric(env: Env, metric: CacheMetric): Promise<void> {
// Aggrega per finestre di 1 minuto
const minuteKey = `metrics:${Math.floor(metric.timestamp / 60000)}:${metric.status}`;
const current = parseInt(await env.METRICS_KV.get(minuteKey) ?? '0');
await env.METRICS_KV.put(minuteKey, String(current + 1), { expirationTtl: 3600 });
}
interface CacheMetric {
url: string;
status: string;
timestamp: number;
country: string;
datacenter: string;
}
interface Env {
METRICS_KV: KVNamespace;
}
결론 및 다음 단계
Cloudflare Workers Cache API는 CDN을 수동 도구에서 구성 요소로 변환합니다. 건축 분야에서 활동 중입니다. 이 문서에 설명된 전략을 사용하면 다음과 같은 전략을 구축할 수 있습니다. 로드를 줄이는 세분화되고 의미론적이며 무효화 가능한 캐싱 계층 일반적인 공개 API 워크로드의 경우 오리진에서 70~90%.
기억해야 할 핵심 사항: 사용 ctx.waitUntil() 응답을 차단하지 않도록
클라이언트에 대해 사용자 정의 캐시 키를 구축하여 다양한 컨텍스트를 분리하고 캐시 태그를 사용합니다.
의미론적 무효화를 위해, 그리고 더 복잡한 패턴을 위해 캐시 API를 KV와 결합합니다.
시리즈의 다음 기사
- 제9조: 현지 작업자 테스트 — Miniflare, Vitest e Wrangler Dev: 배포 없이 작업자에 대한 단위 테스트 및 통합 테스트를 작성하는 방법
- 제10조: 엣지의 풀스택 아키텍처 — 사례 연구 제로에서 프로덕션으로: GitHub Actions의 Workers + D1 + R2 및 CI/CD가 포함된 완전한 REST API입니다.







