회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항N
통계
로그인 회원가입
고객센터
3.10 모듈 로딩 구조

플러그인 / 확장 로딩 방식

A Administrator
2026.04.21 01:03(수정됨) 101 0

1. 전체 모듈 로딩 흐름 — 5단계 파이프라인

브라우저 요청이 들어온 순간부터 플러그인과 확장이 로드되어 실행되기까지의 전 과정을 5단계 파이프라인으로 설명합니다. 각 단계가 완전히 완료된 후에야 다음 단계로 진행됩니다.


1.1 전체 흐름 다이어그램

[ 브라우저 요청 ] ──────────────────────────────────────────────
                                                                 
┌─── STEP 0: 진입 및 전처리 ─────────────────────────────────┐
│  ob_start()               ← 출력 버퍼 시작                  │
│  이중 슬래시 URI 감지 → 301 리다이렉트                      │
│  PHP 5.6 미만 → 즉시 종료                                   │
│  핵심 상수 정의: DX_CMS, DX_ROOT, DX_PLUGINS, DX_EXTEND … │
│  DX_START = microtime(true)  ← 성능 측정 기준점             │
└────────────────────────────────────────────────────────────┘
                      ↓
┌─── STEP 1: 클래스/함수 로드 (실행 없이 정의만) ────────────┐
│  functions.php → DxCache → Secure → DxSanitizer            │
│  Database → HookManager → PluginRegistry → Auth → DxPoint  │
│  DxFriend → DxShop → DxSite → DxTheme → DxSeo             │
│  DxCategory → DxThumb → DxNotification → DxMemberMonitor   │
│  DxPopup → DxBoardSkin → DxMarket → DxSocialAuth(옵션)     │
│  DxExtend → Router → Dispatcher → QueryBuilder → DxContainer│
│  DxRouter                                                    │
└────────────────────────────────────────────────────────────┘
                      ↓
┌─── STEP 2: 보안 초기화 ────────────────────────────────────┐
│  Secure::getInstance() → initSession() → startSession()    │
│  sendSecurityHeaders() → csrfToken()                        │
└────────────────────────────────────────────────────────────┘
                      ↓
┌─── STEP 3: DB 연결 + 설정 로드 ────────────────────────────┐
│  require data/config.php → DB 연결 → $dx_config 배열 채우기│
│  Secure::initSecretKeys()                                   │
└────────────────────────────────────────────────────────────┘
                      ↓
┌─── STEP 4: 서비스 초기화 + 플러그인/확장 로드 ─────────────┐
│                                                             │
│  ① HookManager::getInstance()    훅 시스템 준비            │
│  ② PluginRegistry::getInstance() 등록소 준비               │
│  ③ load_plugins()   ◀◀ 플러그인 로딩 핵심  ◀◀             │
│  ④ _dx_register_point_hooks()    포인트 훅 등록            │
│  ⑤ DxSite::getInstance()         멀티사이트 초기화         │
│  ⑥ DxTheme::getInstance()        테마 엔진 초기화          │
│  ⑦ Auth::getInstance()           인증 세션 복원            │
│  ⑧ DxContainer::registerCoreServices()   DI 컨테이너       │
│  ⑨ DxExtend::ensureDirs()        extend/ 폴더 자동 생성    │
│  ⑩ DxPopup•DxMemberMonitor 훅 등록                        │
│                                                             │
│  ██  extend/top/ 실행  ◀◀ 확장 로딩 핵심 1  ██            │
│                                                             │
└────────────────────────────────────────────────────────────┘
                      ↓
