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

실제 적용 흐름

A Administrator
2026.04.21 01:00(수정됨) 100 0

1. 전체 실행 흐름 조감도

브라우저에서 요청이 들어와 응답이 나갈 때까지 DXCMS가 거치는 모든 단계를 순서대로 정리합니다. Extend 구조(extend/ 파일, Hook, Plugin)가 각각 어느 지점에서 개입하는지를 먼저 파악하면 이후 세부 내용을 훨씬 쉽게 이해할 수 있습니다.


1.1 10단계 처리 파이프라인

브라우저 요청 (예: GET /notice/view/42)
[STEP 0]  index.php 진입 
• ob_start() — 출력 버퍼 시작
• PHP 버전 체크 (5.6 미만 거부) 
• DX_CMS / DX_ROOT / DX_EXTEND 등 핵심 상수 정의
[STEP 1]  클래스/함수 로드 (실행 없이 정의만)
• functions.php / DxCache / Secure / Database / HookManager
• PluginRegistry / Auth / DxSite / DxTheme / DxExtend / Router …
[STEP 2]  보안 초기화 (Secure.php 전담)
• 세션 설정 → 세션 시작 (읽기전용 GET 요청은 스킵)
• 보안 헤더 발행, CSRF 토큰 선제 발급
[STEP 3]  DB 연결 + 설정 로드 (data/config.php)
• DB 연결, $dx_config 전역 배열 채우기
• secret_key 시크릿 키 주입
[STEP 4]  초기화 (DB 연결 완료 후)
HookManager → PluginRegistry → load_plugins()
└─ plugins/*/plugin.php 자동 로드 & 훅 등록
DxSite (멀티사이트) → DxTheme → Auth → DxContainer
DxExtend::ensureDirs() — extend/ 폴더 자동 생성

extend/top/ 실행  (모든 초기화 완료 직후)
[STEP 5]  라우팅
Router::resolve() — URI 파싱 → 라우트 타입 확정
$GLOBALS['dx_route'] 세팅

extend/middle/ 실행  (라우트 확정 직후)
[STEP 6]  디스패치 (Dispatcher)
라우트 타입별 핸들러 실행
home / page / board / admin / auth / api / search / 404
└─ 테마 레이아웃(layout/main.php)으로 래핑하여 렌더링
   └─ dx_hook_top / dx_hook_middle / dx_hook_bottom 발화
[STEP 7]  렌더링 완료

extend/bottom/ 실행  (ob_end_flush() 직전)
[STEP 8]  출력 버퍼 최종 Flush → 브라우저 응답 전송


1.2 Extend 개입 지점 요약

개입 지점 발화 파일/함수 index.php 위치 직전 상태 직후 상태
extend/top/ DxExtend::runTop() STEP 4 끝 모든 초기화 완료 라우팅 전
extend/middle/ DxExtend::runMiddle() Dispatcher::dispatch() 라우트 확정 핸들러 실행 전
extend/bottom/ DxExtend::runBottom() STEP 7 끝 렌더링 완료 ob_end_flush() 전
Hook(dx_top 등) dx_run_hook() 테마 layout/main.php HTML <body> 직후 콘텐츠 출력 전
Plugin(훅 기반) dx_run_hook() 경유 STEP 4: load_plugins() 훅 등록 후 해당 훅 발화 시


2. 각 단계 상세 분석


2.1 STEP 0~1: 진입 및 클래스 로드

index.php가 PHP에 의해 실행되는 즉시 두 가지 작업이 시작됩니다. 출력 버퍼를 열고, 클래스와 함수 정의 파일들을 require_once로 메모리에 올립니다. 이 단계에서는 실제 실행 로직이 없고 정의만 이루어집니다.


실행 코드 (index.php 발췌)

// ── 출력 버퍼 시작 ─────────────────────────────────────
// IIS/CGI에서 header()가 "headers already sent" 없이 동작하도록 보장
ob_start();

// ── 핵심 상수 정의 ─────────────────────────────────────
define('DX_CMS',     true);
define('DX_VERSION', '8.1.0');
define('DX_ROOT',    str_replace('\\', '/', dirname(__FILE__)));
define('DX_CORE',    DX_ROOT . '/core');
define('DX_EXTEND',  DX_ROOT . '/extend');   // ← Extend 루트
define('DX_START',   microtime(true));        // ← 성능 측정 기준점

// ── 클래스/함수 정의 파일 로드 (실행 없이 정의만) ──────
require_once DX_CORE . '/functions.php';        // dx_config, dx_log 등
require_once DX_CORE . '/DxCache.php';           // 캐시 (config.php 캐싱에 필요)
require_once DX_CORE . '/hook/HookManager.php';  // dx_add_hook, dx_run_hook
require_once DX_CORE . '/PluginRegistry.php';    // dx_register_plugin
require_once DX_CORE . '/DxExtend.php';          // ← Extend 엔진
require_once DX_CORE . '/router/Router.php';
require_once DX_CORE . '/router/Dispatcher.php';
// ... (Auth, DxSite, DxTheme 등 약 20개 파일)

💡 포인트: DX_CMS 상수가 정의되는 것이 바로 이 시점입니다.
  extend/ 파일 첫 줄의 if (!defined('DX_CMS')) exit; 가 이 상수를 검증합니다.
  이 체크가 없으면 extend/ 파일을 웹에서 직접 URL로 호출할 수 있게 되어 보안 취약점이 생깁니다.


2.2 STEP 2~3: 보안 초기화 및 DB 연결

Secure.php가 세션 설정, 보안 헤더, CSRF 토큰 발급을 전담합니다. 이후 data/config.php가 로드되어 DB 연결이 이루어집니다. 이 두 단계가 끝나야 비로소 CMS의 모든 기능을 사용할 수 있는 상태가 됩니다.
 
// ── STEP 2: 보안 초기화 ─────────────────────────────────
$_dxSecure = Secure::getInstance();
$_dxSecure->initSession($_dxIsHttps);   // 세션 옵션 설정

// GET + 세션쿠키 없는 경우 세션 시작 스킵 (성능 최적화)
// /admin, /auth, /view/, /api/ 등은 세션 필수 → 항상 시작
if ($_dxNeedSession) {
    $_dxSecure->startSession();
}
$_dxSecure->sendSecurityHeaders();  // X-Frame-Options 등 보안 헤더
$_dxSecure->csrfToken();            // CSRF 토큰 선제 발급

// ── STEP 3: DB 연결 + 설정 로드 ────────────────────────
require_once DX_ROOT . '/data/config.php';
// config.php 내부: Database::getInstance()->connect(...)
// $dx_config 전역 배열이 DB의 dx_settings 테이블에서 채워짐


2.3 STEP 4: 초기화 완료 → extend/top/ 실행

가장 많은 일이 일어나는 단계입니다. 플러그인이 로드되고, 멀티사이트•테마•인증이 초기화된 후, extend/top/ 폴더의 PHP 파일들이 자동 실행됩니다.


2.3.1 플러그인 로드: load_plugins()

load_plugins()는 plugins/ 하위의 모든 폴더를 순회하며 plugin.php를 require_once로 로드합니다. 각 plugin.php는 이 시점에 dx_register_plugin()과 dx_add_hook()을 호출하여 자신을 등록합니다.
 
// core/functions.php 내 load_plugins() 구현
function load_plugins() {
    $dirs = glob(DX_PLUGINS . '/*', GLOB_ONLYDIR);
    foreach ($dirs as $dir) {
        $f = $dir . '/plugin.php';
        if (file_exists($f)) {
            require_once $f;  // ← 플러그인 코드 실행
            // plugin.php 내부에서 이루어지는 일:
            //   dx_register_plugin(...)  → PluginRegistry 등록
            //   dx_add_hook('dx_editor_render', ...)  → 훅 등록
            //   dx_add_hook('dx_bottom', ...)  → 하단 JS 주입 등록
        }
    }
}


2.3.2 핵심 서비스 초기화 순서

순서 호출 역할 완료 후 가용 기능
HookManager::getInstance() 훅 시스템 초기화 dx_add_hook() / dx_run_hook() 사용 가능
PluginRegistry::getInstance() 플러그인 등록소 초기화 dx_register_plugin() 사용 가능
load_plugins() 플러그인 코드 로드 및 훅 등록 plugins/ 하위 훅 모두 활성화
DxSite::getInstance() 멀티사이트 도메인 감지 도메인별 테마•설정 오버라이드
DxTheme::getInstance() 테마 엔진 초기화 테마 폴백 체인 활성화
Auth::getInstance() 인증 세션 검증 isLoggedIn() / isAdmin() 사용 가능
DxContainer::registerCoreServices() DI 컨테이너 서비스 등록 dx_app()->make() 사용 가능
DxExtend::ensureDirs() extend/ 폴더 자동 생성 top•middle•bottom 폴더 보장


2.3.3 extend/top/ 실행

위 8단계가 모두 완료된 직후 DxExtend::runTop()이 호출됩니다. 이 시점은 모든 CMS 기능이 준비된 상태이므로 extend/top/ 파일 안에서 DB 쿼리, 세션 조작, 인증 확인 등 어떤 작업도 가능합니다.
 
// index.php — STEP 4 끝 부분 (실제 소스)
DxExtend::getInstance()->runTop(array(
    'version' => DX_VERSION,      // '8.1.0'
    'path'    => dx_request_uri(),// '/notice/view/42'
));

// DxExtend::runTop() 내부 동작:
// 1. extend/top/ 폴더의 *.php 파일을 파일명 오름차순으로 수집
// 2. 각 파일을 safeExec()로 실행 (에러 격리)
// 3. dx_run_hook('dx_extend_top', $context) 발화

⚠️ 주의: extend/top/ 에서 exit()를 호출하면 이후 모든 처리가 중단됩니다.
점검 모드처럼 의도적인 경우에만 사용하고, 일반 로직에서는 return을 사용하세요.
return은 현재 파일 실행만 중단하고 다음 파일로 넘어갑니다.


2.3.4 실제 내장 예제: extend/top/01_darkmode_early.php

DXCMS에 내장된 다크모드 FOUC(화면 깜빡임) 방지 코드입니다. ob_start 콜백을 이용해 최종 HTML에 인라인 스크립트를 삽입합니다. top/ 슬롯이기 때문에 렌더링 전에 출력 버퍼 가로채기를 설정할 수 있습니다.
 
// extend/top/01_darkmode_early.php 핵심 부분
if (!defined('DX_CMS')) exit;  // 보안 필수

// ob_start 콜백으로 최종 HTML 버퍼를 가로챔
ob_start(function($buffer) {
    // <head> 바로 뒤에 인라인 스크립트 삽입
    $earlyScript = '<script>'
        . '(function(){'
        .   'try{'
        .     'var t=localStorage.getItem("dx-theme");'
        .     'var sys=window.matchMedia("(prefers-color-scheme: dark)").matches;'
        .     'if(t==="dark"||(t===null&&sys)){'
        .       'document.documentElement.classList.add("dx-dark-early");'
        .       'document.documentElement.style.visibility="hidden";'
        .     '}'
        .   '}catch(e){}'
        . '})();'
        . '</script>';

    // <head> 바로 뒤에 삽입
    $buffer = preg_replace('/<head([^>]*)>/i',
                           '<head$1>' . $earlyScript, $buffer, 1);
    return $buffer;
});

// ↑ 이 ob_start 콜백은 렌더링이 완료되고 ob_end_flush()가
//   호출될 때 비로소 실행됩니다.
//   즉, top/에서 등록 → 전체 HTML이 완성된 후 처리됩니다.


2.4 STEP 5: 라우팅 → extend/middle/ 실행

Router가 요청 URI를 분석하여 어떤 타입의 페이지인지 확정합니다. 확정 결과는 $GLOBALS['dx_route']에 저장되고, 이 시점에 extend/middle/ 파일들이 실행됩니다.


2.4.1 Router::resolve() — URI 파싱 과정

/notice/view/42 같은 URI가 들어올 때 Router가 어떻게 라우트를 확정하는지 추적합니다.
 
// Router::resolve() 흐름 (예: GET /notice/view/42)

// 1. URI 정규화
$uri = '/notice/view/42';

// 2. 세그먼트 분리
$segments = ['notice', 'view', '42'];

// 3. 첫 세그먼트가 'admin' / 'auth' / 'api' 인지 확인
//    → 아님 → 게시판 액션인지 확인

// 4. $second('view')가 게시판 액션 목록에 있는지 확인
//    boardActions = ['list','view','write','edit','delete','search','reply','bulk']
//    'view' → 있음 → boards 테이블에서 board_key='notice' 조회

// 5. 게시판 존재 → TYPE_BOARD로 확정
$GLOBALS['dx_route'] = array(
    'type'   => 'board',
    'slug'   => 'notice',
    'action' => 'view',
    'id'     => '42',
    'board'  => ['board_key'=>'notice', 'name'=>'공지사항', ...],
);


2.4.2 라우트 타입별 처리 분기

TYPE 예시 URL 핸들러 특이사항
home / 테마 page/home.php 또는 DB is_home=1 페이지 테마 > DB > pages/home.php 순으로 폴백
page /about DX_PAGES/about.php 접근 레벨(0=공개,1=로그인,9=관리자) 체크
board /notice/view/42... boards/handler.php 읽기/쓰기 레벨 체크 후 핸들러 실행
admin /admin/dashboard admin/index.php 관리자 권한 없으면 로그인 페이지로 리다이렉트
auth /auth/login core/auth/login.php 소셜 callback은 ob 버퍼 초기화 후 실행
api /api/comment core/api/comment.php JSON 헤더 자동 설정
search /search core/search/handler.php 통합 검색
404 존재하지 않는 경로 테마 page/404.php HTTP 404 응답


2.4.3 extend/middle/ 실행 — Dispatcher::dispatch() 내부

Dispatcher::dispatch()가 라우트를 받아 $GLOBALS['dx_route']에 저장한 직후, 핸들러를 실행하기 전에 extend/middle/을 실행합니다.
 
// core/router/Dispatcher.php — dispatch() 실제 소스
public function dispatch() {
    $this->route = $this->router->resolve();
    $GLOBALS['dx_route'] = $this->route;  // ← 라우트 확정

    // ┌─────────────────────────────────────────────────┐
    // │  extend/middle/ 실행                            │
    // │  라우트 확정 직후, 핸들러 실행 전               │
    // └─────────────────────────────────────────────────┘
    DxExtend::getInstance()->runMiddle(array(
        'type'  => $this->route['type'],
        'route' => $this->route,
    ));

    // ← 이 시점부터 핸들러 실행
    switch ($this->route['type']) {
        case 'home':  $this->dispatchHome();  break;
        case 'page':  $this->dispatchPage();  break;
        case 'board': $this->dispatchBoard(); break;
        // ...
    }
}


2.4.4 실제 내장 예제: extend/middle/01_visit_tracker.php

방문자 통계를 수집하는 내장 파일입니다. middle/ 슬롯이기 때문에 $GLOBALS['dx_route']를 읽어 admin•api 경로를 제외하고, 봇을 필터링하며, DB 기록은 register_shutdown_function으로 응답 후 처리합니다.
 
// extend/middle/01_visit_tracker.php 핵심 흐름
if (!defined('DX_CMS')) exit;

// ① 라우트 타입 확인 (middle에서만 가능)
$_vt_route = isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array();
$_vt_type  = isset($_vt_route['type']) ? $_vt_route['type'] : '';
if (in_array($_vt_type, array('admin', 'api'), true)) return;

// ② 봇 판별 (봇은 전혀 기록하지 않음)
$ua_lower = strtolower($_vt_ua);
foreach ($bot_patterns as $b) {
    if (strpos($ua_lower, $b) !== false) { return; }
}

// ③ 순방문자 판단: DxCache 사용 (DB 조회 없음)
$cacheKey = 'vt_u:' . date('Y-m-d') . ':' . substr(md5($ip), 0, 12);
if (!DxCache::get($cacheKey, false)) {
    DxCache::set($cacheKey, 1, $ttl);
    $_vt_isUnique = true;
}

// ④ 응답 후 DB 기록 (register_shutdown_function으로 지연)
register_shutdown_function(function() use ($_vt_data) {
    // fastcgi_finish_request()로 응답 먼저 전송
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }
    // 이후 DB INSERT — 사용자 체감 속도에 영향 없음
    $pdo->prepare("INSERT INTO visits ...")->execute(...);
});


2.5 STEP 6: 디스패치 — 렌더링과 Hook 발화

Dispatcher가 핸들러를 실행하면 실제 HTML이 생성됩니다. 이 과정에서 테마 레이아웃이 로드되고, 레이아웃 안에서 훅(dx_hook_top / dx_hook_middle / dx_hook_bottom)이 발화됩니다.


2.5.1 테마 렌더링 흐름

// Dispatcher::renderPageWithLayout() 흐름

// 1. 테마 레이아웃 파일 결정
$layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
// 폴백 체인: themes/{현재테마}/layout/main.php
//           → themes/default/layout/main.php

// 2. 콘텐츠 파일을 버퍼에 렌더링
ob_start();
extract($context, EXTR_SKIP);
include $contentFile;          // pages/about.php 등
$dx_content = ob_get_clean();  // 콘텐츠 HTML 캡처

// 3. 레이아웃에 콘텐츠 주입
require $layoutFile;
// layout/main.php 내부에서 $dx_content를 출력하고
// dx_hook_top / dx_hook_middle / dx_hook_bottom 을 호출


2.5.2 테마 layout/main.php에서의 Hook 발화

테마 개발자는 layout/main.php 안에서 아래와 같이 훅 함수를 호출합니다. 이 함수들이 호출되는 순간, dx_add_hook()으로 등록된 모든 콜백이 우선순위 순으로 실행됩니다.
 
<!-- themes/default/layout/main.php 구조 (개념) -->
<!DOCTYPE html>
<html>
<head>
  <?php dx_head_assets(); ?>    <!-- CSS/JS 자동 주입 -->
</head>
<body>

  <?php dx_hook_top($context); ?>       <!-- ← Hook 발화 지점 1 -->
  <!--
    내부: dx_run_hook('dx_top', $context)           // 모든 페이지 공통
          dx_run_hook('dx_board_top', $context)     // 게시판이면
          dx_run_hook('dx_page_notice_top', $ctx)   // slug='notice'이면
  -->

  <main>
    <?php echo $dx_content; ?>   <!-- 페이지/게시판 콘텐츠 -->
  </main>

  <?php dx_hook_bottom($context); ?>    <!-- ← Hook 발화 지점 2 -->
  <!--
    내부: dx_run_hook('dx_bottom', $context)
          dx_run_hook('dx_board_bottom', $context)
  -->

</body>
</html>


2.5.3 dx_hook_top/bottom 내부 로직 (HookManager)

dx_hook_top()이 호출되면 세 개의 훅이 순서대로 발화됩니다. 이 중 type•slug 기반 훅은 현재 라우트에 맞는 것만 발화됩니다.
 
// core/hook/HookManager.php 실제 소스
function dx_hook_top($context = array()) {
    dx_run_hook('dx_top', $context);         // 모든 페이지

    if (isset($context['type'])) {
        // 예: $context['type'] = 'board'
        dx_run_hook('dx_board_top', $context); // 게시판 페이지만
    }

    if (isset($context['slug'])) {
        // 예: $context['slug'] = 'notice'
        dx_run_hook('dx_page_notice_top', $context); // notice 게시판만
    }
}

// ─────────────────────────────────────────────────────
// 예: extend/top/에서 이렇게 등록했다면
dx_add_hook('dx_board_top', function($ctx) {
    echo '<div class="board-banner">게시판 전용 배너</div>';
}, 10);

// 이 콜백은 type='board'인 페이지에서만 실행됨
// type='page'나 type='home'에서는 실행되지 않음


2.6 STEP 7: 렌더링 완료 → extend/bottom/ 실행

Dispatcher가 핸들러 실행을 마치면 index.php의 마지막 부분에서 extend/bottom/이 실행됩니다. 이 시점은 HTML이 이미 완성된 후이므로 헤더 변경은 불가하지만, ob_start가 활성화된 상태라면 추가 출력이 가능합니다.
 
// index.php 마지막 부분 (실제 소스)

// DxRouter 또는 Dispatcher가 핸들러 실행 완료

// extend/bottom/ 실행 (렌더링 완료 후)
DxExtend::getInstance()->runBottom(array(
    'elapsed' => round((microtime(true) - DX_START) * 1000, 2),
    // elapsed: 현재까지 경과 시간(ms) - 성능 로그에 활용 가능
));

// 출력 버퍼 최종 flush
if (ob_get_level() > 0) {
    ob_end_flush();
    // ← 여기서 extend/top/01_darkmode_early.php가 등록한
    //   ob_start 콜백이 실행되어 HTML을 가공합니다.
}


실제 내장 예제: extend/bottom/02_darkmode_engine.php

다크모드 JS 파일을 모든 페이지 하단에 자동 주입합니다. bottom/ 슬롯이므로 렌더링 완료 후 JS 삽입이 보장됩니다.
 
// extend/bottom/02_darkmode_engine.php (실제 소스)
if (!defined('DX_CMS')) exit;

$_dmVer  = defined('DX_VERSION') ? DX_VERSION : '1.0';
$_dmBase = rtrim(function_exists('dx_base_url') ? dx_base_url('') : '', '/');

// dx-darkmode-engine.js 로드 스크립트 출력
echo '<script src="' . $_dmBase . '/assets/js/dx-darkmode-engine.js'
   . '?v=' . htmlspecialchars($_dmVer, ENT_QUOTES, 'UTF-8') . '"></script>' . "\n";


3. ob_start 콜백의 실행 타이밍 완전 해부

다크모드 방지처럼 ob_start 콜백을 사용하는 패턴은 처음 보면 언제 실행되는지 헷갈릴 수 있습니다. 실행 시점을 정확히 짚어 봅니다.


3.1 ob_start 중첩 구조

index.php가 시작될 때 이미 ob_start()가 한 번 호출됩니다. 이후 extend/top/01_darkmode_early.php가 ob_start(콜백)을 추가로 호출하면 중첩된 버퍼가 쌓입니다.
 
시간 흐름 →

[index.php 시작]
  ob_start()          ← 버퍼 레벨 1 (콜백 없음)

[extend/top/ 실행]
  ob_start(callback)  ← 버퍼 레벨 2 (다크모드 콜백)

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

[index.php 끝]
  ob_end_flush()      ← 레벨 2 버퍼 → 콜백 실행 → HTML 가공
                         가공된 HTML이 레벨 1 버퍼로 넘어감
  ob_end_flush()      ← 레벨 1 버퍼 → 브라우저로 전송

결과: <head> 바로 뒤에 인라인 스크립트가 삽입된 HTML이 전송됨

💡 이 패턴의 장점:
  • 테마 파일을 전혀 수정하지 않아도 됩니다.
  • 플러그인/extend 파일이 자신을 등록하고, HTML이 완성되는 시점에 자동으로 처리됩니다.
  • CMS 업데이트 후에도 동작이 유지됩니다.


4. 실전 시나리오 — 요청 하나를 끝까지 추적

사용자가 /notice/view/42에 접속할 때 Extend 구조가 어떻게 개입하는지 처음부터 끝까지 추적합니다.


4.1 시나리오 설정

항목
요청 URL GET /notice/view/42
사용자 상태 일반 회원 (로그인)
설치된 extend 파일 top/01_darkmode_early.php, middle/01_visit_tracker.php, bottom/02_darkmode_engine.php
설치된 플러그인 ckeditor4-editor (에디터), dx-socket (소켓)
테마 default


4.2 ms 단위 타임라인

T+0ms
index.php 진입
ob_start() 호출. DX_CMS=true, DX_START=microtime(true) 정의.
 
T+1ms
STEP 1: 클래스 로드
20여 개 core/*.php 파일을 require_once. 실행 없이 클래스/함수 정의만 메모리에 올림.
  
T+2ms
STEP 2: 보안 초기화
Secure::initSession() → startSession() → sendSecurityHeaders() → csrfToken(). /notice/view/42는 view/ 포함이라 세션 필요 → 세션 시작.
 
T+3ms
STEP 3: DB 연결
data/config.php 로드. MySQL 연결. $dx_config 배열에 사이트 설정 로드.
 
T+4ms
load_plugins()
plugins/ckeditor4-editor/plugin.php → dx_register_plugin + dx_add_hook('dx_editor_render'...) 등록. plugins/dx-socket/plugin.php → WebSocket 훅 등록.
 
T+5ms
DxSite + DxTheme + Auth
도메인 감지 → 테마 = 'default' 확정. Auth: 세션에서 회원 정보 복원 → isLoggedIn() = true.
 
T+6ms
extend/top/ 실행
01_darkmode_early.php: ob_start(콜백) 등록. (콜백은 아직 실행 안 됨, ob_end_flush 때 실행됨)
 
T+7ms
Router::resolve()
URI /notice/view/42 파싱. segments=['notice','view','42']. boards 테이블에서 board_key='notice' 조회. TYPE_BOARD 확정. $GLOBALS['dx_route'] 세팅.
 
T+8ms
extend/middle/ 실행
01_visit_tracker.php: type='board' → 기록 대상. 봇 아님. DxCache로 순방문자 확인. register_shutdown_function으로 DB INSERT 예약.
 
T+9ms
Dispatcher::dispatchBoard()
게시판 접근 레벨 체크 (read_level=0 → 통과). boards/handler.php 실행. DB에서 post_id=42 조회. $dx_content 생성.
 
T+10ms
layout/main.php 렌더링
themes/default/layout/main.php 로드. dx_hook_top() → dx_run_hook('dx_top'), dx_run_hook('dx_board_top'). $dx_content 출력. dx_hook_bottom() → dx_run_hook('dx_bottom'), dx_run_hook('dx_board_bottom').
 
T+11ms
extend/bottom/ 실행
02_darkmode_engine.php: echo '<script src=".../dx-darkmode-engine.js">'. elapsed 값 컨텍스트로 받아 성능 로그 가능.
 
T+12ms
ob_end_flush() — 콜백 실행
01_darkmode_early.php의 ob_start 콜백 실행. <head> 뒤에 인라인 스크립트 삽입. HTML 가공 완료 → 브라우저로 전송.
 
T+13ms
register_shutdown_function 실행
fastcgi_finish_request() → 사용자에게 응답 먼저 전송. 이후 방문자 DB INSERT (visits, visit_logs 테이블).


4.3 타임라인 요약 다이어그램

T+0ms   ├── ob_start()                                [index.php]
T+1ms   ├── require 20개 클래스 파일                 [STEP 1]
T+2ms   ├── 세션 시작 / 보안 헤더                   [STEP 2]
T+3ms   ├── DB 연결 / config 로드                   [STEP 3]
T+4ms   ├── load_plugins() → 훅 등록                [STEP 4]
T+5ms   ├── DxSite/DxTheme/Auth 초기화              [STEP 4]
T+6ms   ├── ████ extend/top/ 실행 ████              [Extend top]
        │     └─ ob_start(darkmode_callback) 등록
T+7ms   ├── Router::resolve() → TYPE_BOARD 확정     [STEP 5]
T+8ms   ├── ████ extend/middle/ 실행 ████           [Extend middle]
        │     └─ 방문자 통계 shutdown 예약
T+9ms   ├── boards/handler.php → post#42 조회       [STEP 6]
T+10ms  ├── layout/main.php 렌더링                  [STEP 6]
        │     ├─ dx_hook_top()   → 플러그인 실행
        │     ├─ $dx_content 출력
        │     └─ dx_hook_bottom() → 플러그인 실행
T+11ms  ├── ████ extend/bottom/ 실행 ████           [Extend bottom]
        │     └─ darkmode-engine.js script 태그 출력
T+12ms  ├── ob_end_flush()                           [STEP 8]
        │     └─ darkmode_callback() → <head> 뒤 스크립트 삽입
        │         → 브라우저 전송
T+13ms  └── shutdown: DB INSERT (방문자 로그)        [비동기]


5. 파일 로드 순서 결정 메커니즘

extend/ 폴더에 여러 파일이 있을 때 어떤 순서로 실행되는지, 내부적으로 어떻게 결정되는지를 설명합니다.


5.1 DxExtend::collectFiles() 동작 원리

// core/DxExtend.php — collectFiles() 실제 소스
private function collectFiles($dir) {
    $files = array();

    // 1단계: 해당 폴더의 *.php 파일 수집
    $found = glob($dir . '/*.php');
    if ($found) { sort($found); $files = $found; }
    //           ↑ sort()가 파일명 오름차순 보장

    // 2단계: 하위 폴더 탐색 (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;
}


5.2 파일명 기반 순서 제어

파일명 실행 순서 권장 역할
01_maintenance.php 1번 서비스 점검 모드 (가장 먼저 실행되어야 함)
02_ip_block.php 2번 IP 차단 (점검 모드 다음)
03_global_vars.php 3번 전역 변수 주입
10_my_feature.php 4번 일반 기능 (앞 번호 확보)
99_debug_panel.php 5번 디버그 패널 (가장 나중)
security/01_block.php 6번 하위 폴더: 상위 파일 다음에 실행

⚠️ 파일명 정렬은 사전식(lexicographic) 정렬입니다.
  9_file.php는 10_file.php보다 나중에 실행됩니다. ('9' > '1' 사전순)
  반드시 01_, 02_, 10_, 11_ 처럼 자릿수를 맞추세요.
  예) 01_, 02_, 03_ ... 09_, 10_, 11_ ... 99_


5.3 safeExec() — 에러 격리 메커니즘

DxExtend는 각 파일을 safeExec()로 실행합니다. 한 파일에서 에러가 발생해도 다음 파일이 계속 실행되도록 보장합니다.
 
// DxExtend::safeExec() 핵심 로직
private function safeExec($file, $context, $slot) {
    // 보안: extend/ 경계 검증 (path traversal 방지)
    $realFile   = realpath($file);
    $realExtend = realpath($this->extendRoot);
    if (strpos($realFile, $realExtend . '/') !== 0) {
        dx_log('[DxExtend] 보안 차단: ' . $file, 'warning');
        return;
    }

    // 에러 핸들러 설정 (이 파일의 에러만 로그 기록)
    set_error_handler(function($errno, $errstr, ...) {
        dx_log('[DxExtend] ' . $errstr, 'error');
        return true;  // PHP 기본 핸들러 억제
    });

    try {
        extract($context, EXTR_SKIP);  // 컨텍스트 변수 주입
        $dx_extend_slot = $slot;       // 현재 슬롯 정보 주입
        include $file;
    } catch (Exception $e) {
        dx_log('[DxExtend] Exception: ' . $e->getMessage(), 'error');
        // 예외가 발생해도 다음 파일로 계속 진행
    }

    restore_error_handler();
}


6. Hook 발화 시점 상세

훅이 등록되는 시점과 발화되는 시점은 다릅니다. 이 차이를 이해하는 것이 올바른 훅 사용의 핵심입니다.


6.1 등록 vs 발화 시점

훅 등록 시점 (dx_add_hook 호출) 훅 발화 시점 (dx_run_hook 호출)
plugins/*/plugin.php 로드 시 (STEP 4) 테마 layout/main.php에서 dx_hook_top() 호출 시
extend/top/ 파일 실행 시 (STEP 4) 테마 layout/main.php에서 dx_hook_bottom() 호출 시
extend/middle/ 파일 실행 시 (STEP 5) extend/top/ 완료 후 dx_run_hook('dx_extend_top')
어디서나 가능 extend/middle/ 완료 후 dx_run_hook('dx_extend_middle')


