많은 WordPress 플러그인을 EmDash로 포팅할 수 있습니다. 플러그인 모델은 다릅니다—PHP 대신 TypeScript, 액션/필터 대신 훅, wp_options 대신 구조화된 스토리지—하지만 대부분의 기능은 깔끔하게 매핑됩니다.
포팅 가능성 평가
모든 플러그인을 포팅하는 것이 합리적인 것은 아닙니다. 시작하기 전에 후보를 평가하세요.
좋은 후보
커스텀 필드, SEO 플러그인, 콘텐츠 프로세서, 관리 UI 확장, 분석, 소셜 공유, 폼
좋지 않은 후보
멀티사이트 기능, WooCommerce/Gutenberg 통합, WordPress 코어 내부를 패치하는 플러그인
플러그인 구조 비교
WordPress
wp-content/plugins/my-plugin/
├── my-plugin.php # 플러그인 헤더가 있는 메인 파일
├── includes/
│ ├── class-admin.php
│ └── class-api.php
└── admin/
└── js/ EmDash
my-plugin/
├── src/
│ ├── index.ts # 플러그인 정의 (definePlugin)
│ └── admin.tsx # 관리 UI 익스포트 (React)
├── package.json
└── tsconfig.json 훅 매핑
WordPress는 문자열 훅 이름과 함께 add_action() 및 add_filter()를 사용합니다. EmDash는 플러그인 정의에서 선언된 타입이 지정된 훅을 사용합니다.
라이프사이클 훅
| WordPress | EmDash | 참고사항 |
|---|---|---|
register_activation_hook() | plugin:install | 첫 설치 시 한 번 실행 |
| 플러그인 활성화 | plugin:activate | 활성화될 때 실행 |
| 플러그인 비활성화 | plugin:deactivate | 비활성화될 때 실행 |
register_uninstall_hook() | plugin:uninstall | event.deleteData가 사용자 선택을 나타냄 |
콘텐츠 훅
| WordPress | EmDash | 참고사항 |
|---|---|---|
wp_insert_post_data | content:beforeSave | 수정된 콘텐츠를 반환하거나 오류를 발생시켜 취소 |
save_post | content:afterSave | 저장 후 부작용 |
before_delete_post | content:beforeDelete | 취소하려면 false 반환 |
deleted_post | content:afterDelete | 삭제 후 정리 |
WordPress
add_action('save_post', function($post_id, $post, $update) {
if ($post->post_type !== 'product') return;
$price = get_post_meta($post_id, 'price', true);
if ($price > 1000) {
update_post_meta($post_id, 'is_premium', true);
}
}, 10, 3);
EmDash
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.collection !== "products") return;
const price = event.content.price as number;
if (price > 1000) {
await ctx.kv.set(`premium:${event.content.id}`, true);
}
},
} 미디어 훅
| WordPress | EmDash | 참고사항 |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | 검증 또는 변환 |
add_attachment | media:afterUpload | 업로드 후 반응 |
스토리지 매핑
Options API → KV Store
WordPress
$api_key = get_option('my_plugin_api_key', '');
update_option('my_plugin_api_key', 'abc123');
delete_option('my_plugin_api_key'); EmDash
const apiKey = await ctx.kv.get<string>("settings:apiKey") ?? "";
await ctx.kv.set("settings:apiKey", "abc123");
await ctx.kv.delete("settings:apiKey"); 커스텀 테이블 → 스토리지 컬렉션
WordPress
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';
// 삽입
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// 쿼리
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);
EmDash
// 플러그인 정의에서 선언
storage: {
items: {
indexes: ["status", "createdAt"],
},
},
// 훅 또는 라우트에서:
await ctx.storage.items.put("item-1", {
name: "Item 1",
status: "active",
createdAt: new Date().toISOString(),
});
const result = await ctx.storage.items.query({
where: { status: "active" },
limit: 10,
}); 설정 스키마
WordPress는 관리 폼에 Settings API를 사용합니다. EmDash는 UI를 자동 생성하는 선언적 스키마를 사용합니다.
WordPress
add_action('admin_init', function() {
register_setting('my_plugin', 'my_plugin_api_key');
add_settings_section('main', 'Settings', null, 'my-plugin');
add_settings_field('api_key', 'API Key', function() {
$value = get_option('my_plugin_api_key');
echo '<input type="text" name="my_plugin_api_key"
value="' . esc_attr($value) . '">';
}, 'my-plugin', 'main');
}); EmDash
admin: {
settingsSchema: {
apiKey: {
type: "secret",
label: "API 키",
description: "대시보드의 API 키",
},
enabled: {
type: "boolean",
label: "활성화됨",
default: true,
},
limit: {
type: "number",
label: "항목 제한",
default: 100,
min: 1,
max: 1000,
},
},
} 관리 UI
WordPress 관리 페이지는 PHP입니다. EmDash는 React 컴포넌트를 사용합니다.
import { useState, useEffect } from "react";
export const widgets = {
summary: function SummaryWidget() {
const [count, setCount] = useState(0);
useEffect(() => {
fetch("/_emdash/api/plugins/my-plugin/status")
.then((r) => r.json())
.then((data) => setCount(data.count));
}, []);
return <div>총 항목: {count}</div>;
},
};
export const pages = {
settings: function SettingsPage() {
// 설정 페이지용 React 컴포넌트
return <div>설정 콘텐츠</div>;
},
};
플러그인 정의에 등록:
admin: {
entry: "@my-org/my-plugin/admin",
pages: [{ path: "/settings", label: "대시보드" }],
widgets: [{ id: "summary", title: "요약", size: "half" }],
},
REST API → 플러그인 라우트
WordPress
register_rest_route('my-plugin/v1', '/items', [
'methods' => 'GET',
'callback' => function($request) {
global $wpdb;
$items = $wpdb->get_results("SELECT * FROM items LIMIT 50");
return new WP_REST_Response($items);
},
]); EmDash
routes: {
items: {
handler: async (ctx) => {
const result = await ctx.storage.items.query({ limit: 50 });
return { items: result.items };
},
},
}, 라우트는 /_emdash/api/plugins/{plugin-id}/{route-name}에서 사용할 수 있습니다.
포팅 프로세스
-
WordPress 플러그인 분석
수행하는 작업 문서화: 훅, 데이터베이스 작업, 관리 페이지, REST 엔드포인트.
-
EmDash 개념으로 매핑
WordPress 훅 → EmDash 훅.
wp_options→ctx.kv. 커스텀 테이블 → 스토리지 컬렉션. 관리 페이지 → React 컴포넌트. REST 엔드포인트 → 플러그인 라우트. -
플러그인 스켈레톤 생성
import { definePlugin } from "emdash"; export function createPlugin() { return definePlugin({ id: "my-ported-plugin", version: "1.0.0", capabilities: [], storage: {}, hooks: {}, routes: {}, admin: {}, }); } -
순서대로 구현
스토리지 → 훅 → 관리 UI → 라우트
-
철저하게 테스트
훅이 올바르게 실행되고, 스토리지가 작동하며, 관리 UI가 렌더링되는지 확인합니다.
예제: 읽기 시간 플러그인
WordPress
add_filter('wp_insert_post_data', function($data, $postarr) {
if ($data['post_type'] !== 'post') return $data;
$content = strip_tags($data['post_content']);
$word_count = str_word_count($content);
$read_time = ceil($word_count / 200);
if (!empty($postarr['ID'])) {
update_post_meta($postarr['ID'], '_read_time', $read_time);
}
return $data;
}, 10, 2);
EmDash
export function createPlugin() {
return definePlugin({
id: "read-time",
version: "1.0.0",
admin: {
settingsSchema: {
wordsPerMinute: {
type: "number",
label: "분당 단어 수",
default: 200,
min: 100,
max: 400,
},
},
},
hooks: {
"content:beforeSave": async (event, ctx) => {
if (event.collection !== "posts") return;
const wpm = await ctx.kv.get<number>("settings:wordsPerMinute") ?? 200;
const text = JSON.stringify(event.content.body || "");
const readTime = Math.ceil(text.split(/\s+/).length / wpm);
return { ...event.content, readTime };
},
},
});
} 기능
플러그인은 보안 샌드박싱을 위해 필요한 기능을 선언해야 합니다:
| 기능 | 제공 | 사용 사례 |
|---|---|---|
network:request | ctx.http.fetch() | 외부 API 호출 |
content:read | ctx.content.get(), list() | CMS 콘텐츠 읽기 |
content:write | ctx.content.create(), etc. | 콘텐츠 수정 |
media:read | ctx.media.get(), list() | 미디어 읽기 |
media:write | ctx.media.getUploadUrl() | 미디어 업로드 |
일반적인 함정
전역 상태 없음 — 전역 변수 대신 스토리지를 사용하세요.
모든 것이 비동기 — 스토리지 및 API 호출에는 항상 await를 사용하세요.
직접 SQL 없음 — 구조화된 스토리지 컬렉션을 사용하세요.
파일 시스템 없음 — 파일에는 미디어 API를 사용하세요.
다음 단계
- Hooks — 시그니처가 있는 모든 훅
- Storage — 컬렉션 및 쿼리
- Settings — Block Kit를 통한 KV 기반 설정
- React Admin Pages & Widgets — 관리 페이지 구축 (네이티브 플러그인)