EmDash는 Astro와 Cloudflare 위에 TypeScript로 처음부터 다시 짠 풀스택 CMS다. 공식 슬로건은 "the spiritual successor to WordPress(워드프레스의 정신적 후계자)". 즉 워드프레스가 잘했던 것(확장성·관리자 UX·플러그인 생태계)은 가져오되, 늙은 PHP 구조 대신 타입 안전·서버리스 토대 위에 다시 세운 것이다.
WordPress가 만들어진 2003년엔 PHP가 합리적인 선택이었다. 하지만 지금은 자바스크립트가 풀스택을 점령했고, 서버는 "내가 빌려 켜두는 것"에서 "전 세계 에지에 자동 배포되는 것"으로 바뀌었다. EmDash는 "지금 처음부터 다시 짠다면?"에 대한 진지한 대답이다.
핵심은 두 가지다. ① 플러그인 보안 — 모든 플러그인을 격리된 샌드박스에서 돌려 한 플러그인이 사이트 전체를 뚫지 못하게 막는다. ② AI 네이티브 — Claude 같은 AI 에이전트가 직접 콘텐츠를 읽고 쓰도록 MCP 서버가 인스턴스마다 기본 내장된다.
이 프로젝트가 화제가 된 결정적 이유는 "누가 만들었나"다. EmDash는 개인 사이드 프로젝트가 아니라 Cloudflare가 AI 코딩 에이전트로 약 2개월간 개발한 결과물이다. "AI 에이전트로 워드프레스를 통째로 다시 짠다"는 시도 자체가 2026년적 화두였다.
Cloudflare 분석에 따르면 WordPress 사이트 보안 사고의 96%가 플러그인에서 발생한다. 2025년 한 해에 발견된 고위험(high-severity) 취약점이 직전 2년을 합친 것보다 많았다. 이유는 단순하다 — PHP 플러그인은 데이터베이스·파일시스템·다른 플러그인 어디든 격리 없이 접근할 수 있다. 인기 플러그인 하나가 뚫리면 그걸 설치한 수만 개 사이트가 동시에 털린다.
EmDash는 플러그인 하나하나를 Worker isolate(워커 격리 환경)에서 돌린다. 플러그인은 매니페스트에 선언한 권한만 쓸 수 있다 — 예: content:read(콘텐츠 읽기), email:send(메일 발송). 명시 허가 없이는 외부 네트워크도 못 건드린다. 사고가 나도 그 플러그인 안에서 끝난다.
WordPress 방식은 입주민(플러그인)이 들어올 때 마스터키를 그대로 주는 아파트다. 한 명이라도 악의가 있으면 모든 집 문을 다 연다.
EmDash 방식은 입주민마다 자기 집 열쇠만 주는 아파트다. 옆집을 만지려면 따로 허가를 받아야 하고, 사고가 나도 그 집에서 끝난다.
| 비교 대상 | EmDash의 차별점 |
|---|---|
| WordPress | PHP·격리 없는 플러그인 → TypeScript·샌드박스 플러그인. GPL → 더 자유로운 MIT 라이선스. 서버 상주 → 서버리스 에지 배포 |
| Sanity / Contentful (헤드리스 CMS) | 별도 API 서버를 띄우는 대신 Astro 콘텐츠 레이어에 직접 통합 — 네트워크 호출 한 단계를 없앰. Portable Text 개념은 계승 |
| Strapi (오픈소스 헤드리스) | Node 서버 상주형 vs EmDash는 서버리스 우선. DB/스토리지가 어댑터로 교체 가능(D1·SQLite·PostgreSQL·R2·S3) |
| 일반 CMS 전반 | MCP 서버 기본 내장 — AI 에이전트가 사람의 클릭 없이 직접 글을 쓰고 미디어를 올림. 2026년 시점의 차별 포인트 |
※ 비율은 GitHub 표기 기준의 근사치이며 커밋에 따라 변동한다. WordPress가 PHP 중심인 것과 정반대로, EmDash는 위아래 전 계층이 TypeScript 하나로 통일돼 있다는 점이 핵심.
| 기술 | 역할 | 상세 |
|---|---|---|
| Astro | 프론트 프레임워크 + 통합 진입점 | EmDash는 "Astro integration"으로 동작 — config에 추가하면 관리자 패널·REST API·인증·미디어·플러그인이 한 번에 붙는다 |
| TypeScript | 전 계층 언어 | 코어·어댑터·플러그인 런타임까지 하나의 타입 시스템으로 통일 |
| TipTap | 리치 텍스트 에디터 | Notion 스타일 블록 에디터의 토대. 입력 결과는 Portable Text로 저장 |
| Kysely | 타입 안전 SQL 빌더 | SQL 방언 차이(SQLite·PostgreSQL 등)를 흡수 → DB 교체를 가능케 하는 핵심 |
| Portable Text | 콘텐츠 저장 포맷 | HTML 대신 구조화된 JSON 트리로 저장. 한 콘텐츠를 여러 렌더러가 재해석 |
| 계층 | 선택지 | 비고 |
|---|---|---|
| 데이터베이스 | D1 · SQLite · Turso/libSQL · PostgreSQL | Kysely가 방언 차이를 흡수해 코드 변경 최소 |
| 파일 스토리지 | R2 · AWS S3 · 로컬 파일시스템 | 공통 S3 API로 추상화 |
| 세션 / KV | Cloudflare KV · Redis · 파일 기반 | 배포 환경에 맞춰 선택 |
| 인증 | WebAuthn/Passkey · OAuth · 매직 링크 | 비밀번호 무차별 대입 공격 원천 차단 |
| 기술 | 역할 | 상세 |
|---|---|---|
| Cloudflare Workers | 서버리스 실행 | 전 세계 에지에 자동 배포. 플러그인 샌드박스도 Dynamic Workers 기반 |
| pnpm (모노레포) | 패키지 관리 | 여러 패키지를 한 저장소에서 관리. pnpm build / test / typecheck |
| MCP 서버 | AI 연동 | 모든 EmDash 인스턴스가 자체 원격 MCP 서버를 제공 |
| x402 | 콘텐츠 결제 | HTTP 402(Payment Required) 기반 콘텐츠 수익화 실험 |
EmDash 아키텍처에서 가장 흥미로운 부분은 "인프라를 갈아끼울 수 있게 만든 추상화"다. Cloudflare에 배포하다가 AWS나 자체 서버로 옮기고 싶을 때, 설정 한 줄만 바꾸면 동작한다.
한국 콘센트에 미국 플러그를 꽂으려면 어댑터가 필요하다. 어댑터 덕분에 같은 충전기(비즈니스 로직)를 어디서나 쓸 수 있다.
EmDash의 DB 어댑터도 똑같다. 같은 로직이 어댑터만 바꾸면 D1·SQLite·PostgreSQL·Turso 어디서든 동작한다. 이게 가능한 건 Kysely가 SQL 방언 차이를 흡수해주기 때문 — 인터페이스 기반 의존성 역전 원칙(DIP)의 실전 사례다.
보통 헤드리스 CMS는 별도 REST API 서버를 띄우고 프론트가 그걸 호출한다. EmDash는 그 단계를 없앴다. Astro 콘텐츠 레이어에 CMS가 통합돼 있어 코드에서 직접 콘텐츠를 가져온다.
await getCollection("posts") 한 줄로 DB에서 직접 글을 읽는다. API 호출 단계가 사라져 빌드 시간과 응답 속도가 함께 개선된다.
WordPress가 HTML을 DB에 통째로 박아두는 것과 정반대로, EmDash는 콘텐츠를 구조화된 JSON으로 저장한다. 같은 글을 웹·앱·음성비서 등 여러 매체가 각자 다르게 렌더링할 수 있고, AI가 의미를 역추론할 필요도 없다.
모든 EmDash 인스턴스는 자체 원격 MCP 서버를 제공한다. "이번 주 신상품 5개 블로그 글 써줘"를 Claude에게 시키면, Claude가 직접 EmDash에 접속해 글을 쓰고 미디어를 올린다. 사람의 클릭 없이 콘텐츠가 흐른다.
EmDash는 모노레포(여러 패키지를 한 저장소에서 관리)다. 관심 분야만 골라 읽을 수 있다. 실제 루트에는 packages/ 외에도 apps/, templates/, demos/, docs/, e2e/ 등이 더 있다. 아래는 핵심인 packages/ 내부만 발췌한 것이다.
패키지 이름만 봐도 EmDash의 야심이 보인다 — 인증·에디터·인프라 어댑터·마이그레이션·마켓플레이스·결제까지 CMS 한 채에 필요한 모든 조각을 따로 패키지로 분리해 두었다. 처음 본다면 core/(전체 흐름) → cloudflare/(어댑터 패턴) → auth/(Passkey 구현) 순서로 읽는 걸 권한다.
EmDash는 "쓰는 도구"이자 "읽고 배우는 교본"이다. 잘 쓰인 오픈소스를 정독하면 직접 쓸 일이 없어도 좋은 구조가 머리에 남는다.
배울 것: 위아래 전 계층을 하나의 타입 시스템으로 묶는 법. pnpm 워크스페이스로 18개+ 패키지를 분리하고 의존성을 관리하는 실전 구조. 스키마에서 타입을 뽑아내는 npx emdash types 같은 코드 생성 흐름.
배울 것: packages/cloudflare/가 핵심 교본. D1·R2·Worker Loader를 어떻게 인터페이스 뒤로 숨겨 SQLite ↔ PostgreSQL ↔ D1을 교체 가능하게 만들었는지. Kysely로 SQL 방언 차이를 흡수하는 패턴.
배울 것: Worker isolate에서 남의 코드를 안전하게 실행하는 보안 모델. 매니페스트로 권한을 선언하고 런타임이 강제하는 capability-based security. "남의 코드를 내 시스템에서 돌릴 때"의 정석.
배울 것: packages/auth/는 Passkey 우선 + OAuth/매직링크 폴백의 완전한 구현체. 비밀번호 없는 인증(WebAuthn 스펙)을 실제로 어떻게 붙이는지 학습하기 좋다.
배울 것: 콘텐츠를 HTML이 아닌 JSON 트리로 저장하는 발상의 전환. packages/blocks/에서 블록 타입 정의를, TipTap 에디터에서 입력 → Portable Text 직렬화 흐름을 볼 수 있다.
배울 것: 앱에 MCP 서버를 붙여 AI 에이전트가 직접 조작하게 만드는 패턴. 그리고 gutenberg-to-portable-text/처럼 기존 생태계 데이터를 손실 없이 이전하는 변환기 설계 — 새 도구가 채택되려면 반드시 필요한 다리다.
| 항목 | 로컬 개발 / Safe 모드 | 풀 기능 운영 |
|---|---|---|
| 런타임 | Node.js (LTS 권장) | Cloudflare Workers (에지) |
| 데이터베이스 | 로컬 SQLite 파일 | D1 / Turso / PostgreSQL |
| 스토리지 | 로컬 파일시스템 | R2 / AWS S3 |
| 플러그인 샌드박스 | 제한적(safe 모드) | Dynamic Workers 필요 → Cloudflare 유료($5/월~) |
| 패키지 매니저 | pnpm (모노레포 개발 시) | npm create emdash (사용 시) |
| 계정 | 불필요 (로컬 완결) | Cloudflare 계정 (유료 권장) |
EmDash의 간판 기능인 플러그인 격리는 Cloudflare의 Dynamic Workers에 의존한다. 이건 유료 계정($5/월 이상)에서만 동작한다. 무료로 쓰려면 safe 모드(플러그인 샌드박스 제한)로 가야 한다. 로컬 개발은 Node.js + SQLite로 계정 없이 완전히 돌릴 수 있다.
| 시작 방법 | 장점 | 단점/조건 |
|---|---|---|
npm create emdash@latest | 5분 안에 로컬 띄우기, 계정 불필요 | 샌드박스 플러그인은 제한 |
| Cloudflare 배포 버튼 | 에지 글로벌 배포 자동 | 유료 계정 필요(샌드박스용) |
| Node.js 서버 + SQLite | 자체 서버 운영 가능 | 에지 CDN 이점 없음 |
계정 없이 로컬에서 EmDash를 띄우고 관리자 패널을 체험한다. 포스트 3개를 쓰고 Passkey 등록까지 해본다.
# 한 줄로 스캐폴딩 (Node.js + SQLite, 계정 불필요)
npm create emdash@latest my-blog
cd my-blog
npm run dev # → 관리자 패널 접속, 포스트 작성, Passkey 등록
EmDash가 콘텐츠를 어떻게 저장하는지 DB를 직접 들여다본다. HTML이 아니라 JSON 트리로 저장된 걸 눈으로 확인하면 콘텐츠 모델링의 새 시각이 열린다.
.sqlite 파일을 열고 → posts 테이블의 content 컬럼을 확인 → Portable Text JSON 구조(블록 배열, 마크 정의)를 관찰한다.옛 블로그가 있다면 WordPress 익스포트(WXR XML)를 받아 EmDash 임포트 마법사에 넣는다. Gutenberg 블록이 Portable Text로 어떻게 변환되는지 직접 본다.
gutenberg-to-portable-text 변환 결과를 포스트별로 비교(이미지·링크·SEO 메타 보존 여부 확인).최소 권한만 선언한 1st-party 스타일 플러그인을 만든다. 매니페스트에 선언하지 않은 동작을 시도하면 런타임이 막는 걸 확인한다.
capabilities: ["content:read"]만 선언 → 콘텐츠 읽기 동작 구현 → 일부러 email:send를 선언 없이 호출 → 런타임이 거부하는지 검증. packages/plugins/의 forms·audit-log 플러그인을 레퍼런스로.EmDash 인스턴스의 MCP 서버를 Claude Desktop/Code에 등록해, AI가 직접 글을 쓰고 미디어를 올리게 한다. "이 글 더 길게 다시 써줘"가 새로운 운영 방식이 된다.
# Claude Desktop 설정에 EmDash MCP 서버 등록 (개념 예시)
{
"mcpServers": {
"emdash": {
"url": "https://my-blog.example.com/_mcp",
"headers": { "Authorization": "Bearer <토큰>" }
}
}
}
# → Claude에게: "신상품 5개에 대한 블로그 초안을 EmDash에 작성해줘"
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | EmDash 설치 & 관리자 패널 | npm create emdash로 로컬 기동 |
| 화 | Astro 통합 개념 | astro.config에서 EmDash가 붙는 방식 관찰 |
| 수 | Portable Text 구조 | SQLite에서 content 컬럼 JSON 뜯어보기 |
| 목 | TipTap 에디터 | 블록 추가 → 저장 → 직렬화 결과 비교 |
| 금 | Live Collections | getCollection("posts")로 직접 쿼리 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | WebAuthn/Passkey 개념 | Passkey 등록·로그인 흐름 체험 |
| 화 | packages/auth/ 정독 | Passkey 우선 + OAuth 폴백 코드 읽기 |
| 수 | Kysely 쿼리 빌더 | 타입 안전 SELECT/INSERT 작성 |
| 목 | DB 어댑터 교체 | SQLite → PostgreSQL 설정 바꿔보기 |
| 금 | 스토리지 S3 추상화 | 로컬 파일 → R2/S3 어댑터 이해 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | Worker isolate 격리 개념 | 샌드박스가 무엇을 막는지 정리 |
| 화 | capability 매니페스트 | 권한 선언 → 런타임 거부 실험 |
| 수 | 1st-party 플러그인 분석 | forms·audit-log·ai-moderation 코드 읽기 |
| 목 | plugin-cli로 스캐폴딩 | 나만의 플러그인 뼈대 생성 |
| 금 | 훅 & 관리자 페이지 | 플러그인이 관리자 UI에 끼는 법 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | MCP 프로토콜 개념 | MCP 공식 문서 + EmDash MCP 서버 등록 |
| 화 | AI로 콘텐츠 자동화 | Claude로 초안 작성 → 검토 워크플로 |
| 수 | Gutenberg 변환기 | WXR 임포트 → 변환 결과 비교 |
| 목 | Cloudflare 배포 | D1+R2+Workers로 실제 에지 배포 |
| 금 | 종합 프로젝트 | 임포트 + 플러그인 + MCP 결합 블로그 운영 |
| 키워드 | 설명 |
|---|---|
| CMS | 콘텐츠 관리 시스템. 코드 없이 글·미디어를 웹에 올리게 해주는 관리 도구 |
| 헤드리스 CMS | 화면(프론트)과 콘텐츠 저장소를 분리한 CMS. 보통 API로 콘텐츠를 내려준다 |
| Astro | "필요한 곳에만 JS를 보내는" 웹 프레임워크. EmDash는 이것의 integration으로 동작 |
| 서버리스 | "서버가 없다"가 아니라 "내가 서버를 관리하지 않는다". 요청 올 때만 실행되고 사용량만큼 과금 |
| 에지 컴퓨팅 | 코드를 전 세계 수백 지역에 미리 복사 → 사용자 가까운 곳에서 응답 |
| Cloudflare Workers | 전 세계 에지에 자동 배포되는 서버리스 컴퓨팅. EmDash 운영의 기반 |
| Dynamic Workers | 실행 시점에 격리 워커를 띄우는 Cloudflare 기능. 플러그인 샌드박스의 토대(유료) |
| Worker isolate | 플러그인을 가두는 작은 독립 실행 공간. 다른 영역을 못 만지게 격리 |
| D1 | Cloudflare의 서버리스 SQLite. DB 서버 구축 없이 코드로 DB 생성·쿼리 |
| R2 | Cloudflare의 오브젝트 스토리지(S3 호환). 이미지·미디어 저장 |
| Kysely | 타입 안전 SQL 빌더. SQL을 그대로 쓰되 TS가 컬럼·타입을 검증. DB 교체의 핵심 |
| Portable Text | 콘텐츠를 HTML 대신 JSON 트리로 저장하는 형식(Sanity 발). 멀티 매체 렌더링에 유리 |
| TipTap | Notion 스타일 블록 에디터 라이브러리. 입력을 Portable Text로 직렬화 |
| capability 매니페스트 | 플러그인이 쓸 권한(content:read 등)을 선언하는 명세. 선언 안 한 동작은 차단 |
| Passkey / WebAuthn | 비밀번호 없는 인증. 지문·얼굴로 로그인 → 무차별 대입 공격 차단 |
| MCP | Model Context Protocol. AI 에이전트와 외부 도구(CMS 등)를 잇는 표준 약속 |
| 모노레포 | 여러 패키지를 한 저장소에서 관리하는 방식. EmDash는 pnpm 워크스페이스 사용 |
| Gutenberg | WordPress의 블록 에디터. EmDash는 이걸 Portable Text로 변환해 마이그레이션 지원 |
| x402 | HTTP 402(Payment Required) 기반 콘텐츠 결제 프로토콜 실험 |