WordPress 플러그인 포팅

이 페이지

많은 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는 플러그인 정의에 타입이 있는 훅을 선언합니다.

수명 주기 훅

WordPressEmDash참고
register_activation_hook()plugin:install최초 설치 시 한 번
플러그인 활성화plugin:activate활성화 시
플러그인 비활성화plugin:deactivate비활성화 시
register_uninstall_hook()plugin:uninstallevent.deleteData는 사용자 선택

콘텐츠 훅

WordPressEmDash참고
wp_insert_post_datacontent:beforeSave수정된 콘텐츠를 반환하거나 예외로 취소
save_postcontent:afterSave저장 후 부수 효과
before_delete_postcontent:beforeDelete취소하려면 false 반환
deleted_postcontent: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);
        }
    },
}

미디어 훅

WordPressEmDash참고
wp_handle_upload_prefiltermedia:beforeUpload검증 또는 변환
add_attachmentmedia: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}에 있습니다.

포팅 절차

  1. WordPress 플러그인 분석

    훅, DB 작업, 관리 페이지, REST 엔드포인트를 문서화합니다.

  2. EmDash 개념에 매핑

    WordPress 훅 → EmDash 훅. wp_optionsctx.kv. 사용자 테이블 → 스토리지 컬렉션. 관리 페이지 → React. REST → 플러그인 라우트.

  3. 플러그인 뼈대 생성

    import { definePlugin } from "emdash";
    
    export function createPlugin() {
    	return definePlugin({
    		id: "my-ported-plugin",
    		version: "1.0.0",
    		capabilities: [],
    		storage: {},
    		hooks: {},
    		routes: {},
    		admin: {},
    	});
    }
  4. 구현 순서

    Storage → 훅 → 관리 UI → 라우트

  5. 철저히 테스트

    훅 실행, 스토리지, 관리 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:fetchctx.http.fetch()외부 API 호출
read:contentctx.content.get(), list()CMS 콘텐츠 읽기
write:contentctx.content.create()콘텐츠 수정
read:mediactx.media.get(), list()미디어 읽기
write:mediactx.media.getUploadUrl()미디어 업로드

흔한 실수

전역 상태 금지 — 전역 변수 대신 storage를 사용하세요.

모두 비동기 — storage와 API 호출은 항상 await하세요.

직접 SQL 금지 — 구조화된 스토리지 컬렉션을 사용하세요.

파일 시스템 금지 — 파일은 미디어 API를 사용하세요.

다음 단계