플러그인 시스템 개요

이 페이지

EmDash의 플러그인 시스템을 사용하면 핵심 코드를 수정하지 않고 CMS를 확장할 수 있습니다. 플러그인은 콘텐츠 라이프사이클 이벤트에 훅하고, 자체 데이터를 저장하고, 관리자에게 설정을 노출하고, 관리 패널에 사용자 정의 UI를 추가할 수 있습니다.

설계 철학

EmDash 플러그인에는 샌드박스화네이티브의 두 가지 유형이 있습니다. 샌드박스화된 플러그인은 격리된 V8 워커에서 실행되며 마켓플레이스에서 원클릭으로 설치할 수 있습니다. 네이티브 플러그인은 인프로세스로 실행되며 코드에서 구성됩니다.

샌드박스화된 플러그인을 우선하세요. 관리 UI에서 코드를 건드리거나 재배포하지 않고도 설치, 업데이트 및 제거할 수 있습니다. 빌드 시 통합이 필요한 기능(React 관리 페이지, Portable Text 렌더링 컴포넌트 또는 페이지 조각 주입)이 필요한 경우에만 네이티브 플러그인을 사용하세요.

핵심 원칙:

  • 샌드박스 우선 — 샌드박스용으로 설계하고, 필요한 경우에만 네이티브 모드 사용
  • 선언적 — 훅, 스토리지 및 라우트는 정의 시 선언되며 동적으로 등록되지 않음
  • 타입 안전 — 타입이 지정된 컨텍스트 객체를 사용한 완전한 TypeScript 지원
  • 능력 기반 — 플러그인은 필요한 것을 선언하고, 샌드박스가 이를 강제함

플러그인이 할 수 있는 것

이벤트에 훅

콘텐츠 저장, 미디어 업로드 및 플러그인 라이프사이클 이벤트 전후에 코드를 실행합니다.

데이터 저장

데이터베이스 마이그레이션을 작성하지 않고도 인덱스된 컬렉션에 플러그인별 데이터를 지속합니다.

설정 노출

설정 스키마를 선언하고 구성을 위한 자동 생성된 관리 UI를 얻습니다.

관리 페이지 추가

React 컴포넌트를 사용하여 사용자 정의 관리 페이지 및 대시보드 위젯을 만듭니다.

API 라우트 생성

플러그인의 관리 UI 또는 외부 통합을 위한 엔드포인트를 노출합니다.

HTTP 요청 수행

보안을 위해 선언된 호스트 제한으로 외부 API를 호출합니다.

플러그인 아키텍처

모든 플러그인은 definePlugin()으로 생성됩니다:

import { definePlugin } from "emdash";

export default definePlugin({
	id: "my-plugin",
	version: "1.0.0",

	// 플러그인이 액세스해야 하는 API
	capabilities: ["read:content", "network:fetch"],

	// 플러그인이 HTTP 요청을 할 수 있는 호스트
	allowedHosts: ["api.example.com"],

	// 영구 스토리지 컬렉션
	storage: {
		entries: {
			indexes: ["userId", "createdAt"],
		},
	},

	// 이벤트 핸들러
	hooks: {
		"content:afterSave": async (event, ctx) => {
			ctx.log.info("Content saved", { id: event.content.id });
		},
	},

	// REST API 엔드포인트
	routes: {
		status: {
			handler: async (ctx) => ({ ok: true }),
		},
	},

	// 관리 UI 구성
	admin: {
		settingsSchema: {
			apiKey: { type: "secret", label: "API Key" },
		},
		pages: [{ path: "/dashboard", label: "Dashboard" }],
		widgets: [{ id: "status", size: "half" }],
	},
});

플러그인 컨텍스트

모든 훅 및 라우트 핸들러는 다음에 액세스할 수 있는 PluginContext 객체를 받습니다:

속성설명가용성
ctx.storage플러그인의 문서 컬렉션항상 (선언된 경우)
ctx.kv설정 및 상태를 위한 키-값 저장소항상
ctx.content사이트 콘텐츠 읽기/쓰기read:content 또는 write:content 사용
ctx.media미디어 파일 읽기/쓰기read:media 또는 write:media 사용
ctx.http외부 요청용 HTTP 클라이언트network:fetch 사용
ctx.log구조화된 로거 (debug, info, warn, error)항상
ctx.plugin플러그인 메타데이터 (id, version)항상
ctx.site사이트 정보: name, url, locale항상
ctx.url()경로에서 절대 URL 생성항상
ctx.users사용자 정보 읽기: get(), getByEmail(), list()read:users 사용
ctx.cron작업 예약: schedule(), cancel(), list()항상
ctx.email이메일 전송: send()email:send 사용 + 제공자 구성

컨텍스트 형태는 모든 훅과 라우트에서 동일합니다. 능력으로 게이트된 속성은 플러그인이 필요한 능력을 선언할 때만 존재합니다.

능력

능력은 플러그인 컨텍스트에서 사용 가능한 API를 결정합니다:

능력액세스 권한 부여
read:contentctx.content.get(), ctx.content.list()
write:contentctx.content.create(), ctx.content.update(), ctx.content.delete()
read:mediactx.media.get(), ctx.media.list()
write:mediactx.media.getUploadUrl(), ctx.media.upload(), ctx.media.delete()
network:fetchctx.http.fetch() (allowedHosts로 제한)
network:fetch:anyctx.http.fetch() (제한 없음 — 사용자 구성 URL용)
read:usersctx.users.get(), ctx.users.getByEmail(), ctx.users.list()
email:sendctx.email.send() (제공자 플러그인 필요)
email:provideemail:deliver 독점 훅 등록 (전송 제공자)
email:interceptemail:beforeSend / email:afterSend 훅 등록
page:injectpage:metadata / page:fragments 훅 등록

등록

Astro 구성에서 플러그인을 등록합니다:

import { defineConfig } from "astro/config";
import { emdash } from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
import auditLogPlugin from "@emdash-cms/plugin-audit-log";

export default defineConfig({
	integrations: [
		emdash({
			plugins: [seoPlugin({ generateSitemap: true }), auditLogPlugin({ retentionDays: 90 })],
		}),
	],
});

플러그인은 빌드 시 해결됩니다. 동일한 우선순위를 가진 훅의 경우 순서가 중요합니다 — 배열에서 이전 플러그인이 먼저 실행됩니다.

실행 모드

EmDash는 두 가지 플러그인 실행 모드를 지원합니다:

모드설명플랫폼
샌드박스화강제 제한이 있는 격리된 V8 워커Cloudflare만
네이티브전체 액세스 권한이 있는 인프로세스모두

샌드박스 모드에서는 능력이 런타임 수준에서 강제됩니다 — 플러그인은 선언한 것만 액세스할 수 있습니다. 네이티브 모드에서는 능력이 권고 사항이며 플러그인은 전체 프로세스 액세스 권한을 갖습니다.

다음 단계

사용 가능한 훅

콘텐츠, 미디어 및 플러그인 라이프사이클에 대한 모든 훅 탐색.

네이티브 vs. 샌드박스

실행 모드 비교 및 플러그인에 적합한 것을 선택합니다.