Cloudflare에 배포

이 페이지

이 가이드는 데이터베이스에 D1, 미디어에 R2를 사용해 EmDash 사이트를 Cloudflare Workers에 배포합니다. EmDash Cloudflare 템플릿으로 시작하거나 기존 Astro 사이트에 같은 구성을 적용하세요.

사전 요구 사항

  • Cloudflare 계정
  • 설치된 프로젝트 의존성
  • Cloudflare에 인증된 Wrangler(pnpm wrangler login)

바인딩 구성

Cloudflare 템플릿에는 완전한 Worker 진입점과 이름이 지정된 D1 및 R2 바인딩이 포함되어 있습니다. 첫 배포 시 구성된 이름의 리소스가 아직 없으면 Wrangler가 해당 리소스를 만듭니다. wrangler.jsonc의 이름을 그대로 유지하세요. 이후 배포에서는 Wrangler가 같은 리소스에 다시 연결합니다.

템플릿은 다음 바인딩을 사용합니다.

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "my-emdash-site",
	"main": "./src/worker.ts",
	"compatibility_date": "2026-02-24",
	"compatibility_flags": ["nodejs_compat"],

	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "my-emdash-site",
		},
	],

	"r2_buckets": [
		{
			"binding": "MEDIA",
			"bucket_name": "my-emdash-media",
		},
	],
	"worker_loaders": [{ "binding": "LOADER" }],
	"triggers": { "crons": ["* * * * *"] },
}

DB, MEDIA, LOADER 이름은 EmDash 어댑터와 일치해야 합니다. Cron Trigger는 예약 게시, 플러그인 작업, 백업, 유지 관리를 실행합니다. 사이트에서 샌드박스 플러그인을 사용한다면 Plugin sandbox를 참고하세요.

EmDash 구성

다음 Astro 구성은 D1 및 R2 바인딩을 사용합니다.

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import emdash from "emdash/astro";
import { d1, r2, sandbox } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // Required — the admin UI is a React app
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
			sandboxRunner: sandbox(),
		}),
	],
});

사이트에서 marketplace, registry 또는 sandboxed 플러그인을 사용하지 않는다면 sandboxRunner와 LOADER 바인딩을 생략할 수 있습니다.

Worker 진입점 추가

Worker 진입점은 Astro를 Cron Trigger에 연결하고 플러그인 브리지를 내보냅니다.

import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

export default {
	...handler,
	scheduled: createScheduledHandler(),
} satisfies ExportedHandler;

샌드박스 플러그인이 설치되어 있지 않아도 PluginBridge 내보내기는 무해합니다. 같은 프로젝트에서 나중에 플러그인을 활성화할 수 있다면 그대로 두세요.

일반 유지 관리를 매분이 아닌 다른 일정으로 실행하려면, 같은 Cron 표현식을 createScheduledHandler({ generalCron: "..." })와 triggers.crons에 모두 전달하세요. 둘이 다르면 핸들러는 예상치 못한 트리거를 로그에 기록하고 무시합니다.

빌드와 배포

사이트를 한 번 빌드하고 배포해 Wrangler가 이름이 지정된 D1 데이터베이스와 R2 버킷을 프로비저닝하게 합니다. Wrangler는 pnpm wrangler login으로 생성된 로컬 로그인을 사용합니다.

pnpm build
pnpm wrangler deploy

기본 auto 마이그레이션 모드에서는 배포된 Worker가 첫 요청을 받을 때 EmDash가 대기 중인 코어 마이그레이션을 적용합니다. 새 코드가 트래픽을 받기 전에 배포 파이프라인에서 마이그레이션을 적용해야 하거나, 마이그레이션을 검사·확인·복구해야 할 때는 Manage core database migrations를 사용하세요.