6.2 발화 순서도 — 전체 훅 발화 흐름

요청 처리 순서                          발화되는 훅
─────────────────────────────────────────────────────────
load_plugins()                          (훅 등록만, 발화 없음)
  │
extend/top/ 실행
  │   └─ runTop() 내부                → dx_extend_top 발화
  │
Router::resolve()                       (발화 없음)
  │
extend/middle/ 실행
  │   └─ runMiddle() 내부             → dx_extend_middle 발화
  │
boards/handler.php or pages/xx.php      (발화 없음)
  │
layout/main.php: dx_hook_top()         → dx_top 발화
                                        → dx_{type}_top 발화
                                        → dx_page_{slug}_top 발화
  │
$dx_content 출력                        (발화 없음)
  │
layout/main.php: dx_hook_bottom()      → dx_bottom 발화
                                        → dx_{type}_bottom 발화
                                        → dx_page_{slug}_bottom 발화
  │
extend/bottom/ 실행
  │   └─ runBottom() 내부             → dx_extend_bottom 발화
  │
ob_end_flush() → ob_start 콜백 실행    (훅 아님, PHP 내부)
  │
shutdown functions                      (훅 아님, PHP 내부)


6.3 훅 컨텍스트 변수

각 훅이 발화될 때 전달되는 $context 배열의 내용입니다. extend/ 파일과 훅 콜백 모두 이 정보를 활용할 수 있습니다.
 
