WordPress 플러그인 포팅

이 페이지

WordPress 플러그인을 포팅하려면 콘텐츠 동작, 저장 데이터, HTTP 라우트, 사용자 인터페이스를 분리합니다. 그다음 해당 요구사항을 지원하는 EmDash 플러그인 형식을 선택합니다.

플러그인이 EmDash에 적합한지 확인

좋은 후보는 콘텐츠 검증, 외부 API 호출, 백그라운드 처리, 사용자 정의 저장 레코드, 설정, 관리 도구처럼 WordPress 코어와 독립적인 동작을 갖습니다.

Astro나 EmDash가 이미 대체하는 WordPress 관심사만 구현하는 플러그인은 포팅하지 마세요. 예: PHP 페이지 캐시, WordPress rewrite 규칙, 테마 템플릿 선택, WordPress 코어 글로벌 수정.

사용자 정의 게시 유형이나 필드만 정의하고 런타임 동작이 거의 없다면 플러그인 대신 EmDash 컬렉션과 seed 파일을 만드세요.

sandboxed 또는 native 선택

플러그인 형식 선택부터 시작하세요. 두 형식은 hook 이름과 PluginContext API를 공유하지만 소스 패키지는 다릅니다.

요구사항SandboxedNative
Registry 설치예아니오
격리 런타임예(구성된 runner)아니오
hooks, 라우트, KV, 구조화 스토리지예예
Block Kit 관리 페이지예예
사용자 정의 React 관리 컴포넌트아니오예
공개 렌더링용 Astro 컴포넌트아니오예
원시 페이지 프래그먼트아니오예

native 전용 빌드 타임 또는 UI 표면이 필요할 때만 native를 선택하세요.

Sandboxed 패키지 형식

emdash-plugin init은 현재 sandboxed 형식을 만듭니다:

my-plugin/
├── emdash-plugin.jsonc
├── src/
│   └── plugin.ts
├── tests/
│   └── plugin.test.ts
├── package.json
└── tsconfig.json

매니페스트에는 ID, publisher, capabilities, 허용 호스트, 스토리지 선언이 포함됩니다. 버전은 보통 package.json에서 옵니다.

다음 매니페스트는 인덱스된 스토리지 컬렉션과 content:afterSave에 필요한 capability를 선언합니다:

{
  "$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
  "slug": "read-time",
  "publisher": "did:plc:abc123def456",
  "license": "MIT",
  "author": { "name": "Example Author" },
  "security": { "email": "security@example.com" },
  "capabilities": ["content:read"],
  "allowedHosts": [],
  "storage": {
    "calculations": { "indexes": ["contentId", "updatedAt"] }
  }
}

src/plugin.ts는 SandboxedPlugin 타입의 plain object를 default export합니다. sandboxed hook 핸들러는 { handler }를 사용하고, sandboxed 라우트 핸들러는 (routeCtx, ctx)를 받습니다:

import type { SandboxedPlugin } from "emdash/plugin";

export default {
  hooks: {
    "content:afterSave": {
      handler: async (event, ctx) => {
        await ctx.storage.calculations.put(event.content.id, {
          contentId: event.content.id,
          updatedAt: new Date().toISOString(),
        });
      },
    },
  },
  routes: {
    recent: {
      handler: async (_routeCtx, ctx) => {
        const result = await ctx.storage.calculations.query({
          orderBy: { updatedAt: "desc" },
          limit: 10,
        });
        return { items: result.items };
      },
    },
  },
} satisfies SandboxedPlugin;

라우트는 /_emdash/api/plugins/read-time/recent에서 사용할 수 있습니다. 쿼리가 필터하거나 정렬하려면 해당 스토리지 필드를 먼저 인덱스로 선언해야 합니다.

emdash-plugin build로 패키지를 빌드하세요. 이 형식에 수동 src/index.ts 디스크립터를 추가하지 마세요. 생성된 package.json, 빌드 출력, 사이트 등록은 첫 sandboxed 플러그인을 참고하세요.

Native 패키지 형식

native 패키지는 astro.config.mjs용 디스크립터 factory와 definePlugin()으로 만든 런타임 factory를 모두 export합니다. 선택적 admin 및 Astro 진입점은 별도 패키지 export입니다.

my-native-plugin/
├── src/
│   ├── index.ts
│   ├── admin.tsx
│   └── astro/
│       └── index.ts
├── package.json
└── tsconfig.json

다음 축약 native 진입점은 필요한 두 부분을 보여 줍니다:

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

export interface ReadTimeOptions {
  wordsPerMinute?: number;
}

export function readTimePlugin(options: ReadTimeOptions = {}): PluginDescriptor {
  return {
    id: "read-time",
    version: "0.1.0",
    format: "native",
    entrypoint: "@example/plugin-read-time",
    capabilities: ["content:read"],
    options,
  };
}

export function createPlugin(options: ReadTimeOptions = {}) {
  return definePlugin({
    id: "read-time",
    version: "0.1.0",
    capabilities: ["content:read"],
    admin: {
      settingsSchema: {
        wordsPerMinute: {
          type: "number",
          label: "Words per minute",
          default: options.wordsPerMinute ?? 200,
          min: 1,
        },
      },
    },
    hooks: {
      "content:afterSave": async (event, ctx) => {
        ctx.log.info("Content saved", { id: event.content.id });
      },
    },
  });
}

export default createPlugin;

native hook 핸들러는 함수를 직접 사용할 수 있습니다. native 라우트 핸들러는 하나의 결합된 컨텍스트 인자를 받습니다. 디스크립터와 런타임의 id, version, capabilities, entrypoint를 일치시키세요.