설정 마법사를 완료하기 전까지 EmDash는 첫 요청 시 시드 파일의 스키마와 구조도 한 번 적용합니다. 시드의 샘플 콘텐츠는 관리자가 설정 마법사에서 선택할 때만 적용됩니다. 시드는 빌드 시점에 .emdash/seed.json, package.json#emdash.seed의 경로, seed/seed.json 중 먼저 발견되는 것에서 읽혀 번들에 인라인됩니다. 어느 것도 없으면 기본 제공 시드가 사용됩니다. 기존 데이터베이스에 대한 이후 배포는 그 콘텐츠를 건드리지 않습니다.

이미 배포된 사이트의 스키마나 콘텐츠 모델을 변경하려면 Evolving a Deployed Site를 참고하세요.

예약 게시

Cloudflare에서 EmDash는 Worker의 scheduled 핸들러에서만 예약된 항목을 게시하며, 이 핸들러는 Cron Trigger의 일정에 따라 Cloudflare가 호출합니다. 배포된 Worker에는 다음 두 가지가 모두 필요합니다.

  • wrangler.jsonc의 triggers.crons 목록에 있는 Cron Trigger(바인딩 구성에 나온 것처럼).
  • @emdash-cms/cloudflare/worker의 scheduled 핸들러. main이 가리키는 Worker 진입점에서 내보내야 합니다(Worker 진입점 추가에 나온 것처럼).

둘 중 하나라도 없으면 예약된 항목은 공개되지 않습니다. astro dev를 사용하는 로컬 개발에서는 EmDash가 타이머로 예약된 항목을 게시하므로, 로컬에서 동작하는 일정이 배포된 구성을 보증하지는 않습니다.

프로젝트 루트에서 emdash doctor를 실행해 로컬 프로젝트 파일의 Cron Trigger와 scheduled 핸들러를 확인합니다.

pnpm emdash doctor

emdash doctor가 둘 다 찾으면 출력에 통과한 scheduler wiring 검사가 포함됩니다. 스케줄러 검사가 실패하면 문제를 설명하고 해결 방법을 출력합니다. 이 검사는 이 페이지에 나온 Worker 진입점과, 해결 방법으로 출력되는 한 줄짜리 재내보내기를 인식합니다. 따라서 다른 방식으로 작성된 진입점은 잘 동작하더라도 검사에 실패할 수 있습니다.

스케줄러 검사가 나타나지 않는다면, emdash doctor가 현재 디렉터리에서 Wrangler 구성을 찾지 못했거나 Cron Trigger와 scheduled 핸들러를 모두 찾지 못한 것입니다. D1 프로젝트에서는 database 검사가 로컬 SQLite 파일(기본값 ./data.db)을 찾기 때문에 실패합니다. 이 실패는 스케줄러 검사에 영향을 주지 않습니다.

Cron Trigger나 scheduled 핸들러를 추가하거나 변경한 후에는 다시 배포하세요. Cloudflare가 새로 만들거나 변경한 Cron Trigger를 실행하기 시작하는 데 최대 15분이 걸릴 수 있습니다. 트리거가 실행되기 시작하면 EmDash는 예약 시간이 이미 지난 항목을 게시합니다.

트리거를 확인하려면 pnpm wrangler tail을 실행하세요. 실행할 때마다 "* * * * *" @ 9/29/2026, 10:01:00 AM - Ok 같은 줄이 출력됩니다. Ok는 해당 실행이 잡히지 않은 오류 없이 끝났다는 뜻입니다. EmDash는 게시 오류를 잡아 로그에 기록하므로, Ok가 항목이 게시되었음을 확인해 주지는 않습니다. 항목을 게시한 실행은 [scheduled] Published 1 scheduled item(s) 같은 메시지도 기록하며, 해당 실행의 오류도 같은 출력에 나타납니다.

배포 후 예약된 항목이 예약 시간을 지났는데 최근 5분 안에 완료된 스케줄러 실행이 없으면, 관리자 대시보드에 Scheduled publishing needs attention이 표시됩니다. Cron Trigger가 5분보다 드물게 실행된다면, 다음 실행에서 해당 항목이 게시될 때까지 실행 사이에도 이 알림이 나타날 수 있습니다.

Worker를 D1 가까이에 배치