컨텍스트 키 예시 값 설명
dx_extend_top version 8.1.0 CMS 버전
dx_extend_top path /notice/view/42 요청 URI
dx_extend_middle type board 라우트 타입
dx_extend_middle route {type,slug,action,id,board} 전체 라우트 정보
dx_extend_bottom elapsed 12.34 경과 시간(ms)
dx_top type board 페이지 타입
dx_top slug notice 슬러그
dx_board_top board {board_key,name,...} 게시판 정보
dx_page_notice_top page {slug,title,...} 페이지 정보


7. Plugin이 실행 흐름에 개입하는 방식

Plugin은 독립적인 실행 주체가 아니라 Hook을 통해 적절한 시점에 실행됩니다. Plugin 자체는 load_plugins() 시점에 훅을 등록하고, 해당 훅이 발화될 때 비로소 동작합니다.


7.1 에디터 플러그인(ckeditor4-editor) 흐름 추적

// ─── STEP 4: load_plugins() ───────────────────────────
// plugins/ckeditor4-editor/plugin.php 실행

dx_register_plugin(array(
    'id'   => 'ckeditor4',
    'type' => 'editor',
    'name' => 'CKEditor 4',
));

// dx_editor_render 훅 등록
dx_add_hook('dx_editor_render', function($args) {
    if ($args['editor'] !== 'ckeditor4') return;  // 다른 에디터면 무시
    $name  = $args['name'];
    $value = $args['value'];
    echo '<textarea id="cke_' . $name . '" name="' . $name . '">' . $value . '</textarea>';
    echo '<script>CKEDITOR.replace("' . $name . '");</script>';
}, 10);

