정적 에셋 저장 및 조회하기
Workers KV에 정적 에셋을 저장하고 Workers 애플리케이션에서 저지연·고처리량으로 제공하는 방법을 알아봐요. KV의 전역 복제 특성을 활용하면 정적 파일을 전 세계에서 빠르게 읽는 동시에, KV 바인딩을 통해 코드 배포 없이도 에셋을 유연하게 관리할 수 있어요.
출처: 문서
본문
정적 에셋을 Workers KV에 저장하면 이러한 에셋을 저지연·고처리량으로 전 세계에서 조회할 수 있어요. 그런 다음 이 에셋을 직접 제공하거나, 동적으로 응답을 생성하는 데 사용할 수 있어요. 이는 커스텀 스크립트, KV 제한 내에 들어가는 작은 이미지 같은 파일을 제공할 때, 또는 번역(translations) 같은 정적 에셋에서 동적 HTML 응답을 생성할 때 유용해요.
프런트엔드 또는 풀스택 웹 애플리케이션을 호스팅해야 한다면 Cloudflare Workers 정적 에셋 또는 Cloudflare Pages를 사용하세요. 이 서비스들은 웹 애플리케이션과 그 에셋을 위한 목적에 맞게 설계된 배포 경험을 제공해요.
Workers KV는 배포 없이도 Worker에서 직접 에셋에 접근하고, 수정하고, 저장할 수 있는 더 유연한 API를 제공해요. 이는 배포 번들에 포함되지 않은 커스텀 에셋(업로드된 미디어 에셋, 런타임에 생성된 커스텀 스크립트나 파일 등)을 제공하는 데 유용해요.
Wrangler로 Workers KV에 정적 에셋 쓰기
Workers KV에 정적 에셋을 저장하려면 Wrangler CLI (주로 개발 중에 사용해요), Workers 애플리케이션의 Workers KV 바인딩, 또는 Workers KV REST API (주로 외부 애플리케이션에서 Workers KV에 접근할 때 사용해요)를 사용할 수 있어요. 여기서는 Wrangler CLI를 사용하는 방법을 보여줄게요.
이 시나리오에서는 Workers KV 저장소에 샘플 HTML 파일을 저장할게요.
다음 내용으로 새 파일 index.html 을 만드세요:
Hello World!
그리고 나서 다음 Wrangler 명령을 사용해 프로덕션 및 프리뷰 네임스페이스에 이 파일에 대한 KV 쌍(pair)을 만들 수 있어요:
npx wrangler kv key put index.html --path index.html --namespace-id=<ENTER_NAMESPACE_ID_HERE>
이 명령은 Wrangler 파일에 있는 바인딩이 지정하는 프로덕션 및 프리뷰 네임스페이스에, 파일 이름을 키로, 파일 내용을 값으로 하는 KV 쌍을 만들어요.
Worker 애플리케이션에서 KV의 정적 에셋 제공하기
이 예시에서는 Workers 애플리케이션이 HTTP 요청의 경로로 아무 키 이름이나 받아들여서, 해당 키에 대해 KV 저장소에 저장된 값을 반환해요.
import mime from "mime";
interface Env {
assets: KVNamespace;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
// Return error if not a get request
if(request.method !== 'GET'){
return new Response('Method Not Allowed', {
status: 405,
})
}
// Get the key from the url & return error if key missing
const parsedUrl = new URL(request.url)
const key = parsedUrl.pathname.replace(/^\/+/, '') // Strip any preceding /'s
if(!key){
return new Response('Missing path in URL', {
status: 400
})
}
// Get the mimetype from the key path
const extension = key.split('.').pop();
let mimeType = mime.getType(extension) || "text/plain";
if (mimeType.startsWith("text") || mimeType === "application/javascript") {
mimeType += "; charset=utf-8";
}
// Get the value from the Workers KV store and return it if found
const value = await env.assets.get(key, 'arrayBuffer')
if(!value){
return new Response("Not found", {
status: 404
})
}
// Return the response from the Workers application with the value from the KV store
return new Response(value, {
status: 200,
headers: new Headers({
"Content-Type": mimeType
})
});
},
} satisfies ExportedHandler<Env>;
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "<ENTER_WORKER_NAME>",
"main": "src/index.ts",
"compatibility_date": "2025-03-03",
"observability": {
"enabled": true
},
"kv_namespaces": [
{
"binding": "assets",
"id": "<YOUR_BINDING_ID>"
}
]
}
이 코드는 HTTP 요청에서 가져올 키-값 쌍의 키 이름을 파싱해요. 그런 다음 응답의 올바른 MIME 타입을 결정해서 브라우저가 응답을 어떻게 처리할지 알려줘요. KV 저장소에서 값을 조회할 때 이 코드는 arrayBuffer 를 사용해 이미지, 문서, 비디오/오디오 파일 같은 이진 데이터를 올바르게 처리해요.
Workers KV 네임스페이스 저장소에 HTML 콘텐츠를 값으로 가진 index.html 키의 샘플 키-값 쌍이 있다고 하면, https://<YOUR-WORKER-HOSTNAME>/index.html 에서 Workers 애플리케이션에 접근해서 index.html 파일의 내용을 볼 수 있어요.
이미지나 문서로 시도해 보면 이 Worker가 KV에서 해당 에셋들도 올바르게 제공하는 것을 볼 수 있을 거예요.
키-값 쌍에서 동적 응답 생성하기
정적 에셋을 제공하는 것 외에도, KV 저장소에 저장된 값을 기반으로 동적 HTML 또는 API 응답을 생성할 수도 있어요.
- 먼저 프로젝트 루트에 다음 파일을 만드세요:
[
{
"language_code": "en",
"message": "Hello World!"
},
{
"language_code": "es",
"message": "¡Hola Mundo!"
},
{
"language_code": "fr",
"message": "Bonjour le monde!"
},
{
"language_code": "de",
"message": "Hallo Welt!"
},
{
"language_code": "zh",
"message": "你好,世界!"
},
{
"language_code": "ja",
"message": "こんにちは、世界!"
},
{
"language_code": "hi",
"message": "नमस्ते दुनिया!"
},
{
"language_code": "ar",
"message": "مرحبا بالعالم!"
}
]
- 터미널을 열고 다음 KV 명령을 입력해서 번역 파일에 대한 KV 항목을 만들세요:
npx wrangler kv key put hello-world.json --path hello-world.json --namespace-id=<ENTER_NAMESPACE_ID_HERE>
- Workers 코드를 업데이트해서 요청의
Accept-Language헤더의 언어에 따라 번역된 HTML 파일을 제공하는 로직을 추가하세요:
import mime from 'mime';
import parser from 'accept-language-parser'
interface Env {
assets: KVNamespace;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
// Return error if not a get request
if(request.method !== 'GET'){
return new Response('Method Not Allowed', {
status: 405,
})
}
// Get the key from the url & return error if key missing
const parsedUrl = new URL(request.url)
const key = parsedUrl.pathname.replace(/^\/+/, '') // Strip any preceding /'s
if(!key){
return new Response('Missing path in URL', {
status: 400
})
}
// Add handler for translation path (with early return)
if(key === 'hello-world'){
// Retrieve the language header from the request and the translations from Workers KV
const languageHeader = request.headers.get('Accept-Language') || 'en' // Default to English
const translations : {
"language_code": string,
"message": string
}[] = await env.assets.get('hello-world.json', 'json') || [];
// Extract the requested language
const supportedLanguageCodes = translations.map(item => item.language_code)
const languageCode = parser.pick(supportedLanguageCodes, languageHeader, {
loose: true
})
// Get the message for the selected language
let selectedTranslation = translations.find(item => item.language_code === languageCode)
if(!selectedTranslation) selectedTranslation = translations.find(item => item.language_code === "en")
const helloWorldTranslated = selectedTranslation!['message'];
// Generate and return the translated html
const html = `<!DOCTYPE html>
<html>
<head>
<title>Hello World translation</title>
</head>
<body>
<h1>${helloWorldTranslated}</h1>
</body>
</html>
`
return new Response(html, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8'
}
})
}
// Get the mimetype from the key path
const extension = key.split('.').pop();
let mimeType = mime.getType(extension) || "text/plain";
if (mimeType.startsWith("text") || mimeType === "application/javascript") {
mimeType += "; charset=utf-8";
}
// Get the value from the Workers KV store and return it if found
const value = await env.assets.get(key, 'arrayBuffer')
if(!value){
return new Response("Not found", {
status: 404
})
}
// Return the response from the Workers application with the value from the KV store
return new Response(value, {
status: 200,
headers: new Headers({
"Content-Type": mimeType
})
});
},
} satisfies ExportedHandler<Env>;
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "<ENTER_WORKER_NAME>",
"main": "src/index.ts",
"compatibility_date": "2025-03-03",
"observability": {
"enabled": true
},
"kv_namespaces": [
{
"binding": "assets",
"id": "<YOUR_BINDING_ID>"
}
]
}
이 새로운 코드는 번역된 응답을 제공하는 전용 엔드포인트인 /hello-world 를 제공해요. 이 URL에 접근하면 Worker 코드는 먼저 Accept-Language 요청 헤더에서 클라이언트가 요청한 언어를, 그리고 hello-world.json 키에 대한 KV 저장소에서 번역을 조회해요. 그런 다음 번역된 메시지를 가져와 생성된 HTML을 반환해요.
https://<YOUR-WORKER-HOSTNAME>/hello-world 에서 Workers 애플리케이션에 접근하면, 애플리케이션이 올바르게 번역된 "Hello World" 메시지를 반환하는 것을 확인할 수 있어요.
브라우저의 개발자 콘솔에서 로케일 언어를 바꿔 보세요 (Chromium 브라우저에서는 Show Sensors 를 실행하면 로케일 드롭다운 선택이 나타나요). 이제 Worker가 로케일 언어에 따라 번역된 메시지를 반환하는 것을 볼 수 있을 거예요.
관련 리소스
더 알아보기 (Learn more)
Workers KV에 정적 에셋과 설정을 저장해 전역 저지연으로 제공하는 패턴은 이후 챕터의 KV 바인딩, KV 네임스페이스, 그리고 KV의 읽기/쓰기 API 문서를 함께 보면 더 깊이 이해할 수 있어요.