1. Hook 시스템이란?
Hook(훅) 시스템은 CMS 핵심 코드를 수정하지 않고도 동작에 끼어들거나 값을 변형할 수 있게 해주는 확장 메커니즘입니다. DXCMS는 WordPress의 Action/Filter 개념을 PHP 5.6+ 호환으로 경량화하여 내장하고 있습니다.
핵심 개념 한 줄 요약
- 훅 포인트 = CMS가 "여기서 끼어들어도 돼" 하고 열어 둔 자리
- 콜백 등록 = 개발자가 "나 여기서 이 코드 실행할게" 하고 예약하는 것
- 훅 실행 = CMS가 그 자리에 도달하면 등록된 콜백을 순서대로 호출하는 것
1.1 Hook의 두 가지 종류
| 종류 |
설명 |
| Action Hook (dx_run_hook) |
CMS의 특정 시점에 코드를 실행합니다. 반환값 없음. "이 시점에 내 코드를 실행하라"는 명령입니다. |
| Filter Hook (dx_apply_filter) |
값이 출력되기 전 가로채어 변형합니다. 반환값이 있음. "이 값을 내가 원하는 형태로 바꿔라"는 명령입니다. |
1.2 Hook vs extend/ 폴더 비교
DXCMS에는 Hook 외에 extend/ 폴더라는 또 다른 확장 수단이 있습니다. 두 방식의 차이를 명확히 이해해야 합니다.
| 구분 |
Hook (dx_add_hook) |
extend/ 폴더 |
| 등록 방법 |
코드에서 함수 호출로 등록 |
파일을 폴더에 넣으면 자동 실행 |
| 실행 시점 |
훅 이름으로 정밀 제어 |
top / middle / bottom 3단계만 |
| 우선 순위 |
priority 숫자로 세밀 조정 |
파일명 오름차순으로만 제어 |
| 대상 |
개발자 (플러그인 제작자) |
누구나 (설정 파일, 간단한 코드) |
| 격리 |
try-catch로 자체 관리 필요 |
파일별 자동 에러 격리 |
2. HookManager 클래스 구조
HookManager는 싱글턴(Singleton) 패턴으로 구현되어 있으며 core/hook/HookManager.php에 위치합니다. 개발자는 직접 클래스를 사용하지 않고 전역 헬퍼 함수를 사용합니다.
2.1 핵심 메서드
| 메서드 / 함수 |
설명 |
| dx_add_hook($name, $callback, $priority) |
훅 포인트에 콜백 등록. priority가 낮을수록 먼저 실행 (기본값 10) |
| dx_run_hook($name, $args) |
Action 훅 실행. 등록된 콜백을 순서대로 호출. 반환값 없음 |
| dx_apply_filter($name, $value, $args) |
Filter 훅 실행. 값을 콜백 체인에 통과시켜 변형된 값 반환 |
| dx_add_filter($name, $callback, $priority) |
dx_add_hook의 별칭. Filter 의미를 명확히 하기 위해 사용 |
| dx_remove_hook($name, $callback) |
등록된 특정 콜백 제거. callback 생략 시 해당 훅 전체 제거 |
| dx_has_hook($name) |
해당 훅에 콜백이 등록되어 있는지 확인. boolean 반환 |
2.2 priority(우선순위) 동작 방식
priority는 정수값으로, 숫자가 작을수록 먼저 실행됩니다. 같은 priority면 등록 순서대로 실행됩니다.
// priority 1 → 가장 먼저 실행
dx_add_hook('dx_head', function() { echo '<!-- 먼저 -->'; }, 1);
// priority 10 → 기본값, 두 번째 실행
dx_add_hook('dx_head', function() { echo '<!-- 기본 -->'; });
// priority 999 → 가장 나중에 실행
dx_add_hook('dx_head', function() { echo '<!-- 마지막 -->'; }, 999);
3. 표준 훅 포인트 전체 목록
DXCMS v8.1.0은 아래 훅 포인트를 기본 제공합니다. 플러그인 개발 시 이 목록에서 필요한 훅을 선택해 사용합니다.
3.1 레이아웃 훅 (Layout Hooks)
테마 main.php에서 실행되는 훅입니다. HTML 출력에 직접 개입합니다.
| 훅 이름 |
실행 위치 / 용도 |
| dx_head |
<head> 태그 내부. CSS•JS 링크, 메타 태그 추가 |
| dx_top |
<body> 시작 직후. 전역 배너, 공지, 분석 스크립트 |
| dx_middle |
콘텐츠 영역 내부. 페이지별 맞춤 콘텐츠 삽입 |
| dx_bottom |
</body> 직전. 하단 공통 위젯, 팝업 |
| dx_footer_scripts |
body 닫기 직전 스크립트 영역. JS 초기화 코드 |
| dx_body_bottom |
최종 출력 직전. 전역 JS 실행 (priority 1 = 최우선) |
페이지 타입별 세분화 훅도 자동 생성됩니다. context의 type 값에 따라 다음과 같이 추가 실행됩니다.
// type='board' 게시판 페이지라면 아래 훅들이 추가로 실행됩니다:
dx_board_top // dx_{type}_top
dx_board_middle // dx_{type}_middle
dx_board_bottom // dx_{type}_bottom
// slug='notice' 페이지라면:
dx_page_notice_top
dx_page_notice_middle
dx_page_notice_bottom
3.2 인증 훅 (Auth Hooks)
| 훅 이름 |
전달 인자 / 용도 |
| dx_after_login |
array('user' => $user) — 로그인 성공 후. 포인트 지급, 알림 발송 |
| dx_after_logout |
array('user' => $user) — 로그아웃 후. 세션 정리, 로그 기록 |
| dx_after_register |
array('user_id' => $id, 'data' => ...) — 회원가입 완료 후. 환영 메일 발송 |
3.3 게시판 훅 (Board Hooks)
| 훅 이름 |
전달 인자 / 용도 |
| dx_board_before |
board, action, skin, id — 게시판 처리 시작 전. 접근 제어 |
| dx_board_before_save |
data(참조), board, action — 저장 직전 데이터 변형 가능 |
| dx_board_after_save |
post_id, board, redirect(참조) — 저장 완료 후. 알림, 포인트 |
| dx_after_write |
post_id, board, data — 글 작성 완료 후. 전역 처리 |
| dx_board_before_delete |
post, board_key — 삭제 직전. 관련 파일 정리 |
| dx_board_after_delete |
post_id, board_key — 삭제 완료 후 |
| dx_board_list_context |
context(참조), board — 목록 컨텍스트 변형 |
| dx_board_view_context |
context(참조), board, post — 상세 컨텍스트 변형 |
3.4 커뮤니티 활동 훅
| 훅 이름 |
전달 인자 / 용도 |
| dx_after_comment |
post_id, board_key, comment_id, member_id — 댓글 작성 후 |
| dx_after_like |
post_id, owner_id — 좋아요 클릭 후. 포인트, 알림 |
| dx_add_friend |
member_id, target_id — 친구 추가 완료 후 |
| dx_after_point |
member_id, type, point, balance — 포인트 변동 후 |
| dx_levelup |
member_id, old_level, new_level — 레벨 상승 시 |
3.5 플러그인•드라이버 훅
| 훅 이름 |
용도 |
| dx_editor_render |
에디터 HTML 렌더링. CKEditor4 등 커스텀 에디터 적용 |
| dx_editor_init |
에디터 초기화 JS 코드 삽입 |
| dx_payment_request |
결제 처리. 토스•카카오•이니시스 등 PG사 플러그인이 등록 |
| dx_shop_after_purchase |
구매 완료 후. 디지털 상품 배포, 쿠폰 발급 |
| dx_mailer_drivers |
메일 드라이버 등록. SMTP•SendGrid 등 커스텀 드라이버 |
| dx_sms_drivers |
SMS 드라이버 등록. CoolSMS•NCP 등 커스텀 드라이버 |
| dx_captcha_drivers |
Captcha 드라이버 등록. hCaptcha•Turnstile 등 |
3.6 관리자 훅
| 훅 이름 |
용도 |
| dx_admin_top |
관리자 패널 상단. 공지, 긴급 알림 |
| dx_admin_bottom |
관리자 패널 하단 |
| dx_admin_dashboard_widgets |
대시보드 위젯 추가 (dx-socket 플러그인 등에서 활용) |
4. Action Hook 실전 예제
4.1 페이지 하단에 성능 표시 (디버그)
example-plugin/plugin.php 참고. 모든 페이지 우측 하단에 실행 시간과 DB 쿼리 수를 출력합니다.
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); // priority 999 = 가장 마지막에 실행
}
4.2 로그인 후 환영 메시지 표시
dx_add_hook('dx_after_login', function($args) {
$user = $args['user'];
$_SESSION['welcome_msg'] = $user['nickname'] . '님, 환영합니다!';
});
// 이후 dx_top 훅에서 세션 메시지를 화면에 표시
dx_add_hook('dx_top', function() {
if (!empty($_SESSION['welcome_msg'])) {
echo '<div class="dx-alert dx-alert-success">'
. htmlspecialchars($_SESSION['welcome_msg'])
. '</div>';
unset($_SESSION['welcome_msg']);
}
});
4.3 게시글 저장 전 데이터 보정
dx_board_before_save 훅의 data 인자는 참조(&)로 전달됩니다. 값을 수정하면 실제 저장 데이터에 반영됩니다.
dx_add_hook('dx_board_before_save', function($args) {
$data = &$args['data']; // 참조 전달 — 수정하면 저장에 반영됨
$board = $args['board'];
// 제목이 비어 있으면 내용 앞 20자를 자동 제목으로
if (empty($data['title']) && !empty($data['content'])) {
$data['title'] = mb_substr(strip_tags($data['content']), 0, 20) . '...';
}
// 특정 게시판에서 금지어 필터
if ($board['board_key'] === 'free') {
$data['content'] = str_replace(['광고', '스팸'], '***', $data['content']);
}
});
4.4 회원가입 후 환영 메일 발송
dx_add_hook('dx_after_register', function($args) {
$userId = $args['user_id'];
$data = $args['data'];
try {
$mailer = DxMailer::getInstance();
$mailer->send(
$data['email'],
'회원가입을 환영합니다!',
'<h2>안녕하세요 ' . htmlspecialchars($data['nickname']) . '님</h2>'
. '<p>DXCMS에 가입해 주셔서 감사합니다.</p>'
);
} catch (Exception $e) {
dx_log('[welcome-mail] ' . $e->getMessage(), 'warning');
}
});
4.5 결제 플러그인 연동 (dx_payment_request)
결제 플러그인들은 dx_payment_request 훅에 자신을 등록합니다. 실제 tosspay-payment/plugin.php 구조입니다.
// plugins/my-payment/plugin.php
dx_add_hook('dx_payment_request', function($data) {
// $data['payment'] 에 결제 수단 ID가 담겨 있음
if ($data['payment'] !== 'my_payment') return; // 내 플러그인만 처리
// 결제 처리 로직
$amount = (int)$data['amount'];
$orderId = $data['order_id'];
// 결제 API 호출...
header('Content-Type: application/json');
echo json_encode(array('success' => true, 'redirect' => '/payment/complete'));
exit;
});
4.6 head에 CSS•JS 삽입
dx_head 훅은 <head> 태그 내부에서 실행됩니다. 플러그인 전용 스타일시트나 스크립트를 삽입할 때 사용합니다.
dx_add_hook('dx_head', function() {
$base = rtrim(dx_base_url(''), '/');
echo '<link rel="stylesheet" href="' . $base . '/plugins/my-plugin/style.css">';
echo '<script src="' . $base . '/plugins/my-plugin/script.js"></script>';
});
// body 닫기 직전에 JS 실행이 필요할 때
dx_add_hook('dx_body_bottom', function() {
echo '<script>
document.addEventListener("DOMContentLoaded", function() {
MyPlugin.init();
});
</script>';
});
5. Filter Hook 실전 예제
Filter 훅은 값을 가로채어 변형한 뒤 돌려줍니다. 반드시 변형된 값을 return 해야 합니다. DXCMS에서는 dx_apply_filter()와 dx_add_filter()를 사용합니다.
Filter 훅 사용 원칙
콜백에서 반드시 $value를 return 해야 합니다. return을 빠뜨리면 이후 콜백에 null이 전달됩니다.
여러 콜백이 체인처럼 연결됩니다. 앞 콜백의 반환값이 다음 콜백의 $value가 됩니다.
원본 값의 타입을 유지하는 것이 좋습니다. 문자열로 받았으면 문자열을 반환하세요.
5.1 기본 Filter 훅 등록과 실행 구조
// ① CMS 내부 코드 (훅 포인트 정의)
$content = dx_apply_filter('dx_post_content', $rawContent, array('post_id' => $id));
echo $content; // 필터링된 내용 출력
// ② 플러그인/확장 코드 (필터 등록)
dx_add_filter('dx_post_content', function($value, $args) {
// URL을 자동으로 링크로 변환
$value = preg_replace(
'/(https?:\/\/[^\s<>"]+)/',
'<a href="$1" target="_blank">$1</a>',
$value
);
return $value; // 반드시 return!
});
5.2 다중 Filter 체인
같은 훅에 여러 필터를 등록하면 priority 순으로 체인을 형성합니다.
// 필터 1: HTML 태그 허용 목록 적용 (priority 5)
dx_add_filter('dx_post_content', function($value) {
$allowed = '<p><br><strong><em><ul><ol><li><a><img>';
return strip_tags($value, $allowed);
}, 5);
// 필터 2: 이모지 변환 (priority 10 = 기본, 1 다음에 실행)
dx_add_filter('dx_post_content', function($value) {
return str_replace(':)', '😊', $value);
}, 10);
// 필터 3: 광고 문구 차단 (priority 20, 가장 나중)
dx_add_filter('dx_post_content', function($value) {
return preg_replace('/광고[^\n]*/u', '[광고 차단]', $value);
}, 20);
6. 플러그인에서 Hook 등록하기
DXCMS 플러그인은 plugins/{plugin-name}/plugin.php 파일에 훅을 등록합니다. 이 파일은 CMS 초기화 완료 후 자동으로 include됩니다.
6.1 플러그인 파일 기본 구조
<?php
/**
* My Plugin - 플러그인 설명
*/
if (!defined('DX_CMS')) exit('Direct access not allowed.');
// ① 페이지 상단에 안내 배너 표시
dx_add_hook('dx_top', function($context) {
if (!isset($context['type'])) return;
echo '<div class="my-notice">공지: 서비스 점검 예정 안내</div>';
}, 1);
// ② 레벨업 시 알림 발송
dx_add_hook('dx_levelup', function($args) {
$memberId = $args['member_id'];
$newLevel = $args['new_level'];
// 알림 DB 저장 등 처리
DxNotification::send($memberId, '레벨이 ' . $newLevel . '로 상승했습니다!');
});
// ③ 관리자 대시보드 위젯 추가
dx_add_hook('dx_admin_dashboard_widgets', function() {
echo '<div class="dx-card"><h3>My Plugin 통계</h3>.....</div>';
});
6.2 훅 제거 (우선순위로 교체)
기존에 등록된 훅을 제거하거나 새 콜백으로 교체할 수 있습니다.
// 특정 콜백 제거
$myCallback = function($args) { /* ... */ };
dx_add_hook('dx_top', $myCallback);
// 나중에 제거
dx_remove_hook('dx_top', $myCallback);
// 해당 훅 전체 콜백 제거 (callback 생략)
dx_remove_hook('dx_top');
6.3 훅 등록 여부 확인
if (dx_has_hook('dx_payment_request')) {
// 결제 플러그인이 설치되어 있을 때만 UI 표시
echo '<button>결제하기</button>';
} else {
echo '<p>결제 플러그인을 설치해 주세요.</p>';
}
7. context 인자 활용
dx_hook_top / dx_hook_middle / dx_hook_bottom으로 실행되는 레이아웃 훅에는 context 배열이 전달됩니다. 이 값으로 현재 페이지 종류를 판단할 수 있습니다.
7.1 context 주요 키
| 키 |
설명 / 예시 |
| type |
페이지 유형. 'board', 'page', 'auth', 'admin', 'search' 등 |
| slug |
페이지 슬러그. board_key 또는 page slug. 예: 'free', 'notice' |
| action |
현재 액션. 'list', 'view', 'write', 'edit' 등 |
| board_key |
게시판 키값 (type=board일 때). 예: 'free' |
7.2 특정 페이지에서만 실행
dx_add_hook('dx_bottom', function($context) {
// 게시판 목록 페이지에서만 실행
if (($context['type'] ?? '') !== 'board') return;
if (($context['action'] ?? '') !== 'list') return;
echo '<script>console.log("게시판 목록 페이지");</script>';
});
dx_add_hook('dx_middle', function($context) {
// 'free' 게시판에서만 사이드 위젯 표시
if (($context['board_key'] ?? '') !== 'free') return;
echo '<aside class="free-board-widget">.....</aside>';
});
8. 커스텀 훅 포인트 만들기
플러그인이나 테마에서 직접 훅 포인트를 정의하면, 다른 플러그인이 해당 시점에 끼어들 수 있습니다. 오픈 구조를 만들어 확장성을 높이는 방법입니다.
8.1 Action 훅 포인트 정의
// 내 플러그인 A: 처리 완료 후 훅 포인트 제공
function my_plugin_process($data) {
// 처리 로직...
$result = do_something($data);
// 다른 플러그인이 끼어들 수 있는 포인트 제공
dx_run_hook('my_plugin_after_process', array(
'data' => $data,
'result' => $result,
));
return $result;
}
// 플러그인 B: A의 훅 포인트를 활용
dx_add_hook('my_plugin_after_process', function($args) {
dx_log('[플러그인B] 처리 완료: ' . json_encode($args['result']), 'info');
});
8.2 Filter 훅 포인트 정의
// 플러그인 A: 출력 전 필터 포인트 제공
function my_plugin_render($html) {
// 다른 플러그인이 HTML을 변형할 수 있게 허용
$html = dx_apply_filter('my_plugin_render', $html);
echo $html;
}
// 플러그인 C: A의 HTML에 광고 삽입
dx_add_filter('my_plugin_render', function($html) {
return $html . '<div class="ad">광고</div>';
});
9. 디버그 & 트러블슈팅
9.1 실행된 훅 목록 확인
HookManager::getExecuted()로 현재 요청에서 실행된 훅 목록을 확인할 수 있습니다.
if (defined('DX_DEBUG') && DX_DEBUG) {
dx_add_hook('dx_bottom', function() {
$executed = HookManager::getInstance()->getExecuted();
echo '<pre style="font-size:11px;background:#f5f5f5;padding:10px">';
echo '실행된 훅 (' . count($executed) . '개):\n';
print_r($executed);
echo '</pre>';
}, 9999);
}
9.2 등록된 훅 전체 목록 확인
$allHooks = HookManager::getInstance()->getAll();
// 반환 예시:
// ['dx_head', 'dx_body_bottom', 'dx_after_login', 'dx_payment_request', ...]
// 특정 훅에 몇 개의 콜백이 등록되어 있는지 확인
$cnt = HookManager::getInstance()->count('dx_payment_request');
// 예: 2 (tosspay + kakaopay 두 개 플러그인이 등록된 경우)
9.3 자주 발생하는 실수
| 실수 |
올바른 방법 |
| Filter 훅에서 return을 빠뜨림 |
콜백 내에서 반드시 return $value; 작성 |
| 참조 인자를 복사로 받음 |
data(참조)는 &$args['data']로 받아야 수정 반영됨 |
| 훅 이름 오타 |
dx_has_hook()으로 훅 이름 존재 여부 먼저 확인 |
| 훅 등록 위치가 늦음 |
플러그인 파일 최상단에서 등록해야 실행 전에 예약됨 |
| priority 충돌 |
코어 훅은 1~99, 플러그인은 100+, 디버그는 999로 규칙 통일 |
10. 훅 개발 가이드라인
10.1 priority 사용 규칙 권장
| 범위 |
용도 |
| 1 ~ 9 |
최우선 실행 필요 (보안 체크, 인증 확인, 필수 데이터 주입) |
| 10 (기본) |
일반 플러그인 로직 (기본값 유지 권장) |
| 11 ~ 99 |
코어에 의존하는 로직 (코어 콜백 실행 후 처리) |
| 100 ~ 998 |
서드파티 플러그인 간 충돌 방지용 범위 |
| 999 |
디버그, 로깅, 정리 작업 (가장 마지막에 실행) |
10.2 안전한 훅 콜백 작성
dx_add_hook('dx_after_write', function($args) {
// ① 필수 인자 확인
if (empty($args['post_id'])) return;
// ② try-catch로 에러 격리 (훅 실패가 CMS 전체를 멈추면 안 됨)
try {
$postId = (int)$args['post_id'];
// 처리 로직...
} catch (Exception $e) {
dx_log('[my-plugin][dx_after_write] ' . $e->getMessage(), 'error');
}
});
10.3 훅 이름 네이밍 규칙
- DXCMS 내장 훅: dx_ 접두사 (예: dx_after_login)
- 플러그인 자체 훅: {plugin-name}_ 접두사 (예: tosspay_after_payment)
- 테마 훅: theme_ 접두사 (예: theme_header_end)
- 충돌 방지를 위해 고유한 접두사를 반드시 사용하세요.
최종 정리
Hook은 CMS 코드를 수정하지 않고 동작을 확장하는 핵심 메커니즘입니다.
Action 훅(dx_run_hook)은 실행, Filter 훅(dx_apply_filter)은 값 변형에 사용합니다.
priority로 실행 순서를 제어하며, 낮은 숫자가 먼저 실행됩니다.
모든 콜백은 try-catch로 에러를 격리하여 CMS 안정성을 보장하세요.
커스텀 훅 포인트를 정의하면 내 플러그인도 다른 플러그인이 확장할 수 있습니다.