Cloudflare는 기본적으로 방문자와 가까운 곳에서 Worker를 실행합니다. EmDash의 서버 렌더링 요청은 D1과 여러 번 왕복하므로, Targeted Placement를 사용해 Worker를 D1 프라이머리 가까이에서 실행하면 이러한 요청이 더 빨라집니다.

Wrangler는 선택자를 정확히 하나만 지정한 placement.mode: "targeted"를 받아들입니다. 선택자는 region, host, hostname 중 하나입니다. D1 프라이머리 위치를 가리키는 값을 선택하고, 그 결과로 얻은 placement 객체를 wrangler.jsonc에 추가하세요. Targeted Placement와 함께 D1 읽기 복제본을 활성화하지 마세요. EmDash의 session 설정은 기본값 "disabled"로 유지해 읽기와 쓰기가 가까운 프라이머리를 사용하게 하세요.

객체 캐시

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는 Worker 앞단에 엣지 캐시를 둡니다. 일치하는 요청은 Worker를 전혀 실행하지 않고 제공됩니다.

활성화

  1. Astro의 Cloudflare 캐시 프로바이더를 사용해 라우트 규칙과 Astro.cache가 캐시 헤더를 설정하게 하고, 무효화에는 cache.purge()를 사용합니다.

    import { cacheCloudflare } from "@astrojs/cloudflare/cache";
    
    export default defineConfig({
     adapter: cloudflare(),
     cache: {
       provider: cacheCloudflare(),
     },
     routeRules: {
       "/": { maxAge: 300, swr: 86400 },
       // Other public routes can use different cache lifetimes.
     },
    });

    @astrojs/cloudflare 어댑터는 cacheCloudflare()를 감지하고 생성된 배포 구성에서 Workers Cache를 활성화합니다.

  2. 플랫폼 API로 Worker 코드에서 캐시된 응답을 퍼지합니다. 이 호출에는 Cloudflare REST 자격 증명이 필요하지 않습니다.

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // Or purge selected tags:
    await cache.purge({ tags: ["posts"] });

EmDash 관리자 및 API 응답은 이미 Cache-Control: private, no-store를 보내며 저장되지 않습니다. 공개 페이지는 Cache-Control / routeRules / Astro.cache를 통해 자체 캐싱을 제어합니다.

활성화하기 전에 알아 둘 점이 두 가지 있습니다.

  1. Cache-Control 헤더가 없는 응답도 캐시됩니다. Workers Cache는 RFC 9111 휴리스틱 신선도를 적용합니다. 헤더가 전혀 없는 200은 2시간 동안 캐시됩니다. 모든 커스텀 라우트에 명시적인 Cache-Control을 지정하세요(세션에 의존하는 모든 것에는 private, no-store를 사용합니다).
  2. 캐시된 페이지는 로그인한 편집자와 공유됩니다. 캐시는 Worker보다 먼저 실행되므로 요청 쿠키를 기준으로 우회할 수 없습니다. 로그인한 편집자는 항목이 만료될 때까지 공개 페이지의 캐시된 익명 버전(시각적 편집 도구 모음이 없는 버전)을 받을 수 있습니다. 편집자용으로 렌더링된 응답 자체는 저장되지 않으므로(private, no-store가 붙습니다), 반대 방향으로는 아무것도 유출되지 않습니다.

Workers Caching은 객체 캐시(objectCache: kvCache({ binding: "CACHE" }))와 별개이며, 객체 캐시는 Worker 아래의 KV에 데이터베이스 쿼리 결과를 캐시합니다.

커스텀 도메인

첫 배포에서는 workers.dev URL이 부여됩니다. 커스텀 도메인은 Worker와 같은 계정에서 Cloudflare가 관리하는 활성 도메인이어야 합니다. Worker가 workers.dev URL에서 정상적으로 응답한 뒤에 프로덕션 도메인을 Wrangler 라우트로 추가하세요.