// ─── STEP 6: 게시판 글쓰기 페이지 렌더링 ──────────────
// boards/write.php (스킨 파일)
dx_run_hook('dx_editor_init', array(
    'name'  => 'content',
    'value' => '',
    'board' => $board,
));
// ↑ dx_editor_init 훅 발화 → load_plugins()에서 등록된 브릿지 실행
//   브릿지 내부: dx_render_editor() 호출
//   dx_render_editor() → dx_run_hook('dx_editor_render', ...)
//   → ckeditor4-editor의 콜백이 실행되어 에디터 HTML 출력


7.2 소켓 플러그인(dx-socket) 흐름 추적

// ─── STEP 4: dx-socket/plugin.php ──────────────────────
dx_register_plugin(array('id'=>'dx-socket','type'=>'socket',...));

// 1. 모든 페이지 하단에 소켓 클라이언트 JS 주입
dx_add_hook('dx_bottom', function($ctx) {
    if (dx_config('socket_enabled', '0') !== '1') return;
    echo '<script src="/plugins/dx-socket/socket-core.js.php"></script>';
}, 100);

// 2. 관리자 대시보드에 실시간 접속자 위젯 추가
dx_add_hook('dx_admin_dashboard_widget', function() {
    require __DIR__ . '/admin/widget.php';
}, 10);

