많은 WordPress 플러그인을 EmDash로 포팅할 수 있습니다. 모델은 다릅니다—PHP 대신 TypeScript, actions/filters 대신 hooks, wp_options 대신 구조화된 저장소—이지만 대부분의 기능은 깔끔하게 매핑됩니다.
포팅 적합성 평가
모든 플러그인이 포팅할 가치는 없습니다. 시작 전 후보를 평가하세요.
적합한 경우
사용자 정의 필드, SEO, 콘텐츠 처리기, 관리 UI 확장, 분석, 소셜, 폼
부적합한 경우
멀티사이트 기능, WooCommerce/Gutenberg 연동, WordPress 코어 내부를 수정하는 플러그인
플러그인 구조 비교
WordPress
wp-content/plugins/my-plugin/
├── my-plugin.php # Main file with plugin header
├── includes/
│ ├── class-admin.php
│ └── class-api.php
└── admin/
└── js/ EmDash
my-plugin/
├── src/
│ ├── index.ts # Plugin definition (definePlugin)
│ └── admin.tsx # Admin UI exports (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 저장소
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';
// Insert
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// Query
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);
EmDash
// Declare in plugin definition
storage: {
items: {
indexes: ["status", "createdAt"],
},
},
// In hooks or routes:
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 Key",
description: "Your API key from the dashboard",
},
enabled: {
type: "boolean",
label: "Enabled",
default: true,
},
limit: {
type: "number",
label: "Item Limit",
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>Total items: {count}</div>;
},
};
export const pages = {
settings: function SettingsPage() {
// React component for settings page
return <div>Settings content</div>;
},
};
플러그인 정의에 등록:
admin: {
entry: "@my-org/my-plugin/admin",
pages: [{ path: "/settings", label: "Dashboard" }],
widgets: [{ id: "summary", title: "Summary", 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 플러그인 분석
훅, DB 작업, 관리 페이지, 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: {}, }); } -
구현 순서
Storage → 훅 → 관리 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: "Words per minute",
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 };
},
},
});
} Capabilities
보안 샌드박스를 위해 필요한 capabilities를 선언해야 합니다.
| Capability | 제공 | 사용 사례 |
|---|---|---|
network:fetch | ctx.http.fetch() | 외부 API 호출 |
read:content | ctx.content.get(), list() | CMS 콘텐츠 읽기 |
write:content | ctx.content.create() 등 | 콘텐츠 수정 |
read:media | ctx.media.get(), list() | 미디어 읽기 |
write:media | ctx.media.getUploadUrl() | 미디어 업로드 |
흔한 실수
전역 상태 금지 — 전역 변수 대신 storage를 사용하세요.
모두 비동기 — storage와 API 호출은 항상 await하세요.
직접 SQL 금지 — 구조화된 스토리지 컬렉션을 사용하세요.
파일 시스템 금지 — 파일은 미디어 API를 사용하세요.
다음 단계
- Hooks Reference — 시그니처가 있는 모든 훅
- Storage API — 컬렉션과 쿼리
- Settings — 설정 스키마와 KV
- Admin UI — 관리 페이지 만들기