{
	"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}

다시 배포하고 두 주소를 모두 확인하세요. DNS를 테스트하는 동안 workers.dev 주소를 계속 사용할 수 있게 두면 라우팅 문제와 애플리케이션 문제를 구분하는 데 도움이 됩니다.

siteUrl, EMDASH_SITE_URL, SITE_URL이 모두 없으면 설정은 실행 중인 주소를 사이트의 오리진으로 기록하며, 설정 중에 만든 패스키는 그 주소에서만 동작합니다. 프로덕션 도메인에서 설정을 실행하거나, 설정 전에 EMDASH_SITE_URL을 해당 도메인으로 지정하세요. 설정 후 EMDASH_SITE_URL을 다른 도메인으로 지정하면 로그인이 그 도메인으로 옮겨지고, 이전 주소에서 만든 패스키는 그 도메인에서 더 이상 동작하지 않습니다.

공개 R2 접근

기본적으로 미디어는 EmDash의 인증된 미디어 라우트를 통해 제공됩니다. 버킷에 공개 커스텀 도메인이 있다면 해당 오리진을 publicUrl로 설정해 생성되는 미디어 URL이 이를 사용하게 하세요.

storage: r2({
	binding: "MEDIA",
	publicUrl: "https://media.example.com",
}),

버킷의 공개 접근은 미디어만이 아니라 접근 가능한 모든 객체에 적용됩니다. 자동 JSON 백업은 같은 스토리지 백엔드의 backups/ 접두사를 사용하므로, 그 접두사를 공개 도메인으로 노출하지 마세요. Choose media storage에서 안전한 경계를 설명합니다.

이미지 변환

EmDash는 Cloudflare의 IMAGES 바인딩을 통해 Worker 안에서 R2 미디어의 크기를 조정하고 다시 인코딩합니다. emdash/ui의 Image 컴포넌트와 리치 텍스트의 이미지는 모두 Cloudflare 어댑터 아래에서 EmDash가 설치하는 이미지 엔드포인트를 통해 렌더링됩니다. 내부 /_emdash/api/media/file/… 라우트의 미디어에 대해서는 이 엔드포인트가 HTTP 페치 없이 R2 바인딩에서 원본 바이트를 직접 읽습니다. 이러한 변환은 Cloudflare Access 뒤에서도, 그리고 global_fetch_strictly_public을 사용할 때도 계속 동작합니다. 버킷 URL로 제공되는 미디어(공개 R2 접근 참고)는 대신 어댑터 자체의 변환 엔드포인트를 사용하며, 이 엔드포인트는 변환 전에 HTTP로 파일을 가져옵니다.

바인딩을 선언할 필요는 없습니다. @astrojs/cloudflare가 astro build 중에 생성하는 Worker 구성에 이를 추가하며, 이는 Workers Caching을 위해 cache를 추가하는 방식과 같습니다. 런타임 이미지 서비스가 cloudflare-binding일 때마다 그렇게 합니다. 즉 imageService가 설정되지 않았거나, 그 문자열 자체이거나, { runtime: "cloudflare-binding" }일 때입니다. 그 밖의 모든 값("passthrough", "compile", "cloudflare", "custom")은 바인딩을 추가하지 않습니다. 직접 wrangler.jsonc에 나열해 두면 의도가 분명해집니다.

{
	"images": {
		"binding": "IMAGES",
	},
}

배포가 실제로 무엇을 받는지 확인하려면 wrangler.jsonc가 아니라 생성된 구성을 읽으세요. 빌드는 .wrangler/deploy/config.json을 쓰며, 이 파일이 wrangler deploy를 병합된 파일(기본값 dist/server/wrangler.json)로 안내합니다. 거기서 images 항목을 찾아보세요.

Cloudflare는 이러한 변환을 Images transformations로 청구합니다. 원본 이미지와 매개변수의 고유한 조합마다 달력 월 기준으로 한 번 청구되며, 같은 달 안의 반복 요청은 무료입니다. 사이트에 원본 이미지가 500개 있고 모든 이미지에 대해 썸네일 크기 하나와 히어로 크기 하나를 요청하면, 두 매개변수 세트는 그 달에 변환된 이미지 1,000개로 계산됩니다. Images Free 플랜은 월 5,000건의 고유 변환을 포함합니다. 이 한도를 넘으면 캐시된 변환은 계속 제공되지만, 새 변환은 9422 오류를 반환하고 이미지 요청이 실패합니다.

Cloudflare Access 인증

Cloudflare Access는 Access 애플리케이션에 연결된 ID 프로바이더로 패스키 인증을 대체할 수 있습니다. 대상(audience) 값은 비밀 런타임 설정입니다. 환경 변수 이름을 지정해 astro.config.mjs에 넣지 않도록 하세요.

import { access } from "@emdash-cms/cloudflare";

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50,
			Editors: 40,
		},
	}),
}),