// ─── STEP 6: layout/main.php ────────────────────────────
// dx_hook_bottom() 호출 → dx_run_hook('dx_bottom')
// → dx-socket의 콜백 실행 → 소켓 JS 태그 출력


8. 실전 패턴 모음


8.1 패턴 1: 점검 모드 (top/)

모든 초기화가 완료된 직후 실행되므로 Auth, dx_is_admin() 등을 사용할 수 있습니다.
 
// extend/top/01_maintenance.php
if (!defined('DX_CMS')) exit;

$maintenance = array('enabled'=>true, 'end_time'=>'2026-05-01 03:00');
if (!$maintenance['enabled']) return;

// Auth는 이미 초기화되어 있으므로 isAdmin() 사용 가능
if (function_exists('dx_is_admin') && dx_is_admin()) return;

http_response_code(503);
header('Retry-After: 3600');
echo '<h1>🔧 점검중 — 종료 예정: ' . $maintenance['end_time'] . '</h1>';
exit;
// exit 이후 extend/top/ 나머지 파일, Router, Dispatcher 전혀 실행 안 됨


8.2 패턴 2: 라우트 기반 리다이렉트 (middle/)

라우트가 확정된 후 실행되므로 $GLOBALS['dx_route']를 읽어 조건부 리다이렉트를 구현합니다.
 
