회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
3.8 Extend 구조

실제 적용 흐름

D DX
2026.04.21 01:00(수정됨) 136 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

로그인 후 댓글을 작성할 수 있습니다.
1. DX 철학 / 개념 왜 DXCMS를 만들었는가 2026.04.20 1. DX 철학 / 개념 DXCMS란 무엇인가 2026.04.20 DXCMS 활용 (CMS) DXCMS 날코딩•막코딩 완전 허용 2026.04.12
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록