pnpm wrangler secret put CF_ACCESS_AUDIENCE로 CF_ACCESS_AUDIENCE를 설정하세요. authentication guide에서 사용자 프로비저닝, 기본 역할, 역할 동기화를 설명합니다.

이메일

프로덕션 Worker에는 기본 이메일 전송 서비스가 없습니다. 이메일 플러그인이 활성화될 때까지 매직 링크 로그인, 팀 초대, 댓글 알림은 Email is not configured를 반환합니다.

Cloudflare 이메일 플러그인은 send_email 바인딩을 사용합니다. 먼저 Cloudflare Email Sending으로 발신자 도메인을 등록하고 확인하세요. Cloudflare는 From 주소가 승인된 발신자가 아닌 메시지를 거부합니다.

바인딩을 추가하고 프로바이더를 등록하세요.

{
	"send_email": [{ "name": "EMAIL" }],
}
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [
		cloudflareEmail({
			from: { email: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]",
		}),
	],
}),

배포 후 Extensions에서 플러그인을 활성화하고 Settings → Email에서 선택하세요. 발신자가 승인되고 바인딩이 존재할 때까지 전송은 실패합니다.

플러그인은 binding 옵션이 다른 이름을 지정하지 않는 한 EMAIL이라는 이름의 바인딩을 사용합니다. 활성화된 이메일 프로바이더가 이것 하나뿐이면 EmDash가 자동으로 선택합니다. 프로바이더가 둘 이상 활성화되어 있다면 Settings → Email에서 Cloudflare 프로바이더를 선택하세요. 선택 사항인 replyTo 주소는 승인된 From 주소를 바꾸지 않고 회신을 받습니다. 플러그인은 개별 메시지에 replyTo를 설정할 수 있으며, 그 메시지에 대해서는 이 옵션을 덮어씁니다.

AI Search 플러그인에는 네이티브 플러그인 등록과 ai_search_namespaces 바인딩이 모두 필요합니다. 이를 배포한 뒤 관리자에서 Cloudflare AI Search를 열고, 컬렉션을 선택한 다음 Sync All Content를 실행하세요. 초기 동기화는 플러그인을 활성화하기 전에 게시된 콘텐츠를 인덱싱하며, 이후 변경 사항은 hooks가 동기화된 상태로 유지합니다.

import { aiSearch } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [aiSearch()],
}),
{
	"ai_search_namespaces": [{ "binding": "AI_SEARCH", "namespace": "default" }],
}

사이트에서 검색 라우트를 노출하세요.

export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";

레이아웃에 검색 인터페이스를 추가하세요. 트리거 슬롯은 사이트 디자인에 맞는 버튼을 받습니다.

---
import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
---

<AISearchSnippet apiUrl="/api/ai-search" placeholder="Search content">
	<button slot="trigger" type="button">Search</button>
</AISearchSnippet>

Worker 시크릿

시크릿 값은 pnpm wrangler secret put <NAME>으로 저장하세요. wrangler.jsonc에 넣거나 빌드 시점의 import.meta.env 값에서 읽지 마세요.