// extend/middle/02_redirect_old_url.php
if (!defined('DX_CMS')) exit;

$route = isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array();

// 구 URL /board/notice → 새 URL /notice 리다이렉트
if (isset($route['type']) && $route['type'] === 'board'
    && isset($route['slug']) && $route['slug'] === 'board'
    && isset($route['action']) && $route['action'] === 'notice') {
    dx_redirect(dx_base_url('notice'), 301);
    exit;
}

// 비회원 접근 시 특정 게시판 차단
if (isset($route['slug']) && $route['slug'] === 'premium') {
    if (!Auth::getInstance()->isLoggedIn()) {
        dx_redirect(dx_base_url('auth/login') . '?redirect=' . urlencode(dx_current_url()));
        exit;
    }
}


8.3 패턴 3: 응답 후 비동기 처리 (middle/)

DB 집중 작업이나 외부 API 호출은 register_shutdown_function으로 응답 후 처리합니다. 사용자 체감 속도가 크게 향상됩니다.
 
// extend/middle/03_async_analytics.php
if (!defined('DX_CMS')) exit;

$route = isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array();
if (isset($route['type']) && $route['type'] === 'api') return;

$data = array(
    'url'       => dx_request_uri(),
    'ip'        => dx_ip(),
    'referer'   => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
    'timestamp' => date('Y-m-d H:i:s'),
);

