Cloudflare Workers는 EmDash를 위한 빠르고 전 세계에 분산된 런타임을 제공합니다. 이 가이드에서는 데이터베이스에 D1, 미디어 스토리지에 R2를 사용한 배포를 다룹니다.
사전 요구 사항
- Cloudflare 계정
- Wrangler CLI 설치 (
npm install -g wrangler) - Cloudflare 인증 완료 (
wrangler login)
바인딩 구성
프로젝트 루트에 D1과 R2 바인딩이 포함된 wrangler.jsonc를 생성합니다. Wrangler는 첫 번째 배포 시 리소스가 아직 존재하지 않으면 자동으로 프로비저닝합니다.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-emdash-site",
"compatibility_date": "2025-01-15",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media",
},
],
}
EmDash 구성
D1과 R2를 사용하도록 Astro 설정을 업데이트합니다:
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import emdash from "emdash/astro";
import { d1, r2 } from "@emdash-cms/cloudflare";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
react(), // 필수 — 관리 UI는 React 앱
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
첫 번째 부팅
데이터베이스 마이그레이션은 배포 후 첫 번째 요청 시 자동으로 실행되며, 새로 적용할 것이 있을 때마다 이후 부팅 시에도 실행됩니다.
데이터베이스가 비어 있고(컬렉션 없음) 설정 마법사가 완료되지 않은 경우, EmDash는 첫 번째 부팅 시 시드 파일도 적용합니다. 시드는 빌드 시 .emdash/seed.json, package.json#emdash.seed의 경로, 또는 seed/seed.json에서 읽히며 — 먼저 찾은 것이 사용됩니다 — 번들에 인라인됩니다. 없는 경우 내장 기본 시드가 사용됩니다. 기존 데이터베이스에 대한 후속 배포는 콘텐츠를 변경하지 않습니다.
이미 배포된 사이트의 스키마나 콘텐츠 모델을 변경하려면 배포된 사이트 진화를 참조하세요.
예약 게시
Cloudflare Workers에서 예약 게시, 플러그인 cron, 유지보수 작업은 Worker Cron Trigger에서 실행됩니다. 새 Cloudflare 템플릿에는 이 설정이 자동으로 포함됩니다. 기존 프로젝트를 업데이트하는 경우 @emdash-cms/cloudflare/worker에서 EmDash Worker 엔트리를 내보내세요:
export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";
그런 다음 wrangler.jsonc에 Cron Trigger를 추가합니다:
{
"triggers": {
"crons": ["* * * * *"],
},
}
배포
Cloudflare Workers에 배포:
wrangler deploy
사이트가 https://my-emdash-site.<your-subdomain>.workers.dev에서 라이브됩니다.
Read Replica
전 세계에 분산된 사이트의 경우, D1 읽기 복제를 활성화하여 항상 프라이머리 데이터베이스에 접근하는 대신 가까운 레플리카로 읽기 쿼리를 라우팅합니다. 이는 프라이머리 리전에서 먼 방문자의 지연 시간을 크게 줄여줍니다.
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
storage: r2({ binding: "MEDIA" }),
}),
Cloudflare 대시보드 또는 REST API를 통해 D1 데이터베이스 자체에서도 읽기 복제를 활성화해야 합니다.
세션 모드와 북마크 기반 일관성의 작동 방식에 대해서는 데이터베이스 옵션 — Read Replica를 참조하세요.
Object Cache
D1의 읽기 부하를 줄이려면, 콘텐츠 및 설정 쿼리 결과를 Cloudflare KV에 캐시합니다. 매 요청마다 데이터베이스를 쿼리하는 대신 KV에서 읽기가 제공됩니다:
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
KV 설정, 옵션, 무효화 동작에 대해서는 Object Cache를 참조하세요.
Workers Cache
Cloudflare의 Workers Cache (wrangler.jsonc의 "cache": { "enabled": true })는 Worker 앞에 엣지 캐시를 배치합니다: 일치하는 요청은 Worker를 전혀 실행하지 않고 제공됩니다. 이는 EmDash와 잘 작동합니다:
- EmDash 관리 및 API 응답은
Cache-Control: private, no-store를 보내며 저장되지 않습니다. - 공개 페이지는 반환하는
Cache-Control헤더를 통해 자체 캐싱을 제어합니다.
활성화 전에 알아야 할 두 가지:
Cache-Control헤더가 없는 응답도 캐시됩니다. Workers Cache는 RFC 9111 휴리스틱 프레시니스를 적용합니다 — 헤더가 없는200은 2시간 동안 캐시됩니다. 모든 커스텀 라우트에 명시적인Cache-Control을 지정하세요(세션 의존적인 것에는private, no-store사용).- 캐시된 페이지는 로그인한 편집자와 공유됩니다. 캐시는 Worker 전에 실행되므로 요청 쿠키를 기반으로 우회할 수 없습니다. 로그인한 편집자는 항목이 만료될 때까지 공개 페이지의 캐시된 익명 변형을 — 시각적 편집 도구 모음 없이 — 받을 수 있습니다. 편집자가 렌더링한 응답 자체는 저장되지 않으므로(
private, no-store보유), 반대 방향으로는 누출되지 않습니다.
커스텀 도메인
Cloudflare 대시보드에서 커스텀 도메인을 추가합니다:
- Workers & Pages > 해당 worker로 이동
- Custom Domains > Add Custom Domain 클릭
- 도메인을 입력하고 DNS 설정 지침을 따릅니다
퍼블릭 R2 액세스
R2에서 직접 미디어를 제공하려면 (성능을 위해 권장):
- Cloudflare 대시보드에서 R2 > 해당 버킷으로 이동
- Settings > Public access 클릭
- 퍼블릭 액세스를 활성화하고 퍼블릭 URL을 기록
- 스토리지 설정 업데이트:
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxx.r2.dev"
}),
Cloudflare Access 인증
조직에서 Cloudflare Access를 사용하는 경우, passkey 대신 인증 공급자로 사용하여 기존 ID 공급자를 통한 싱글 사인온을 제공할 수 있습니다. 다음 설정으로 활성화합니다:
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
"Admins": 50,
"Editors": 40,
},
}),
}),
모든 설정 옵션에 대해서는 인증 가이드를 참조하세요.
이메일
Workers에서 유일한 내장 email:deliver 핸들러는 개발 콘솔 스텁이므로,
이메일 의존 플로우 — 매직 링크 로그인, 팀 초대, 댓글
알림 — 은 프로덕션에서 **“Email is not configured”**로 실패합니다.
cloudflareEmail() 플러그인은 외부 API 키 없이 네이티브 send_email Worker 바인딩을 사용하여
Cloudflare Email Sending
을 통해 실제 이메일을 전달합니다.
1. 발신 도메인 등록
Cloudflare 대시보드에서 Email로 이동하여 보내는 도메인(또는 주소)을 확인합니다. Email Sending은 확인되지 않은 발신자의 메시지를 거부합니다.
2. 바인딩 추가
wrangler.jsonc에 send_email 바인딩을 선언합니다:
{
"send_email": [{ "name": "EMAIL" }],
}
3. 공급자 등록
emdash() 통합에 플러그인을 추가합니다:
import { d1, r2 } from "@emdash-cms/cloudflare";
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
plugins: [
cloudflareEmail({
from: { email: "[email protected]", name: "My Site CMS" },
replyTo: "[email protected]", // 선택 사항
binding: "EMAIL", // 선택 사항, 기본값 "EMAIL"
}),
],
}),
4. 활성화 및 선택
배포한 후 Admin → Extensions에서 플러그인을 활성화하고 Settings → Email에서 공급자로 선택합니다.
옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
from | string | { email, name? } | — (필수) | Email Sending에 등록된 도메인의 발신자 주소. |
replyTo | string | — | 선택적 Reply-To. from이 no-reply 서브도메인 주소일 때 유용. |
binding | string | "EMAIL" | wrangler.jsonc의 send_email 바인딩 이름. |
환경 변수
권장: 암호화 키
EMDASH_ENCRYPTION_KEY는 플러그인 시크릿을 저장 시 암호화하기 위한
키입니다(웹훅 토큰, Turnstile 키 등). 키는 시작 시 검증되며,
플러그인 시크릿 암호화는 활성화되면 이를 사용합니다. 모든 배포에서 설정하여
나중에 설정을 변경하지 않고도 시크릿이 보호되도록 하세요.
키는 사용자가 제공하며 데이터베이스에 저장되지 않습니다. 암호화된 암호문만 저장됩니다. 키를 잃으면 그것으로 암호화된 모든 시크릿을 잃게 됩니다.
다음 명령으로 키를 생성하고 Worker 시크릿으로 저장합니다:
npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY
선택 사항: 안정 값 오버라이드
EmDash는 프리뷰 HMAC 시크릿과 댓글 작성자 IP 해시 솔트를 자동 생성하고 첫 사용 시 데이터베이스에 저장합니다. 아래 환경 변수는 값을 직접 고정해야 하는 경우의 오버라이드입니다 — 예를 들어, 별도 프로세스의 프리뷰 Worker가 메인 사이트와 시크릿을 공유해야 할 때.
| 변수 | 목적 |
|---|---|
EMDASH_PREVIEW_SECRET | 자동 생성된 프리뷰 HMAC 시크릿의 오버라이드. |
EMDASH_IP_SALT | 자동 생성된 댓글 작성자 IP 해시 솔트의 오버라이드. |
EMDASH_AUTH_SECRET | 선택 사항. 설정되면 IP 솔트 소스로 사용됩니다(EMDASH_IP_SALT도 설정된 경우 후자가 우선). 이미 이에 의존하는 설치의 댓글 작성자 IP 해시를 안정적으로 유지합니다. 새 배포에서는 설정하지 마세요. |
설정에서 import.meta.env 또는 Cloudflare env 바인딩을 사용하여 환경 변수에 액세스합니다.
EmDash가 사용하는 모든 시크릿의 전체 목록 — 저장 위치, 교체 단계, 키를 잃었을 때 무엇이 깨지는지 포함 — 에 대해서는 시크릿 및 키 관리를 참조하세요.
프리뷰 배포
프리뷰 브랜치 배포:
wrangler deploy --env preview
wrangler.jsonc에 환경 섹션 추가:
{
"env": {
"preview": {
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db-preview",
},
],
},
},
}
문제 해결
”D1 binding not found”
wrangler.jsonc의 바인딩 이름이 데이터베이스 설정과 일치하는지 확인합니다:
// 일치해야 함: d1({ binding: "DB" })
"binding": "DB"
“R2 binding not found”
R2 버킷이 올바르게 바인딩되어 있는지 확인합니다:
// 일치해야 함: r2({ binding: "MEDIA" })
"binding": "MEDIA"
마이그레이션 오류
스키마 오류가 보이면 Worker 로그를 tail (wrangler tail)하고 오류를 재현하여 기본 메시지를 캡처한 다음, 해당 출력으로 이슈를 제출하세요.