React 관리 페이지, Portable Text 렌더러, 페이지 프래그먼트를 추가하기 전에 첫 native 플러그인을 읽으세요.

WordPress 동작 매핑

Hooks

WordPress action이나 filter는 이름뿐 아니라 의도를 매핑하세요:

WordPressEmDash
register_activation_hook()최초 설치는 plugin:install, 활성화는 plugin:activate
register_uninstall_hook()plugin:uninstall
wp_insert_post_datacontent:beforeSave
save_postcontent:afterSave
before_delete_postcontent:beforeDelete
deleted_postcontent:afterDelete
wp_handle_upload_prefiltermedia:beforeUpload
add_attachmentmedia:afterUpload

hook 이벤트는 각각 고유한 타입 형태를 갖습니다. WordPress 콜백 인자를 옮기기 전에 hook 레퍼런스를 확인하세요.

엔트리 데이터를 받는 콘텐츠 hook에는 content:read가 필요합니다. 포팅 코드가 호출하는 API에 따라 capability를 추가하세요:

Capability사용 가능 API
content:read콘텐츠 읽기, 엔트리 데이터를 노출하는 콘텐츠 hook 등록
content:write콘텐츠 생성·업데이트·게시·삭제(읽기 포함)
media:read미디어 레코드 읽기
media:write미디어 생성·업데이트(읽기 포함)
network:requestallowedHosts에 나열된 호스트에 대한 ctx.http

옵션과 사용자 정의 테이블

사용자 구성에는 ctx.settings, 작은 내부 값에는 ctx.kv를 사용하세요. 두 저장소 모두 플러그인별로 격리됩니다. 자격 증명은 admin.settingsSchema의 secret 필드로 선언해 EmDash가 암호화하도록 하세요.

쿼리 가능한 플러그인 레코드에는 선언된 ctx.storage.<collection> 컬렉션을 사용합니다. 스토리지 선언은 sandboxed에서는 emdash-plugin.jsonc, native에서는 definePlugin()에 둡니다. EmDash 데이터베이스를 열거나 플러그인 코드에서 SQL을 보간하지 마세요.

다음 비교는 WordPress 글로벌을 새 플러그인에 노출하지 않고 옵션 값 하나를 포팅합니다:

WordPress

$api_key = get_option('read_time_api_key', '');
update_option('read_time_api_key', $new_api_key);

EmDash

import type { PluginContext } from "emdash/plugin";

export async function saveApiKey(ctx: PluginContext, newApiKey: string) {
  await ctx.settings.set("apiKey", newApiKey);
}

export async function readApiKey(ctx: PluginContext) {
  return await ctx.settings.get<string>("apiKey") ?? "";
}

WordPress 사용자 정의 테이블은 스토리지 선언 전에 필터와 정렬에 쓰는 필드를 파악하세요. 다음 sandboxed 매니페스트 조각은 쿼리에 쓰는 두 필드를 모두 인덱스합니다:

"storage": {
  "jobs": { "indexes": ["status", "createdAt"] }
}

런타임에서 job 레코드를 저장하고 쿼리할 수 있습니다:

await ctx.storage.jobs.put("job-123", {
  status: "pending",
  createdAt: new Date().toISOString(),
});

const pending = await ctx.storage.jobs.query({
  where: { status: "pending" },
  orderBy: { createdAt: "asc" },
  limit: 50,
});

해당 쿼리를 실행하기 전에 매니페스트 또는 native 스토리지 정의에서 status와 createdAt을 모두 인덱스로 선언하세요.

REST 엔드포인트

WordPress REST 라우트를 플러그인 라우트에 매핑합니다. EmDash는 /_emdash/api/plugins/<plugin-id>/<route-name>에 마운트합니다. 입력을 받을 때는 inputSchema를 정의하고 JSON 직렬화 가능한 데이터를 반환하세요.

설정과 관리 페이지

sandboxed 플러그인은 Block Kit로 관리 페이지를 설명하고 라우트와 KV로 값을 읽고 씁니다. 관리 앱에 React를 포함하지 않습니다.

native 플러그인은 생성 폼에 admin.settingsSchema를 사용할 수 있습니다. 사용자 정의 React 페이지, 위젯, 필드 위젯, 목록 열에는 패키지 export adminEntry를 사용하세요.

파일과 미디어

업로드 또는 생성 파일에는 미디어 API를 사용하세요. sandboxed 플러그인은 파일 시스템 접근이 없습니다. native 플러그인은 호스트 프로세스를 공유하지만, 배포 로컬 파일 쓰기는 이식 가능한 스토리지 전략이 아닙니다.

플러그인 포팅

  1. WordPress hooks, 옵션, 사용자 정의 테이블, cron, REST 라우트, 관리 페이지, 블록, 숏코드, 외부 호스트를 목록화합니다.

  2. Astro 라우팅, EmDash 콘텐츠 모델, 배포 플랫폼에 속하는 동작을 제거합니다.

  3. sandboxed 또는 native 패키지 형식을 선택하고, 남은 동작에 필요한 capability와 허용 호스트를 기록합니다.

  4. KV 키와 구조화 스토리지 컬렉션을 정의합니다. where 또는 orderBy에 쓰는 필드마다 인덱스를 추가합니다.

  5. 관찰 가능한 동작을 하나씩 포팅하고, 대표 콘텐츠와 실패 케이스로 hook 또는 라우트를 테스트합니다.

  6. 기본 라우트와 스토리지가 동작한 뒤 Block Kit 또는 native 관리 UI를 추가합니다.

  7. 설치, 업그레이드, 활성화, 비활성화, 데이터 삭제 유/무 제거, capability 변경을 테스트합니다.

다음 단계