// ← 응답 먼저 보내고 나서 처리
register_shutdown_function(function() use ($data) {
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();  // PHP-FPM: 즉시 응답 완료
    }
    // 여기서 외부 API 호출, 무거운 DB 작업 등 수행
    // 사용자는 이미 페이지를 받아 브라우저에서 렌더링 중
    $db = Database::getInstance();
    $db->query("INSERT INTO analytics ...", $data);
});


8.4 패턴 4: HTML 전체 가공 (top/ + bottom/ 조합)

ob_start 콜백으로 전체 HTML을 가공합니다. top/에서 콜백을 등록하고, 실제 가공은 ob_end_flush() 시점(bottom/ 이후)에 자동 실행됩니다.
 
// extend/top/05_html_processor.php
if (!defined('DX_CMS')) exit;

ob_start(function($buffer) {
    // 예1: 특정 텍스트 자동 링크화
    $buffer = preg_replace('/\bDXCMS\b/', '<a href="/">DXCMS</a>', $buffer);

    // 예2: 성능 측정 주석 삽입
    $elapsed = round((microtime(true) - DX_START) * 1000, 2);
    $buffer = str_replace('</body>',
        '<!-- DX: ' . $elapsed . 'ms --></body>', $buffer);

    return $buffer;
});
// ↑ 이 콜백은 index.php 마지막의 ob_end_flush() 시점에 실행됨
//   즉, bottom/ 실행이 끝난 후에 전체 HTML을 받아 가공


8.5 패턴 5: 슬롯 정보 활용

$dx_extend_slot 변수를 통해 현재 어느 슬롯(top/middle/bottom)에서 실행 중인지 알 수 있습니다.
 
// extend/top/99_multi_slot.php (실제로는 슬롯별로 분리 권장)
if (!defined('DX_CMS')) exit;

// $dx_extend_slot은 DxExtend::safeExec()가 자동 주입
// 'top', 'middle', 'bottom' 중 하나
if ($dx_extend_slot === 'top') {
    // top에서만 실행할 코드
} elseif ($dx_extend_slot === 'middle') {
    // middle에서만 실행할 코드
}

// context 변수도 extract()로 자동 주입
// top에서는 $version, $path 사용 가능
// middle에서는 $type, $route 사용 가능
// bottom에서는 $elapsed 사용 가능


9. 실행 흐름 디버깅 방법

Extend 구조에서 문제가 생겼을 때 어떻게 원인을 추적하는지 설명합니다.


9.1 DX_DEBUG 모드 활성화

// data/config.php 에서 설정
define('DX_DEBUG', true);

// DX_DEBUG = true 시:
// • 에러가 화면에 표시됩니다.
// • dx_log()의 info 레벨도 기록됩니다.
// • 에디터/페이지 에러가 노란 박스로 표시됩니다.

