1. 전체 실행 흐름 개요
DXCMS의 모든 요청은 index.php 단일 진입점을 통해 처리됩니다. 요청이 들어온 순간부터 응답이 완료될 때까지 5단계를 거치며, 각 단계마다 훅 포인트가 배치되어 있습니다.핵심 원칙
- 훅은 "CMS가 특정 작업을 완료한 직후" 또는 "특정 작업을 시작하기 직전"에 실행됩니다.
- 훅 등록(dx_add_hook)은 STEP 4 load_plugins() 시점에 완료되어야 합니다.
- 훅 실행(dx_run_hook)은 반드시 등록 이후에 이루어집니다. 등록 전 실행은 무시됩니다.
1.1 5단계 실행 구조 (index.php 기준)
1. STEP 1 — 클래스•함수 로드
functions.php, DxCache, Secure, Database, HookManager, PluginRegistry, Auth, DxExtend, Dispatcher 등 모든 클래스 파일을 require_once로 로드합니다. 이 단계는 정의만 하며 실행은 없습니다.
2. STEP 2 — 보안 초기화
세션 설정 → 세션 시작 → 보안 헤더 발행 → CSRF 토큰 발급 순서로 실행됩니다. 훅 없음.
3. STEP 3 — DB 연결 + 설정 로드
data/config.php를 실행해 DB 연결, dx_config 설정, DX_SECRET_KEY 주입을 완료합니다. 훅 없음.
4. STEP 4 — 초기화 + 훅 등록
HookManager, PluginRegistry, load_plugins(), DxSite, DxTheme, Auth 순서로 초기화. 플러그인이 여기서 dx_add_hook을 실행합니다.
dx_body_bottom (팝업) dx_after_login (회원모니터) dx_after_logout (회원모니터)
5. STEP 5 — 라우팅 + 디스패치 + 렌더링
routes/ 로드 → DxRouter → Dispatcher::dispatch() 순서. extend/top → 라우팅 → extend/middle → 핸들러 → 렌더링 → extend/bottom.
1.2 extend/ 폴더와 Hook의 실행 시점 비교
| 실행 방식 | 실행 시점 (index.php 기준) | 주요 용도 |
|---|---|---|
| extend/top/ | STEP 4 완료 직후 (플러그인•Auth•DxSite•DxTheme 모두 완료) | 점검 모드, IP 차단, 전역 변수 주입 |
| extend/middle/ | Dispatcher::dispatch() | 내부 라우트 확정 직후, 핸들러 실행 전 방문자 로그(01_visit_tracker.php), 접근 제어 |
| extend/bottom/ | Dispatcher 완료 직후 ob_end_flush() 직전 | 캐시 저장, 성능 로그, 정리 작업 |
| dx_head 훅 | 테마 main.php의 </head> 바로 앞 | CSS•JS•메타태그 삽입 |
| dx_top 훅 | 테마 main.php의 <body> 시작 직후 헤더 네비게이션 아래 | 공지 배너, 팝업 트리거 |
| dx_middle 훅 | 테마 main.php의 <main> 태그 안 실제 콘텐츠 바로 앞 | 사이드 위젯, 보조 콘텐츠 |
| dx_bottom 훅 | 테마 main.php의 </footer> 직후 스크립트 블록 앞 | 플로팅 버튼, 하단 위젯 |
| dx_footer_scripts 훅 | 테마 main.php의 모든 JS 다음 | 추가 스크립트 태그 삽입 |
| dx_body_bottom 훅 | 맨 마지막 (dx_footer_scripts 바로 다음) | 팝업 렌더링, 전역 JS 실행 |
2. index.php 단계별 실행 타이밍
2.1 STEP 4 — 초기화 완료 후 훅 등록
STEP 4는 훅 시스템이 활성화되는 핵심 단계입니다. load_plugins() 호출 시 plugins/ 폴더의 plugin.php 파일들이 실행되며, 각 플러그인이 dx_add_hook으로 콜백을 등록합니다.
HookManager::getInstance(); // 훅 매니저 싱글턴 생성
PluginRegistry::getInstance(); // 플러그인 레지스트리 생성
load_plugins(); // ← plugins/*/plugin.php 실행
// 이 시점에 dx_add_hook이 모두 등록됨
_dx_register_point_hooks(); // 포인트 시스템 훅 등록
DxSite::getInstance(); // 멀티사이트 도메인 설정 적용
DxTheme::getInstance(); // 테마 결정 (DxSite 이후)
Auth::getInstance(); // 세션 기반 인증 초기화
DxContainer::getInstance()->registerCoreServices();
// CMS가 직접 등록하는 훅들
dx_add_hook('dx_body_bottom', function() {
DxPopup::render(); // 팝업 자동 출력 (priority 50)
}, 50);
dx_add_hook('dx_after_login', function($args) {
DxMemberMonitor::onLogin((int)$args["user"]["id"]);
}, 20);
// extend/top/ 실행 — 모든 초기화 완료 직후
DxExtend::getInstance()->runTop(array(
'version' => DX_VERSION,
'path' => dx_request_uri(),
));
주의: 훅 등록 가능 최초 시점
dx_add_hook()은 HookManager가 생성된 이후부터 호출 가능합니다.
STEP 1의 함수 파일 로드 시점에는 훅을 등록할 수 없습니다 (HookManager 미생성).
플러그인의 plugin.php는 load_plugins() 내부에서 include 되므로 STEP 4가 안전한 등록 시점입니다.
2.2 STEP 5 — 라우팅과 extend/middle 실행
// index.php STEP 5 — Dispatcher 실행 전후
// ① routes/ 폴더 자동 로드 (DxRouter 기반 라우트)
$_dxRouteFiles = glob(DX_ROOT . "/routes/*.php");
foreach ($_dxRouteFiles as $f) require_once $f;
// ② 라우팅 + 디스패치
if (!DxRouter::dispatch()) {
$dispatcher = new Dispatcher(new Router());
$dispatcher->dispatch();
// dispatch() 내부에서:
// 1) Router::resolve() — 라우트 확정
// 2) extend/middle/ 실행 ← 이 시점에 $GLOBALS["dx_route"] 사용 가능
// 3) switch(type) — 핸들러 실행
}
// ③ 렌더링 완료 후 extend/bottom/ 실행
DxExtend::getInstance()->runBottom(array(
'elapsed' => round((microtime(true) - DX_START) * 1000, 2),
));
// ④ 출력 버퍼 flush
if (ob_get_level() > 0) ob_end_flush();
3. 테마 레이아웃 훅 실행 타이밍
테마의 themes/default/layout/main.php가 렌더링될 때 6개의 훅이 HTML 구조 내 특정 위치에서 실행됩니다. 각 훅이 HTML의 어느 위치에 있는지 정확히 파악해야 올바른 훅을 선택할 수 있습니다.
3.1 main.php HTML 구조와 훅 위치
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>...</title>
<style>...</style> <!-- CMS 기본 스타일 -->
★ [훅 1] dx_head ← 289번 줄 (</head> 바로 앞)
<!-- Google Analytics, 웹마스터 메타태그 추가 위치 -->
<script src="jquery.min.js"></script>
</head>
<body>
<!-- 헤더 네비게이션, 드로어 메뉴 -->
★ [훅 2] dx_top ← 649번 줄 (헤더 아래, 상단 유틸바 위)
<div class="dx-topbar"> <!-- 상단 유틸바 --></div>
<header>...</header> <!-- GNB 메뉴 --></header>
<main id="dx-main">
★ [훅 3] dx_middle ← 1135번 줄 (콘텐츠 바로 앞)
<!-- 실제 페이지 콘텐츠 ($dx_content) -->
<?php echo $dx_content; ?>
</main>
<footer>...</footer>
★ [훅 4] dx_bottom ← 1854번 줄 (</footer> 직후)
<script>/* CMS 기본 JS 코드 */</script>
★ [훅 5] dx_footer_scripts ← 2129번 줄
★ [훅 6] dx_body_bottom ← 2130번 줄
</body>
</html>
3.2 레이아웃 훅 상세 설명
| 훅 이름 | HTML 문맥 | 주요 용도 |
|---|---|---|
| dx_head | <head> 내부 </head> 바로 앞 | 외부 CSS 파일 링크 구글 폰트 로드 favicon 추가 커스텀 메타태그 |
| dx_top | <body> 내부 헤더 메뉴 아래 | 전체 페이지 공지 배너 점검 안내 띠 A/B 테스트 overlay |
| dx_middle | <main> 내부 $dx_content 앞 | 콘텐츠 앞 보조 안내 사이드 패널 맞춤형 위젯 |
| dx_bottom | </footer> 직후 <script> 블록 앞 | 플로팅 버튼(채팅, 상단이동) 하단 위젯 쿠키 동의 배너 |
| dx_footer_scripts | CMS JS 다음 </body> 직전 | 추가 <script> 태그 외부 라이브러리 로드 |
| dx_body_bottom | 맨 마지막 (dx_footer_scripts 다음) | 팝업 렌더링(priority 50) 전역 JS 초기화 디버그 도구 |
3.3 dx_top의 세분화 — 페이지 타입별 자동 훅
dx_hook_top(), dx_hook_middle(), dx_hook_bottom() 함수는 context의 type과 slug 값에 따라 추가 훅을 자동으로 실행합니다. 이를 통해 특정 페이지 타입에서만 실행되는 훅을 만들 수 있습니다.
// HookManager.php의 dx_hook_top() 내부 구현
function dx_hook_top($context = array()) {
dx_run_hook('dx_top', $context); // ① 전체 페이지 공통
if (isset($context['type'])) {
dx_run_hook('dx_' . $context['type'] . '_top', $context);
// 예: type='board' → dx_board_top
// 예: type='page' → dx_page_top
// 예: type='admin' → dx_admin_top ← 관리자 전용!
}
if (isset($context['slug'])) {
dx_run_hook('dx_page_' . $context['slug'] . '_top', $context);
// 예: slug='notice' → dx_page_notice_top
// 예: slug='free' → dx_page_free_top
}
}
// ─────────────────────────────────────────────
// 실제 활용 예시: 게시판에서만 실행
dx_add_hook('dx_board_top', function($context) {
echo '<div class="board-notice">게시판 이용 규칙...</div>';
});
// notice 게시판 목록에서만 실행
dx_add_hook('dx_page_notice_top', function($context) {
echo '<div class="notice-banner">공지사항 배너</div>';
});
4. 게시판 핸들러 훅 실행 타이밍
게시판 요청(boards/handler.php)은 자체적인 훅 포인트를 가집니다. 핸들러 진입부터 렌더링 완료까지 총 8개의 훅 포인트가 배치되어 있습니다.
4.1 boards/handler.php 실행 흐름
// boards/handler.php 실행 순서 (실제 소스 기반)
// ─── 공통 진입 훅 ─────────────────────────────────
dx_run_hook('dx_board_before', array(
'board' => $board,
'action' => $action, // list | view | write | edit | reply | delete
'skin' => $boardSkin,
'id' => $id,
));
// ─── 액션별 분기 (switch) ─────────────────────────
switch ($action) {
case 'list':
// 목록 컨텍스트 준비 후:
dx_run_hook('dx_board_list_context',
array('context' => &$ctx, 'board' => $board)); // 참조 전달
// 렌더링...
break;
case 'view':
// 상세 컨텍스트 준비 후:
dx_run_hook('dx_board_view_context',
array('context' => &$ctx, 'board' => $board, 'post' => $post));
// 렌더링...
break;
case 'write':
case 'edit':
dx_run_hook('dx_board_write_context',
array('context' => &$ctx, 'board' => $board));
// 저장 처리 시:
dx_run_hook('dx_board_before_save',
array('data' => &$data, 'board' => $board, 'action' => $action));
// DB 저장 완료 후:
dx_run_hook('dx_after_write',
array('post_id' => $postId, 'board' => $board, 'data' => $data));
dx_run_hook('dx_board_after_save',
array('post_id' => $postId, 'board' => $board, 'redirect' => &$redirect));
break;
case 'delete':
dx_run_hook('dx_board_before_delete',
array('post' => $post, 'board' => $board));
// 삭제 처리...
dx_run_hook('dx_board_after_delete',
array('post_id' => $id, 'board' => $board));
break;
}
// ─── 공통 종료 훅 ─────────────────────────────────
dx_run_hook('dx_board_after', array(
'board' => $board,
'action' => $action,
'skin' => $boardSkin,
));
4.2 게시판 훅 타이밍 표
| 훅 이름 | 실행 시점 | 인자 특이사항 |
|---|---|---|
| dx_board_before | 핸들러 진입 즉시 모든 처리 전 | 일반 전달. 여기서 exit()하면 게시판 전체 차단 가능 |
| dx_board_list_context | 목록 데이터 조회 완료 렌더링 직전 | context를 참조(&)로 전달. 콘텐츠를 변형하면 목록에 반영됨 |
| dx_board_view_context | 게시글 조회 완료 렌더링 직전 | context, post 참조 전달 |
| dx_board_write_context | 작성 폼 렌더링 직전 | context 참조 전달 |
| dx_board_before_save | DB 저장 직전 (write•edit•reply 공통) | data를 참조(&)로 전달. 값을 수정하면 실제 저장 데이터에 반영됨 |
| dx_after_write | DB 저장 완료 직후 | 포인트, 알림, 통계 처리에 사용 |
| dx_board_after_save | dx_after_write 바로 다음 | redirect를 참조로 전달. 저장 후 이동 URL 변경 가능 |
| dx_board_before_delete | 삭제 처리 직전 | 연관 파일•댓글 정리에 사용 |
| dx_board_after_delete | 삭제 완료 직후 | 캐시 무효화, 통계 갱신에 사용 |
| dx_board_after | 모든 처리 완료 handler.php 맨 끝 | 로그, 정리 작업 |
5. 인증•커뮤니티 훅 실행 타이밍
5.1 로그인 관련 훅
// core/auth/Auth.php 내부 — login() 메서드
// DB에서 사용자 조회 → 비밀번호 검증 → 세션 저장 완료 후:
dx_run_hook('dx_after_login', array('user' => $user));
// ↑ 이 훅이 실행되는 시점에는 세션에 사용자 정보가 이미 저장되어 있음
// ↑ Auth::getInstance()->isLoggedIn() === true
// core/auth/Auth.php 내부 — logout() 메서드
// 세션에서 사용자 정보를 제거하기 전:
dx_run_hook('dx_after_logout', array('user' => $user));
// ↑ 이 훅 실행 시점에는 아직 세션이 살아있음
// core/auth/Auth.php 내부 — register() 메서드
// DB에 회원 INSERT 완료 후:
dx_run_hook('dx_after_register', array(
'user_id' => $id,
'data' => $safeData, // 저장된 회원 데이터
));
5.2 커뮤니티 활동 훅 타이밍
| 훅 이름 | 위치 파일 | 실행 시점 |
|---|---|---|
| dx_after_comment | core/api/comment.php | DB에 댓글 INSERT 완료 직후 알림 발송, 포인트 지급에 활용 |
| dx_after_like | core/api/like.php | 좋아요 DB 처리 완료 직후 게시물 소유자에게 알림 발송 |
| dx_add_friend | core/DxFriend.php | 친구 관계 DB 저장 완료 직후 |
| dx_after_point | core/DxPoint.php | 포인트 지급/차감 DB 저장 후 레벨업 체크 전 |
| dx_levelup | core/DxPoint.php | 레벨업 조건 충족 확인 후 DB 레벨 업데이트 완료 직후 |
| dx_shop_after_purchase | core/DxShop.php | 결제 완료 + 주문 DB 저장 후 디지털 상품 배포에 활용 |
6. 훅 등록과 실행의 순서 관계
6.1 등록 → 실행 타이밍 원칙
핵심 규칙
dx_add_hook()은 반드시 dx_run_hook() 호출 이전에 실행되어야 합니다.
STEP 4의 load_plugins()에서 plugin.php가 실행되므로, 플러그인의 dx_add_hook은 STEP 5보다 항상 앞에 있습니다.
예외: extend/top/ 파일에서 dx_add_hook을 등록하면, 해당 파일은 load_plugins() 이후에 실행되므로 테마 훅(dx_head 등)에 등록 가능합니다.
6.2 훅 등록 가능 위치 정리
| 등록 위치 | 실행 시점 | 등록 가능한 훅 범위 |
|---|---|---|
| plugins/*/plugin.php | STEP 4 — load_plugins() | 모든 훅 (가장 권장) |
| extend/top/*.php | STEP 4 완료 직후 | 테마 훅(dx_head, dx_top ...) 포함 모든 훅 |
| extend/middle/*.php | Dispatcher::dispatch() 내부 | dx_middle, dx_bottom 등 렌더링 훅만 |
| extend/bottom/*.php | 렌더링 완료 후 | 훅 등록 불가 (이미 모든 훅 실행 완료) |
| data/config.php | STEP 3 | HookManager 미생성 → 훅 등록 불가 |
6.3 등록 타이밍 오류 예시
// ❌ 잘못된 예: extend/bottom에서 dx_top 훅 등록
// extend/bottom/bad_hook.php
dx_add_hook('dx_top', function($ctx) {
echo '<div>이 코드는 절대 실행되지 않습니다</div>';
});
// → dx_top은 렌더링 중(STEP 5)에 이미 실행됨
// → extend/bottom은 렌더링 완료 후 실행됨
// → 등록 시점이 실행 시점보다 늦어 콜백이 무시됨
// ✅ 올바른 예: plugin.php에서 dx_top 훅 등록
// plugins/my-plugin/plugin.php
dx_add_hook('dx_top', function($ctx) {
echo '<div>이 코드는 정상 실행됩니다</div>';
});
// → plugin.php는 STEP 4에서 실행됨
// → dx_top은 STEP 5 렌더링 중에 실행됨
// → 등록이 실행보다 먼저 완료됨 → 정상 작동
7. 전체 훅 실행 순서 다이어그램
아래는 HTTP 요청이 들어온 순간부터 응답이 완료될 때까지 모든 훅이 실행되는 순서를 나타낸 다이어그램입니다.
HTTP 요청 수신
↓
STEP 1: 클래스•함수 로드 (실행 없음)
functions.php → DxCache → Secure → Database →
HookManager → PluginRegistry → Auth → DxExtend → ...
↓
STEP 2: 보안 초기화 (세션, 보안 헤더, CSRF)
↓
STEP 3: DB 연결 + data/config.php 실행
↓
STEP 4: 초기화 + 훅 등록
load_plugins() ← 플러그인 dx_add_hook 등록 완료
DxSite → DxTheme → Auth
CMS 내부 dx_add_hook 등록
★ extend/top/ 실행 ← 최초 extend 실행 지점
↓
STEP 5: 라우팅 + 디스패치
Router::resolve() → 라우트 확정
★ extend/middle/ 실행 ← dx_route 확정 직후
switch(type) → 핸들러 실행
├── 게시판: dx_board_before
│ dx_board_{action}_context
│ dx_board_before_save (저장 시)
│ dx_after_write / dx_board_after_save
│ dx_board_after
└── 기타: 페이지, 관리자, API 처리...
테마 layout/main.php 렌더링
├── <head> 안 → ★ dx_head
├── 헤더 아래 → ★ dx_top (+ dx_{type}_top)
├── <main> 안 → ★ dx_middle (+ dx_{type}_middle)
├── 푸터 아래 → ★ dx_bottom (+ dx_{type}_bottom)
├── 스크립트 후 → ★ dx_footer_scripts
└── 맨 마지막 → ★ dx_body_bottom (팝업 포함)
★ extend/bottom/ 실행 ← 렌더링 완료 후
↓
ob_end_flush() → HTTP 응답 전송 완료
(register_shutdown_function 콜백 실행)
★ visit_logs INSERT ← 응답 후 비동기 처리
8. 실전 타이밍 선택 가이드
목적에 맞는 훅을 빠르게 선택하기 위한 의사결정 가이드입니다.
8.1 목적별 훅 선택
| 하고 싶은 것 | 선택할 훅 | 이유 |
|---|---|---|
| 모든 페이지 <head>에 CSS 추가 | dx_head | HTML head 내부에서 실행됨 |
| 모든 페이지 상단에 배너 표시 | dx_top | 헤더 아래, 콘텐츠 위에서 실행됨 |
| 게시판 목록 상단에만 안내 추가 | dx_board_top | type=board일 때만 실행됨 |
| notice 게시판에만 추가 UI | dx_page_notice_top | slug=notice일 때만 실행됨 |
| 페이지 하단 플로팅 버튼 | dx_bottom | 푸터 아래, 스크립트 앞에서 실행됨 |
| 외부 JS 라이브러리 추가 | dx_footer_scripts | 모든 CMS JS 로드 후 실행됨 |
| 팝업 / 전역 초기화 JS | dx_body_bottom | 맨 마지막 실행 (DOM 완성 후) |
| 로그인 후 포인트 지급 | dx_after_login | 세션 저장 완료 후 실행됨 |
| 글 저장 전 내용 검사•수정 | dx_board_before_save | data 참조(&)로 전달, 수정 반영됨 |
| 글 저장 후 알림 발송 | dx_after_write | DB 저장 완료 후 실행됨 |
| 점검 모드 구현 | extend/top/ | 모든 초기화 완료 후 가장 먼저 실행 |
| 방문자 IP 로깅 | extend/middle/ | 라우트 확정 후, 응답 후 비동기 처리 |
8.2 타이밍 오류 체크리스트
- 훅이 실행되지 않는다면: plugin.php에서 dx_add_hook을 등록했는지 확인 (config.php나 extend/bottom은 불가)
- 레이아웃 훅(dx_head 등)이 실행되지 않는다면: API, 단독 페이지(is_standalone) 요청인지 확인 — 이 경우 main.php가 로드되지 않음
- dx_board_before_save에서 데이터 수정이 반영 안 된다면: &$args['data'] 참조로 받아야 함
- dx_after_login 훅이 실행 안 된다면: 소셜 로그인 콜백(_callback) 파일에서 Auth::login()이 호출되는지 확인
- extend/middle에서 등록한 훅이 실행 안 된다면: 이미 지나간 훅에 등록한 것 — plugin.php로 이동 필요
최종 요약
훅 등록은 STEP 4 load_plugins()에서 완료되어야 합니다. plugin.php가 가장 안전합니다.
레이아웃 훅(dx_head~dx_body_bottom)은 테마 main.php 렌더링 중에 실행됩니다.
extend/top/은 모든 초기화 완료 후 라우팅 전, extend/middle/은 라우트 확정 후, extend/bottom/은 렌더링 완료 후입니다.
게시판 훅은 boards/handler.php 내에서, 인증 훅은 Auth.php 내에서 실행됩니다.
dx_body_bottom은 테마 렌더링의 맨 마지막이며, 팝업(priority 50)보다 작은 priority를 주면 팝업보다 먼저 실행됩니다.