EMDASH_ENCRYPTION_KEY는 시크릿으로 선언된 플러그인 설정을 암호화합니다. 플러그인 시크릿을 저장하기 전에 설정하고, D1 백업과는 별도로 보관하세요. 교체하는 동안에는 새 키를 먼저 제공하고, 모든 플러그인 시크릿이 다시 저장될 때까지 이전 키를 쉼표 뒤에 유지하세요. 교체와 복구는 Secrets and key management에서 설명합니다.

플러그인 브리지는 이 Worker 시크릿 바인딩을 직접 읽습니다. 생성된 관리자 설정 라우트는 process.env를 통해 읽습니다. nodejs_compat을 사용하면 Cloudflare는 호환성 날짜가 2025-04-01 이상일 때 기본적으로 process.env를 채웁니다. 더 이른 날짜로 고정된 프로젝트는 암호화된 설정을 저장하기 전에 nodejs_compat_populate_process_env도 추가해야 합니다.

EmDash는 런타임에 process.env에서 시크릿을 읽습니다. Worker 코드는 cloudflare:workers에서 가져온 env에서 바인딩을 읽습니다. 시크릿을 import.meta.env로 읽지 마세요. Vite는 이 값들을 빌드 시점에 치환하며, 서버 번들에 기록할 수 있습니다.

미리보기 HMAC 시크릿과 댓글 작성자 IP 솔트는 런타임 재정의를 제공하지 않으면 생성되어 데이터베이스에 저장됩니다. 정확한 변수, 저장 위치, 교체 시 영향은 Secrets and key management에 나열되어 있습니다.

미리보기 배포

이름이 지정된 Wrangler 환경은 바인딩을 상속하지 않습니다. 별도의 미리보기 리소스를 만들고 빌드 전에 preview 환경에 기록하세요.

pnpm wrangler d1 create my-emdash-site-preview \
  --binding DB --env preview --update-config
pnpm wrangler r2 bucket create my-emdash-media-preview \
  --binding MEDIA --env preview --update-config

미리보기 환경은 미리보기 Worker가 사용하는 모든 바인딩을 반복해서 지정해야 합니다. Wrangler가 리소스 식별자를 기록한 뒤의 코어 D1, R2, 샌드박스 바인딩은 다음과 같은 형태입니다.

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "my-emdash-site-preview",
					"database_id": "00000000-0000-0000-0000-000000000000",
				},
			],
			"r2_buckets": [
				{
					"binding": "MEDIA",
					"bucket_name": "my-emdash-media-preview",
				},
			],
			"worker_loaders": [{ "binding": "LOADER" }],
		},
	},
}

Wrangler가 기록한 미리보기 UUID를 사용하세요. 미리보기에서 해당 기능을 사용한다면 선택 사항인 KV, AI Search, 이메일 및 기타 바인딩도 반복해서 지정하세요. 미리보기 전용 시크릿은 pnpm wrangler secret put <NAME> --env preview로 추가하세요.

미리보기 환경을 빌드하고 배포하세요. 첫 요청이 기본 auto 모드를 통해 대기 중인 코어 마이그레이션을 적용합니다.

pnpm build
pnpm wrangler deploy --env preview

공유하기 전에 미리보기 URL, 관리자 로그인, 미디어 업로드, 그리고 선택적 바인딩을 확인하세요. 미리보기 바인딩을 프로덕션 데이터베이스나 버킷에 연결하지 마세요.

배포 확인

배포 후 공개 페이지 하나를 요청하고, /_emdash/admin에 로그인하고, 테스트 미디어 파일을 업로드하고 가져온 다음, pnpm wrangler tail에 scheduled 핸들러가 나타나는지 확인하세요.

문제 해결

”D1 binding not found”

wrangler.jsonc의 바인딩 이름이 데이터베이스 구성과 일치하는지 확인하세요.

// Must match: d1({ binding: "DB" })
"binding": "DB"

”R2 binding not found”

R2 버킷이 올바르게 바인딩되어 있는지 확인하세요.

// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"

마이그레이션 오류

스키마 오류가 보이면 Worker 로그를 tail(wrangler tail)하고 오류를 재현해 근본 메시지를 확보한 다음, 그 출력과 함께 이슈를 등록하세요.