9.2 실행된 extend 파일 목록 확인
// extend/bottom/99_debug_extend.php (개발 환경에서만 사용)
if (!defined('DX_CMS')) exit;
if (!defined('DX_DEBUG') || !DX_DEBUG) return;

$executed = DxExtend::getInstance()->getExecuted();
echo '<pre style="position:fixed;bottom:0;left:0;background:#000;color:#0f0;
          font-size:11px;padding:10px;z-index:9999;max-height:200px;overflow:auto">';
echo 'Extend 실행 파일: ';
foreach ($executed as $item) {
    echo $item['slot'] . '/' . $item['file'] . " ";
}
echo '</pre>';


9.3 훅 등록 현황 확인

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

$allHooks = HookManager::getInstance()->getAll();
$executed = HookManager::getInstance()->getExecuted();

echo '<pre style="...">';
echo '등록된 훅: ' . implode(', ', $allHooks) . " ";
echo '발화된 훅: ' . implode(', ', $executed);
echo '</pre>';


9.4 에러 로그 확인

extend/ 파일에서 발생하는 에러는 DxExtend::safeExec()가 가로채어 data/error.log에 기록합니다.
 
// data/error.log 로그 형식 예시
[2026-05-01 12:00:00] [extend/top/01_maintenance.php:25]
  [DxExtend] Undefined variable: maintenance (line 25)

[2026-05-01 12:00:01] [extend/middle/02_custom.php:10]
  [DxExtend] Exception: Database connection failed

// → 파일명과 줄 번호가 함께 기록되어 원인 파악이 쉬움


9.5 성능 측정 패턴

// extend/bottom/98_perf_log.php (DX_DEBUG 모드 전용)
if (!defined('DX_CMS')) exit;
if (!defined('DX_DEBUG') || !DX_DEBUG) return;

// bottom 컨텍스트에서 $elapsed가 자동 주입됨
// (DxExtend::runBottom(['elapsed' => round(..., 2)]))

$elapsed = isset($elapsed) ? $elapsed : round((microtime(true) - DX_START) * 1000, 2);
$queries = Database::getInstance()->getQueryCount();
$route   = isset($GLOBALS['dx_route']) ? json_encode($GLOBALS['dx_route']) : '-';

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 &nbsp;|&nbsp; 🗄 ' . $queries . ' queries';
echo '<br>📍 ' . htmlspecialchars($route, ENT_QUOTES);
echo '</div>';


10. 빠른 참조 — 슬롯 선택 체크리스트


10.1 어느 슬롯에 넣어야 하나?

하고 싶은 일 슬롯 이유
점검 모드 (exit로 중단) top/ 인증•세션 사용 가능, 라우팅 전 차단
IP 차단 top/ 가장 빠른 시점, DB 조회도 가능
ob_start 콜백 등록 top/ 렌더링 전에 버퍼 설정해야 함
전역 PHP 변수 주입 top/ 모든 파일이 참조할 수 있도록 먼저 정의
훅 등록(dx_add_hook) top/ 또는 plugin.php 발화 전에 등록되어 있으면 됨
방문자 통계 기록 middle/ 라우트 확인 후 관리자•API 제외 가능
라우트 기반 리다이렉트 middle/ $GLOBALS['dx_route'] 사용 필요
A/B 테스트 분기 middle/ 라우트 확정 후 핸들러 전 처리 가능
캐시 저장 bottom/ 렌더링 완료 후 저장
성능 로그 출력 bottom/ 경과 시간($elapsed) 컨텍스트 제공
임시 파일 정리 bottom/ 요청 처리 완료 후 정리
JS 파일 script 태그 추가 bottom/ DOM 완성 후 추가 (또는 훅 사용)


10.2 가용 변수 체크리스트

변수/함수 top/ middle/ bottom/
DB::query(), db_*()
Auth::isLoggedIn(), dx_is_admin()
dx_config(), dx_log()
$GLOBALS['dx_route'] ❌ (미확정)
$GLOBALS['dx_route']['type']
$elapsed (경과 시간ms) ✅ (컨텍스트)
$version (CMS 버전) ✅ (컨텍스트)
$path (요청 URI) ✅ (컨텍스트)
$type (라우트 타입) ✅ (컨텍스트)
헤더 변경 (header()) ❌ (이미 출력)
ob_start() 호출 ⚠️ 가능하나 주의 ❌ (너무 늦음)


10.3 최종 요약

DXCMS Extend 구조 — 핵심 정리

1. index.php가 단일 진입점. ob_start()로 시작, ob_end_flush()로 마무리.
2. STEP 1~3: 클래스 로드 → 보안 → DB. Extend 개입 없음.
3. STEP 4: load_plugins() → 훅 등록 완료 → extend/top/ 실행.
   이 시점부터 모든 CMS 기능 사용 가능.
4. STEP 5: Router가 라우트 확정 → Dispatcher가 extend/middle/ 실행.
   이 시점부터 $GLOBALS['dx_route'] 사용 가능.
5. STEP 6: 핸들러 + 테마 렌더링. 레이아웃에서 Hook 발화.
6. STEP 7: extend/bottom/ 실행 → ob_end_flush() → 브라우저 전송.
7. 훅 콜백은 등록(dx_add_hook)과 발화(dx_run_hook)가 분리되어 있음.
8. Plugin은 load_plugins() 시 훅을 등록하고, 훅 발화 시 동작함.
9. 무거운 작업은 register_shutdown_function으로 응답 후 처리.
10. 코어 파일은 절대 수정하지 않는다. 모든 확장은 extend/를 통해.

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.8 Extend 구조 Extend 실제 소스 코드 완전 분석 • 12가지 실전 사례 2026.05.02 3.8 Extend 구조 코어 수정 없이 CMS를 확장하는 방법 2026.05.02 3.8 Extend 구조 실제 적용 흐름 2026.04.21 3.8 Extend 구조 Extend 개념 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록