┌─── STEP 5: 라우팅 + 디스패치 ──────────────────────────────┐
│  routes/*.php 자동 로드  ← 클래스 기반 라우트 정의         │
│  DxRouter::dispatch()    ← 클래스 기반 라우터 우선 시도    │
│    └─ 매칭 없으면 Dispatcher(Router) 폴백                  │
│         └─ Router::resolve() → $GLOBALS['dx_route'] 세팅  │
│                                                             │
│  ██  extend/middle/ 실행  ◀◀ 확장 로딩 핵심 2  ██         │
│                                                             │
│  핸들러 실행 (home/page/board/admin/auth/api/search/404)   │
│  테마 layout/main.php 렌더링                                │
│    └─ dx_hook_top() / dx_hook_bottom() 발화                │
│                                                             │
│  ██  extend/bottom/ 실행  ◀◀ 확장 로딩 핵심 3  ██         │
│                                                             │
│  ob_end_flush() → 브라우저 전송                            │
│  register_shutdown_function 실행 (비동기 후처리)           │
└────────────────────────────────────────────────────────────┘


1.2 플러그인•확장 로딩 핵심 3지점

지점 호출 코드 위치 직전 상태 역할
① load_plugins() load_plugins() STEP 4 ③ HookManager·PluginRegistry 초기화 후 plugins/*/plugin.php 전체 로드 및 훅 등록
② extend/top/ DxExtend::runTop() STEP 4 모든 초기화 완료 파일 기반 확장 실행 (초기화 완료 시점)
③ extend/middle/ DxExtend::runMiddle() Dispatcher::dispatch() 라우트 확정 파일 기반 확장 실행 (라우트 확정 시점)
④ extend/bottom/ DxExtend::runBottom() STEP 5 렌더링 완료 파일 기반 확장 실행 (렌더링 완료 시점)
⑤ routes/*.php glob + require_once STEP 5 시작 DxRouter 실행 전 클래스 기반 라우트 정의 자동 로드


2. 플러그인•확장 관련 핵심 상수

index.php STEP 0에서 정의되는 상수들입니다. 플러그인과 확장 파일 안에서 이 상수들을 사용하여 경로를 안전하게 참조합니다.
 
// index.php — STEP 0 핵심 상수 정의 (실제 소스)
define('DX_CMS',     true);              // 직접 접근 차단용: if (!defined('DX_CMS')) exit;
define('DX_VERSION', '8.1.0');
define('DX_ROOT',    str_replace('\\', '/', dirname(__FILE__)));
define('DX_CORE',    DX_ROOT . '/core');
define('DX_DATA',    DX_ROOT . '/data');
define('DX_THEMES',  DX_ROOT . '/themes');
define('DX_PLUGINS', DX_ROOT . '/plugins');  // ← 플러그인 루트
define('DX_PAGES',   DX_ROOT . '/pages');
define('DX_BOARDS',  DX_ROOT . '/boards');
define('DX_EXTEND',  DX_ROOT . '/extend');   // ← 확장 루트
define('DX_START',   microtime(true));        // 성능 측정 기준점
 
상수 경로 예시 플러그인·확장에서 활용
DX_CMS true 보안: if (!defined('DX_CMS')) exit;
DX_ROOT /var/www/html DX_ROOT . '/my-config.php'
DX_PLUGINS /var/www/html/plugins DX_PLUGINS . '/my-plugin/assets/'
DX_EXTEND /var/www/html/extend DxExtend::getInstance()->getRoot()
DX_DATA /var/www/html/data DX_DATA . '/uploads/2026/'
DX_THEMES /var/www/html/themes DxTheme 폴백 체인 기준 경로
DX_VERSION 8.1.0 플러그인 최소 버전 요구사항 비교
DX_START 1746345600.123 bottom/: round((microtime(true)-DX_START)*1000,2)


3. STEP 1 — 클래스/함수 정의 파일 로드

STEP 1은 "실행 없이 정의만" 하는 단계입니다. require_once로 클래스 정의와 함수 정의를 메모리에 올리지만, 실제 동작(DB 연결, 세션 시작 등)은 전혀 일어나지 않습니다. 플러그인 로딩은 이보다 훨씬 후인 STEP 4에서 이루어집니다.


3.1 로드 순서와 이유

순서 파일 이유 (왜 이 순서인가)
1 functions.php 전역 함수 정의. 다른 클래스 생성자에서 dx_log(), dx_config() 호출 가능하도록
2 DxCache.php config.php 캐싱에 필요. 가장 빨리 로드해야 함
3 Secure.php 보안 클래스. 고유 경로(security/{hash}/)에서 로드. 없으면 core/Secure.php 폴백
4 DxSanitizer.php Secure가 사용하는 입력 정제 클래스
5 Database.php DB 연결 클래스. 이후 모든 클래스가 의존
6 HookManager.php 훅 시스템. PluginRegistry보다 먼저 (플러그인이 훅을 등록하므로)
7 PluginRegistry.php 플러그인 등록소. HookManager 다음
8 Auth.php 인증. Database 완료 후
9~N 나머지 core/*.php 기능 클래스들. 순서 의존성 낮음
N-2 DxExtend.php extend 엔진. 라우터보다 먼저
N-1 Router.php / Dispatcher.php 라우터·디스패처
N DxContainer.php / DxRouter.php DI 컨테이너 + 클래스 기반 라우터


3.2 Secure.php 특수 로딩 (보안 경로)

Secure.php는 설치 시 data/config.php에 저장된 16자리 해시 경로에서 로드됩니다. 이 경로를 모르면 파일을 직접 수정할 수 없어 보안성이 높아집니다.
 
// index.php 실제 소스
// DX_SECURITY_PATH = 설치 시 생성된 16자리 해시 (예: a3f8c1d2e4b5f6a7)
// config.php에서 선취 파싱 (STEP 3보다 먼저)
if (defined('DX_SECURITY_PATH')) {
    $_dxSecurePath = DX_CORE . '/security/' . DX_SECURITY_PATH . '/Secure.php';
    if (!file_exists($_dxSecurePath)) {
        $_dxSecurePath = DX_CORE . '/Secure.php';  // 파일 손실 시 폴백
    }
} else {
    $_dxSecurePath = DX_CORE . '/Secure.php';  // 구버전 호환
}
require_once $_dxSecurePath;

// 실제 파일 구조
// core/security/a3f8c1d2e4b5f6a7/Secure.php  ← 운영 환경
// core/Secure.php                             ← 폴백


4. STEP 4 — 서비스 초기화 및 플러그인 로딩 (핵심)

STEP 4는 가장 많은 작업이 일어나는 단계입니다. 특히 load_plugins()가 plugins/ 디렉토리의 모든 plugin.php를 로드하고 훅을 등록합니다. 이 단계가 끝나야 모든 CMS 기능과 플러그인 기능을 사용할 수 있습니다.


4.1 STEP 4 세부 실행 순서

HookManager::getInstance()
훅 시스템 초기화. 이후 dx_add_hook()·dx_run_hook() 사용 가능.
 
PluginRegistry::getInstance()
플러그인 등록소 초기화. 이후 dx_register_plugin() 사용 가능.
 
load_plugins()
DX_PLUGINS 하위 모든 폴더의 plugin.php 로드. 훅 등록. ← 핵심
 
_dx_register_point_hooks()
포인트·경험치 훅(dx_after_write 등) 5개 등록.
 
DxSite::getInstance()
도메인 감지 → $dx_config 멀티사이트 오버라이드.
 
DxTheme::getInstance()
테마 엔진 초기화. DxSite 이후 (테마 확정 뒤).
 
Auth::getInstance()
세션/Remember Me 쿠키로 로그인 상태 복원.
 
DxContainer::registerCoreServices()
db·auth·cache·hook·site·theme 서비스를 DI 컨테이너에 등록.
 
DxExtend::ensureDirs()
extend/top·middle·bottom/ 폴더 없으면 자동 생성.
 
팝업·모니터 훅 등록
DxPopup·DxMemberMonitor 훅 등록 (dx_body_bottom·dx_after_login 등).
 
extend/top/ 실행
DxExtend::runTop(). 모든 초기화 완료 후 첫 번째 확장 실행 지점.


4.2 load_plugins() 상세 분석

load_plugins()는 STEP 4의 핵심입니다. plugins/ 하위의 모든 폴더를 glob()으로 탐색하여 plugin.php를 순서대로 로드합니다.
 
// core/functions.php — load_plugins() 실제 소스
function load_plugins()
{
    if (!defined('DX_PLUGINS') || !is_dir(DX_PLUGINS)) return;
    $dirs = glob(DX_PLUGINS . '/*', GLOB_ONLYDIR);  // 하위 폴더 전체 목록
    if (!$dirs) return;

    foreach ($dirs as $dir) {
        $f = $dir . '/plugin.php';
        if (file_exists($f)) {
            require_once $f;   // ← 플러그인 코드 실행 (훅 등록 등)
        }
        // plugin.php 없는 폴더는 조용히 건너뜀
    }

    // DXB CSS 엔진 자동 주입 (테마 파일 수정 불필요)
    _dx_inject_dxb_css();

    // 에디터 훅 브릿지 등록
    // dx_editor_init → dx_render_editor() → dx_editor_render 훅 발화
    dx_add_hook('dx_editor_init', function($args) {
        $name    = isset($args['name'])    ? $args['name']    : 'content';
        $value   = isset($args['value'])   ? $args['value']   : '';
        $options = isset($args['options']) ? $args['options'] : array();
        if (isset($args['board'])) $options['board'] = $args['board'];
        dx_render_editor($name, $value, $options);
    }, 10);
}


4.3 load_plugins() 실행 순서 규칙

glob()이 반환하는 디렉토리 목록의 순서가 플러그인 로드 순서입니다. 운영체제와 파일시스템에 따라 다르지만, 일반적으로 알파벳 오름차순입니다.
 
plugins/
├── ckeditor4-editor/   ← ① 첫 번째 로드 (알파벳 순)
│   ├── plugin.php      ← require_once로 로드
│   └── manifest.php
├── dx-socket/          ← ② 두 번째
│   └── plugin.php
├── example-plugin/     ← ③ 세 번째
│   └── plugin.php
├── kakao-pay/          ← ④ 네 번째
│   └── plugin.php
└── tinymce/            ← ⑤ 다섯 번째
    └── plugin.php

// 폴더에 plugin.php가 없으면 건너뜀
// manifest.php만 있는 폴더 → 건너뜀 (정보 파일만)
// plugin.php 있는 폴더만 로드됨

플러그인 로드 순서 의존성 주의
플러그인 A가 플러그인 B의 훅에 의존하는 경우, 폴더명으로 순서를 제어합니다.
예: 플러그인 B가 먼저 로드되어야 하면 폴더명을 '01_b-plugin'처럼 숫자 접두어로 지정합니다.
그러나 대부분의 경우 플러그인은 상호 독립적으로 설계해야 합니다.


4.4 plugin.php 내부에서 일어나는 일

각 plugin.php가 로드될 때 실행되는 전형적인 코드 패턴입니다. 이 시점에서 ①등록과 ②훅 연결이 동시에 이루어집니다.
 
// plugins/example-plugin/plugin.php (내장 예제 전체)
if (!defined('DX_CMS')) exit('Direct access not allowed.');

// DX_DEBUG 모드에서만 실행 시간 + 쿼리 수 표시
if (defined('DX_DEBUG') && DX_DEBUG) {
    dx_add_hook('dx_bottom', function($context) {
        $time = round((microtime(true) - DX_START_TIME) * 1000, 2);
        $db   = Database::getInstance();
        echo '<div style="position:fixed;bottom:10px;right:10px;
               background:rgba(0,0,0,.7);color:#fff;
               padding:6px 12px;border-radius:8px;
               font-size:11px;z-index:9999">';
        echo '⚡ ' . $time . 'ms | DB: ' . $db->getQueryCount() . '쿼리';
        echo '</div>';
    }, 999);
}

// 이 플러그인의 특징:
// • dx_register_plugin() 없음 → PluginRegistry에 등록 안 함
// • 단순히 dx_add_hook()으로 동작만 추가
// • 조건부 실행 (DX_DEBUG 모드에서만)


5. DxExtend 엔진 — 확장 파일 자동 실행

DxExtend는 extend/ 폴더의 PHP 파일을 자동으로 탐색하고 안전하게 실행하는 엔진입니다. v1.3.0 기준으로 보안 경계 검증, 에러 격리, 컨텍스트 주입, 디버그 로그 기능을 제공합니다.


5.1 DxExtend 클래스 구조

class DxExtend {
    private static $instance = null;   // 싱글턴
    private $extendRoot;               // DX_ROOT . '/extend'
    private $executed = array();        // 실행된 파일 로그

    // ─── 3개 공개 실행 메서드 ───
    public function runTop($context)    → runSlot('top',    $context)
    public function runMiddle($context) → runSlot('middle', $context)
    public function runBottom($context) → runSlot('bottom', $context)

    // ─── 내부 실행 로직 ───
    private function runSlot($slot, $context)
        → collectFiles($dir)
           → safeExec($file, $context, $slot)

    // ─── 유틸 ───
    public function ensureDirs()        // top•middle•bottom 폴더 자동 생성
    public function getExecuted()       // 실행 로그 반환 (디버그용)
    public function getRoot()           // extend/ 루트 경로 반환
}

// 전역 헬퍼 함수
function dx_extend_top($ctx)    → DxExtend::getInstance()->runTop($ctx)
function dx_extend_middle($ctx) → DxExtend::getInstance()->runMiddle($ctx)
function dx_extend_bottom($ctx) → DxExtend::getInstance()->runBottom($ctx)


5.2 collectFiles() — 파일 수집 알고리즘

extend/top/ 같은 슬롯 폴더에서 PHP 파일을 탐색합니다. 파일명 오름차순으로 정렬되므로 파일명의 숫자 접두어가 실행 순서를 결정합니다.
 
// DxExtend::collectFiles() 실제 소스
private function collectFiles($dir)
{
    $files = array();

    // 1단계: 슬롯 폴더 직접 하위의 *.php 수집
    $found = glob($dir . '/*.php');
    if ($found) {
        sort($found);          // 파일명 오름차순 정렬
        $files = $found;
    }

    // 2단계: 1단계 하위 폴더 탐색 (1단계 재귀만)
    $subDirs = glob($dir . '/*', GLOB_ONLYDIR);
    if ($subDirs) {
        sort($subDirs);        // 폴더도 이름순 정렬
        foreach ($subDirs as $sub) {
            $sub = glob($sub . '/*.php');
            if ($sub) {
                sort($sub);
                $files = array_merge($files, $sub);
            }
        }
    }
    return $files;
}

// 실행 순서 예시: extend/top/ 폴더가 아래와 같을 때
// extend/top/
//   01_maintenance.php     ← 1번째
//   02_ip_block.php        ← 2번째
//   10_global_vars.php     ← 3번째
//   security/              ← 4번째 이후 (하위 폴더)
//     01_waf.php           ← 4번째
//     02_rate_limit.php    ← 5번째

파일명 정렬은 사전식(lexicographic) 정렬입니다.
9_file.php는 10_file.php보다 나중에 실행됩니다. ('9' > '1' 사전순)
반드시 01_, 02_, 09_, 10_, 11_ 처럼 자릿수를 맞추세요.
잘못된 예: 1_, 2_, 10_ → 10_이 1_보다 먼저 실행됨


5.3 safeExec() — 파일 안전 실행 메커니즘

각 파일을 실행할 때 4가지 보호 장치가 작동합니다. 하나의 파일에서 오류가 발생해도 다른 파일은 정상 실행됩니다.
 
// DxExtend::safeExec() 실제 소스 (핵심 부분)
private function safeExec($file, $context, $slot)
{
    // ─── 보호 1: 보안 경계 검증 ───────────────────────
    $realFile   = @realpath($file);
    $realExtend = @realpath($this->extendRoot);
    if (!$realFile || !$realExtend) return;

    // 백슬래시 정규화 (Windows 호환)
    $realFile   = str_replace('\\\\', '/', $realFile);
    $realExtend = str_replace('\\\\', '/', $realExtend);

    // extend/ 경계 밖의 파일 실행 차단 (path traversal 방어)
    if (strpos($realFile, $realExtend . '/') !== 0) {
        dx_log('[DxExtend] 보안 차단: ' . $file, 'warning');
        return;
    }

    $this->executed[] = array('slot' => $slot, 'file' => basename($file));

    // ─── 보호 2: 에러 핸들러 (에러 격리) ──────────────
    set_error_handler(function($errno, $errstr, $errfile, $errline) {
        if (error_reporting() === 0) return true; // @ 억제된 에러 무시
        dx_log('[DxExtend] ' . $errstr . ' (line ' . $errline . ')', 'error');
        return true;  // PHP 기본 핸들러 억제 (화면 출력 방지)
    });

    try {
        // ─── 보호 3: 컨텍스트 변수 주입 ─────────────
        extract($context, EXTR_SKIP);  // 기존 변수 덮어쓰지 않음
        $dx_extend_slot = $slot;       // 현재 슬롯 정보 주입

        include $file;

    } catch (Exception $e) {
        // ─── 보호 4: 예외 격리 ──────────────────────
        dx_log('[DxExtend] Exception: ' . $e->getMessage(), 'error');
        // 예외가 발생해도 catch로 잡혀 다음 파일 계속 실행
    }

    restore_error_handler();
}


5.4 컨텍스트 변수 — 슬롯별 주입 변수

safeExec()가 extract($context, EXTR_SKIP)을 호출하여 각 슬롯별 컨텍스트 변수를 파일 스코프에 자동 주입합니다. EXTR_SKIP이므로 파일 내 기존 변수를 덮어쓰지 않습니다.
 
슬롯 자동 주입 변수 출처 예시 값
top/ $version DX_VERSION 상수 '8.1.0'
top/ $path dx_request_uri() '/notice/view/42'
top/ $dx_extend_slot DxExtend 내부 'top'
middle/ $type $GLOBALS['dx_route']['type'] 'board'
middle/ $route $GLOBALS['dx_route'] 전체 array(type,slug,action,id,…)
middle/ $dx_extend_slot DxExtend 내부 'middle'
bottom/ $elapsed round((microtime(true)-DX_START)*1000,2) 12.34 (ms)
bottom/ $dx_extend_slot DxExtend 내부 'bottom'


5.5 runTop() / runMiddle() / runBottom() 발화 시점

// ─── runTop() 발화 위치: index.php ────────────────────────
DxExtend::getInstance()->runTop(array(
    'version' => DX_VERSION,
    'path'    => dx_request_uri(),
));
// 직전: Auth, DxContainer, DxPopup 훅 등록 완료
// 직후: STEP 5 routes/*.php 로드

// ─── runMiddle() 발화 위치: Dispatcher::dispatch() ────────
DxExtend::getInstance()->runMiddle(array(
    'type'  => $this->route['type'],
    'route' => $this->route,
));
// 직전: Router::resolve() → $GLOBALS['dx_route'] 세팅 완료
// 직후: switch($route['type']) 핸들러 실행

// ─── runBottom() 발화 위치: index.php 끝 ──────────────────
DxExtend::getInstance()->runBottom(array(
    'elapsed' => round((microtime(true) - DX_START) * 1000, 2),
));
// 직전: DxRouter::dispatch() 또는 Dispatcher::dispatch() 완료
// 직후: ob_end_flush() → 브라우저 전송


6. 플러그인 파일 구조 및 로딩 규칙


6.1 플러그인 디렉토리 구조

plugins/
├── my-plugin/                 ← 플러그인 ID = 폴더명
│   ├── plugin.php             ← 필수: 로딩 엔진이 이 파일을 실행
│   ├── manifest.php           ← 선택: 마켓 메타 정보 (배열 return)
│   ├── admin/                 ← 선택: 관리자 설정 페이지
│   │   └── settings.php
│   ├── assets/                ← 선택: CSS/JS/이미지
│   │   ├── style.css
│   │   └── editor.js
│   └── controllers/           ← 선택: DI 컨테이너 자동 로드 컨트롤러
│       └── MyController.php
│
├── example-plugin/            ← 내장 예제 (복사해서 시작)
│   ├── plugin.php
│   └── manifest.php
│
└── dx-payment-helper.php      ← 단일 파일 형식 (폴더 불필요)
    // ※ 단일 파일은 폴더가 없으므로 load_plugins()가 로드 안 함
    // ※ 단일 파일은 별도 require_once 필요


6.2 plugin.php 작성 규칙

규칙 내용 이유
보안 체크 필수 if (!defined('DX_CMS')) exit; 첫 줄에 URL 직접 접근 시 코드 노출 방지
require_once로 로드 중복 로드 방지 load_plugins()가 require_once 사용
훅 등록만 수행 실행 코드 최소화 load_plugins 시점에는 라우트 미확정
$GLOBALS 접근 주의 dx_route 미확정 시점 라우트 정보는 middle/ 이후에만
exit() 금지 return 사용 exit는 나머지 플러그인 로드 중단
PHP 5.6 호환 화살표 함수·match 사용 금지 최소 PHP 5.6 지원 요구


6.3 manifest.php 구조

// plugins/my-plugin/manifest.php (내장 예제 전체)
return array(
    'name'        => 'Example Plugin',
    'version'     => '1.0.0',
    'description' => '플러그인 개발 예제. 복사해서 만들어보세요.',
    'author'      => 'DesignOneX',
    'author_url'  => 'https://designonex.com',
    'type'        => 'example',     // editor|payment|socket|captcha|커스텀
    'min_version' => '1.8.0',       // 최소 CMS 버전
    'tags'        => '예제,샘플',
    'category'    => 'utility',
    'homepage'    => 'https://designonex.com',
    'license'     => 'MIT',
);

// manifest.php는 마켓 등록 및 관리자 플러그인 목록에서 사용
// plugin.php와 독립적 — manifest.php만 있어도 마켓 등록 가능
// plugin.php가 없으면 로직 실행 없음 (정보 파일만)


6.4 plugin.php 완성 예제 — 에디터 플러그인

// plugins/my-editor/plugin.php
if (!defined('DX_CMS')) exit('Direct access not allowed.');

// ① PluginRegistry에 등록
dx_register_plugin(array(
    'id'          => 'my-editor',
    'type'        => 'editor',
    'name'        => 'My Custom Editor',
    'version'     => '1.0.0',
    'description' => '마크다운 기반 커스텀 에디터',
    'author'      => '홍길동',
    'priority'    => 10,
    'settings'    => array(
        'height' => array('label'=>'에디터 높이(px)', 'type'=>'number'),
    ),
));

// ② 에디터 렌더링 훅 등록
dx_add_hook('dx_editor_render', function($args) {
    if ($args['editor'] !== 'my-editor') return;  // 내 에디터만

    $name   = htmlspecialchars($args['name'],  ENT_QUOTES);
    $value  = htmlspecialchars($args['value'], ENT_QUOTES, 'UTF-8');
    $height = dx_config('plugin_my-editor_height', 400);

    echo '<div class="my-editor-wrap">';
    echo '<textarea id="my-ed-' . $name . '" name="' . $name . '" style="height:' . $height . 'px">' . $value . '</textarea>';
    echo '<script src="' . dx_base_url('plugins/my-editor/assets/editor.js') . '"></script>';
    echo '</div>';
}, 10);

// ③ 모든 페이지 하단에 CSS 로드 (선택)
dx_add_hook('dx_bottom', function($ctx) {
    // 활성 에디터가 my-editor일 때만 CSS 로드
    if (dx_active_plugin('editor') !== 'my-editor') return;
    echo '<link rel="stylesheet" href="' . dx_base_url('plugins/my-editor/assets/style.css') . '">';
}, 10);


7. extend/ 파일 구조 및 작성 규칙


7.1 3개 슬롯 전체 규칙

슬롯 실행 시점 가용 변수 주의 사항 권장 용도
top/ 모든 초기화 완료 직후, 라우팅 전 DB·세션·Auth·dx_config() 모두 사용 가능. $version, $path $GLOBALS['dx_route'] 미확정. 헤더 변경 가능 점검 모드, IP 차단, ob_start 등록, 전역 변수 주입
middle/ 라우트 확정 직후, 핸들러 실행 전 $type, $route (dx_route 전체) 핸들러 실행 전이므로 페이지 출력 불가 방문자 통계, 라우트별 분기, A/B 테스트
bottom/ 렌더링 완료 후, ob_end_flush 전 $elapsed (ms). 기타 CMS 변수 헤더 변경 불가 (출력 완료). ob_start 등록 늦음 캐시 저장, 성능 로그, JS 태그 추가, 정리


7.2 extend 파일 작성 필수 규칙

<?php
// ─── 필수 보안 헤더 (첫 번째 줄) ──────────────────────────
if (!defined('DX_CMS')) exit;

// ─── 슬롯별 주입 변수 확인 ─────────────────────────────────
// top/: $version, $path, $dx_extend_slot
// middle/: $type, $route, $dx_extend_slot
// bottom/: $elapsed, $dx_extend_slot

// ─── 안전한 변수 접근 (isset 사용) ─────────────────────────
$type = isset($type) ? $type : '';
$route = isset($route) ? $route : array();

// ─── exit 대신 return 사용 ──────────────────────────────────
if ($type === 'admin') return;  // ← 이 파일만 건너뜀
// exit;  ← 절대 금지: 이후 파일 전체가 실행 안 됨

// ─── 무거운 작업은 응답 후 처리 ────────────────────────────
register_shutdown_function(function() use ($route) {
    if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
    $db = Database::getInstance();
    $db->query("INSERT INTO my_log...", array(...));
});


7.3 파일명 네이밍 규칙

패턴 예시 실행 순서 설명
NN_설명.php 01_maintenance.php 1번째 숫자 2자리 필수. 자릿수 맞춰야 정렬 올바름
NN_설명.php 02_ip_block.php 2번째 기능별로 파일 분리 권장
NN_설명.php 10_my_feature.php 3번째 앞 번호 여유 확보 (나중에 삽입 가능)
비활성화 99_old.php.disabled 실행 안 됨 .disabled 추가 시 *.php 패턴 불일치 → 제외
하위 폴더 security/01_waf.php 상위 파일 후 security/ 폴더 하위 → 상위 폴더 파일 실행 후


8. routes/ 폴더 자동 로드 (v6.2.0+)

v6.2.0부터 routes/ 폴더를 지원합니다. 이 폴더의 PHP 파일을 파일명 오름차순으로 자동 로드하여 DxRouter에 클래스 기반 라우트를 정의할 수 있습니다.


8.1 routes/ 로드 소스

// index.php STEP 5 — routes/*.php 자동 로드 (실제 소스)
$_dxRouteDir = DX_ROOT . '/routes';
if (is_dir($_dxRouteDir)) {
    $_dxRouteFiles = glob($_dxRouteDir . '/*.php');
    if ($_dxRouteFiles) {
        sort($_dxRouteFiles);               // 파일명 오름차순
        foreach ($_dxRouteFiles as $_dxRouteFile) {
            require_once $_dxRouteFile;     // 라우트 정의 로드
        }
    }
}
unset($_dxRouteDir, $_dxRouteFiles, $_dxRouteFile);

// 우선순위: DxRouter → 기존 Dispatcher 폴백
if (!DxRouter::dispatch()) {
    $_dxDispatcher = new Dispatcher(new Router());
    $_dxDispatcher->dispatch();
}


8.2 routes/ 파일 예시

// routes/web.php — 클래스 기반 라우트 정의
if (!defined('DX_CMS')) exit;

DxRouter::get('/my-page', 'MyPageController@index');
DxRouter::post('/my-api', 'MyApiController@handle');
DxRouter::get('/my-page/{id}', 'MyPageController@show');

// routes/api.php — API 전용 라우트
DxRouter::post('/api/my-endpoint', function() {
    dx_json(array('status' => 'ok'));
});

routes/ vs extend/middle/ 차이
routes/*.php: DxRouter에 클래스 기반 라우트를 정의하는 용도. 라우팅 전에 로드.
extend/middle/: 라우트 확정 후 실행. 방문자 통계, 접근 제어 등 미들웨어 역할.
두 방식은 목적이 다릅니다: routes/는 새 URL 경로를 만들고, extend/middle/은 기존 경로에 동작을 추가합니다.


9. 플러그인•확장 로딩 후 실행 흐름

모든 플러그인과 확장이 로드된 후, 실제 요청을 처리하는 흐름에서 이들이 어떻게 동작하는지 추적합니다.


9.1 훅 등록 vs 발화 시점 분리

시간 흐름 →

[STEP 4: load_plugins()]
  plugins/my-plugin/plugin.php 로드
    → dx_add_hook('dx_bottom', myCallback, 10)  ← 등록만, 발화 안 함
    → dx_add_hook('dx_editor_render', ...)       ← 등록만
    → dx_register_plugin(...)                     ← 등록소에 기록

[STEP 4: extend/top/ 실행]
  01_darkmode_early.php
    → ob_start(callback)                          ← 콜백 등록만, 실행 안 함

[STEP 5: Dispatcher → 테마 layout/main.php]
  dx_hook_bottom($context)
    → dx_run_hook('dx_bottom', $context)         ← 발화!
    → myCallback($context) 실행                   ← 등록된 콜백 실행

[STEP 5: 렌더링 완료 → ob_end_flush()]
  → ob_start 콜백 실행                            ← 등록된 콜백 실행!
    → <head> 뒤에 다크모드 스크립트 삽입

핵심: 등록(STEP 4)과 발화(STEP 5)는 완전히 분리됩니다.


9.2 에디터 플러그인 전체 실행 추적

[STEP 4: load_plugins()]
  plugins/my-editor/plugin.php
    → dx_register_plugin({id:'my-editor', type:'editor', ...})
       → PluginRegistry: plugins['editor']['my-editor'] = {...}
    → dx_add_hook('dx_editor_render', editorCallback, 10)
       → HookManager: hooks['dx_editor_render'] = [{callback, priority:10}]

[관리자: 설정에서 my-editor 선택]
  → dx_settings.active_editor = 'my-editor' DB 저장

[STEP 5: boards/write.php 스킨 실행]
  dx_render_editor('content', $value, $options)
    → dx_active_plugin('editor')
       → dx_config('active_editor') = 'my-editor'
    → dx_has_hook('dx_editor_render') = true
    → dx_run_hook('dx_editor_render', {name,value,editor:'my-editor'})
       → HookManager::run()
       → editorCallback({name,value,editor:'my-editor'}) 실행
       → 에디터 HTML 출력


9.3 extend/top/ ob_start 콜백 실행 시점

                실행 흐름                     ob 버퍼 상태
───────────────────────────────────────────────────────────
[index.php 시작]
  ob_start()                                  레벨 1 버퍼 시작

[STEP 4: extend/top/ 실행]
  01_darkmode_early.php:
    ob_start(darkmode_callback)               레벨 2 버퍼 시작
    // 콜백은 등록만, 실행 안 됨

[STEP 5: 렌더링]
  echo "<!DOCTYPE html>..."                   레벨 2 버퍼에 쌓임

[index.php 끝: ob_end_flush()]
  ob_end_flush() 호출
    → 레벨 2 버퍼 → darkmode_callback 실행   콜백 실행!
    → <head> 뒤에 스크립트 삽입              HTML 가공
    → 가공된 HTML → 레벨 1 버퍼로 이동
  ob_end_flush() 다시 호출
    → 레벨 1 버퍼 → 브라우저 전송            응답!

결론: extend/top/에서 ob_start를 등록하면
     전체 HTML이 완성된 후 자동으로 가공됩니다.


10. 플러그인•확장 로딩 실전 패턴


10.1 플러그인에서 DB 기반 설정 읽기

plugin.php 로드 시점(STEP 4)은 이미 DB 연결이 완료된 후이므로 설정을 DB에서 읽을 수 있습니다. DxCache로 캐싱하면 성능 영향이 없습니다.
 
// plugins/my-plugin/plugin.php
if (!defined('DX_CMS')) exit;

// ① dx_config()로 관리자 설정 읽기 (DB 기반, 캐싱됨)
$apiKey    = dx_config('my_plugin_api_key', '');
$isEnabled = dx_config('my_plugin_enabled', '0') === '1';

if (!$isEnabled || !$apiKey) return;  // 비활성 시 건너뜀

// ② 싱글턴 서비스 등록
dx_app()->singleton('my_api', function() use ($apiKey) {
    return new MyApiClient($apiKey);
});

// ③ 훅 등록
dx_add_hook('dx_bottom', function($ctx) {
    // 등록된 서비스 꺼내 사용
    $api = dx_make('my_api');
    echo '<script>window.__MY_PLUGIN_ID__ = "' . dx_esc($api->getTrackId()) . '";</script>';
}, 10);


10.2 extend/middle/ — 라우트 기반 조건부 처리

// extend/middle/02_premium_check.php
if (!defined('DX_CMS')) exit;

// middle: $type, $route가 자동 주입됨
$type = isset($type) ? $type : '';
$slug = isset($route['slug']) ? $route['slug'] : '';

// premium 게시판에 비회원 접근 차단
if ($type === 'board' && $slug === 'premium') {
    if (!Auth::getInstance()->isLoggedIn()) {
        dx_set_flash('프리미엄 회원 전용 게시판입니다.', 'error');
        dx_redirect(dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url()));
        exit;
    }
    // 구독 만료 체크
    $userId = Auth::getInstance()->get('id', 0);
    $cacheKey = 'premium_' . $userId;
    $isPremium = DxCache::get($cacheKey, null);
    if ($isPremium === null) {
        $sub = Database::getInstance()->row(
            'SELECT * FROM subscriptions WHERE user_id=? AND expires_at > NOW() LIMIT 1',
            array($userId)
        );
        $isPremium = $sub ? true : false;
        DxCache::set($cacheKey, $isPremium, 300);
    }
    if (!$isPremium) {
        dx_set_flash('구독이 필요합니다.', 'warning');
        dx_redirect(dx_base_url('subscribe'));
        exit;
    }
}


10.3 extend/bottom/ — 성능 로그

// extend/bottom/99_perf_log.php
if (!defined('DX_CMS')) exit;
if (!defined('DX_DEBUG') || !DX_DEBUG) return;

// bottom: $elapsed가 자동 주입됨
$elapsed = isset($elapsed) ? $elapsed : round((microtime(true)-DX_START)*1000,2);
$queries = Database::getInstance()->getQueryCount();
$route   = json_encode(isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array());
$memory  = round(memory_get_peak_usage(true) / 1024 / 1024, 2);

echo '<div style="position:fixed;bottom:10px;right:10px;
         background:rgba(15,23,42,.9);color:#94a3b8;
         padding:8px 14px;border-radius:8px;font-size:11px;
         font-family:monospace;z-index:9999;line-height:1.9">';
echo '⚡ ' . $elapsed . 'ms  🗄 ' . $queries . ' queries  💾 ' . $memory . 'MB';
echo '<br>📍 ' . htmlspecialchars($route, ENT_QUOTES);
echo '</div>';


10.4 디버그: 실행된 플러그인•확장 파일 목록 확인

// extend/bottom/98_debug_loader.php (개발 환경 전용)
if (!defined('DX_CMS')) exit;
if (!defined('DX_DEBUG') || !DX_DEBUG) return;

// 실행된 extend 파일 목록
$executed = DxExtend::getInstance()->getExecuted();

// 등록된 훅 목록
$allHooks      = HookManager::getInstance()->getAll();
$executedHooks = HookManager::getInstance()->getExecuted();

// 등록된 플러그인 목록
$types   = PluginRegistry::getInstance()->getAllTypes();
$plugins = array();
foreach ($types as $type) {
    $plugins[$type] = PluginRegistry::getInstance()->getByType($type);
}

echo '<pre style="position:fixed;top:10px;right:10px;
         background:#0f172a;color:#94a3b8;
         padding:12px;border-radius:8px;font-size:10px;z-index:9999;max-height:400px;overflow:auto">';
echo '=== Extend 실행 파일 === ';
foreach ($executed as $item) {
    echo '[' . $item['slot'] . '] ' . $item['file'] . " ";
}
echo ' === 등록된 훅 === ' . implode(', ', $allHooks);
echo ' === 발화된 훅 === ' . implode(', ', $executedHooks);
echo '</pre>';


플러그인•확장 로딩 핵심 원칙 12가지

1. 모든 로딩은 index.php 단일 진입점을 통해 순서대로 진행된다.
2. STEP 1: 클래스 로드(정의만). STEP 4: 서비스 초기화 + 플러그인 로드. STEP 5: 라우팅.
3. load_plugins()는 plugins/*/plugin.php를 폴더명 오름차순으로 자동 로드한다.
4. plugin.php 없는 폴더는 건너뜀. manifest.php만 있어도 마켓 등록은 가능.
5. plugin.php 첫 줄: if (!defined('DX_CMS')) exit; 필수. exit() 대신 return 사용.
6. 훅 등록(STEP 4)과 발화(STEP 5)는 완전히 분리된다. 등록 시 실행 안 됨.
7. extend/ 파일은 파일명 오름차순 실행. 01_, 02_, 10_ 자릿수를 맞춰야 한다.
8. safeExec(): 경계 검증 → 에러 핸들러 → extract(EXTR_SKIP) → include → restore.
9. top/: 초기화 완료 후. middle/: 라우트 확정 후. bottom/: 렌더링 완료 후.
10. ob_start 콜백은 top/에 등록하고 ob_end_flush() 시점에 자동 실행된다.
11. routes/*.php는 STEP 5 시작에 로드. DxRouter에 클래스 기반 라우트 정의.
12. 무거운 작업은 register_shutdown_function으로 응답 후 처리한다.

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.10 모듈 로딩 구조 자동 로딩 구조 2026.04.21 3.10 모듈 로딩 구조 플러그인 / 확장 로딩 방식 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록