1. Extend 구조란 무엇인가?
DXCMS v8.1.0은 "코어 파일을 절대 수정하지 않아도 된다"는 철학을 중심으로 설계된 확장 아키텍처를 제공합니다. 이 구조를 Extend 구조라고 부르며, 크게 세 가지 레이어로 구성됩니다.
1.1 왜 코어를 수정하면 안 되는가?
많은 개발자가 기능 추가를 위해 CMS 코어 파일을 직접 수정하는 유혹에 빠집니다. 그러나 이 방식은 다음과 같은 치명적인 문제를 낳습니다.
- 업데이트 불가: 코어 파일을 수정하면 DXCMS 새 버전이 나와도 덮어쓸 수 없게 됩니다.
- 충돌 위험: 여러 개발자가 같은 파일을 수정하면 병합 충돌이 발생합니다.
- 추적 불가: 어떤 기능이 어디서 추가됐는지 파악하기 어려워집니다.
- 롤백 불가: 문제 발생 시 원래 상태로 되돌리는 것이 매우 어렵습니다.
- 보안 위험: 핵심 보안 로직이 의도치 않게 변경될 수 있습니다.
✅ DXCMS의 원칙
"코어는 건드리지 않는다. 모든 커스터마이징은 extend/, plugins/, 훅을 통해서만 한다."
이 원칙을 지키면 CMS를 업데이트해도 커스텀 기능이 그대로 유지됩니다.
1.2 Extend 3계층 구조
DXCMS의 확장 메커니즘은 사용 난이도와 목적에 따라 3계층으로 나뉩니다.| 계층 | 수단 | 위치 | 대상 | 특징 |
|---|---|---|---|---|
| 1계층 (가장 쉬움) | extend/ 폴더 | extend/top•middle•bottom/ | 누구나 | 파일을 폴더에 넣으면 자동 실행 |
| 2계층 (중간) | Hook 시스템 | plugins/ 또는 extend/ | 개발자 | 특정 시점에 콜백 등록•실행 |
| 3계층 (심화) | Plugin 시스템 | plugins/{plugin-name}/ | 플러그인 개발자 | 에디터•결제 등 교체 가능한 기능 모듈 |
1.3 실행 흐름 다이어그램
DXCMS 요청 처리의 전체 흐름에서 Extend 구조가 어느 시점에 실행되는지 확인하세요.
[ 브라우저 요청 ] → index.php
│
├─ ① CMS 초기화 (DB • 세션 • 인증 • DxSite • DxTheme • 플러그인 로드)
│
├─ ② extend/top/ 실행 ← 점검모드, IP 차단, 전역 변수 주입 등
│ └─ dx_extend_top 훅도 여기서 발화
│
├─ ③ Router: 라우트 확정 ($GLOBALS['dx_route'] 세팅)
│
├─ ④ extend/middle/ 실행 ← 접근 로그, A/B 테스트, 라우트별 처리
│ └─ dx_extend_middle 훅도 여기서 발화
│
├─ ⑤ Dispatcher: 핸들러 실행 (페이지 렌더링)
│
├─ ⑥ ob_end_flush() 직전
│
└─ ⑦ extend/bottom/ 실행 ← 캐시 저장, 성능 로그, 정리 작업
└─ dx_extend_bottom 훅도 여기서 발화
[ 브라우저 응답 전송 ]
2. extend/ 폴더 자동 실행 시스템 (DxExtend)
extend/ 폴더 시스템은 "파일만 넣으면 실행되는" 가장 단순한 확장 방법입니다. DxExtend 엔진이 지정된 폴더의 PHP 파일을 파일명 오름차순으로 자동 실행합니다.
2.1 폴더 구조
extend/
├── top/
│ ├── 01_maintenance.php ← 점검 모드
│ ├── 02_ip_block.php ← IP 차단
│ └── 03_global_vars.php ← 전역 변수 주입
│
├── middle/
│ ├── 01_visit_tracker.php ← 방문자 통계 (내장)
│ ├── 02_analytics.php ← GA 연동 등
│ └── 03_ab_test.php ← A/B 테스트
│
└── bottom/
├── 01_cache_write.php ← 페이지 캐시 저장
├── 02_darkmode_engine.php ← 다크모드 JS 주입 (내장)
└── 03_perf_log.php ← 성능 로그
📌 파일 실행 규칙
• *.php 파일만 실행됩니다 (다른 확장자는 무시)
• 파일명 오름차순으로 실행됩니다 (01_ → 02_ → ... 순서)
• 하위 폴더의 파일도 자동 탐색합니다 (1단계 재귀)
• 한 파일에서 에러가 나도 다른 파일 실행에 영향 없음 (에러 격리)
• .disabled 확장자를 붙이면 비활성화됩니다 (PHP 인식 불가)
2.2 각 슬롯의 실행 시점과 용도
2.2.1 extend/top/ — CMS 초기화 완료 직후
| 항목 | 내용 |
|---|---|
| 실행 시점 | DB 연결, 세션 시작, 인증, DxSite, DxTheme, 플러그인 로드가 모두 완료된 직후 |
| 가용 자원 | DB 쿼리, 세션, Auth, dx_config(), dx_log(), 모든 CMS 함수 사용 가능 |
| 주의 사항 | 라우트 정보 ($GLOBALS['dx_route'])는 아직 확정되지 않음 |
| 권장 용도 | 점검 모드, IP 차단, 커스텀 인증, 전역 PHP 변수 주입, ob_start 훅 등록 |
2.2.2 extend/middle/ — 라우트 확정 직후
| 항목 | 내용 |
|---|---|
| 실행 시점 | Router가 요청 URL을 분석하여 라우트를 확정한 직후, 핸들러 실행 전 |
| 가용 자원 | top/의 모든 자원 + $GLOBALS['dx_route'] (현재 라우트 정보) |
| 주의 사항 | 핸들러 실행 전이므로 페이지 출력을 가로챌 수 있음 |
| 권장 용도 | 방문자 로그, 방문 통계, 접근 제어, A/B 테스트, 라우트별 분기 처리 |
$GLOBALS['dx_route'] 구조 예시:
$GLOBALS['dx_route'] = array(
'type' => 'board', // 라우트 타입 (page, board, admin, api 등)
'slug' => 'notice', // 페이지/게시판 슬러그
'action' => 'list', // 동작 (list, view, write, edit 등)
'id' => 123, // 게시글 ID (있는 경우)
);
2.2.3 extend/bottom/ — 렌더링 완료 후
| 항목 | 내용 |
|---|---|
| 실행 시점 | ob_end_flush() 직전 (출력 버퍼 플러시 전) |
| 가용 자원 | 모든 CMS 자원, 단 헤더 변경 불가 (이미 출력 시작) |
| 주의 사항 | ob_start가 활성인 경우 추가 출력 가능, 그렇지 않으면 헤더 불가 |
| 권장 용도 | 페이지 캐시 저장, 성능 측정 출력, 임시 파일 정리, JS 인젝션 |
2.3 파일 작성 방법
extend/ 파일 작성 시 반드시 지켜야 할 규칙이 있습니다.
필수 보안 헤더
<?php
// 직접 접근 차단 (필수)
if (!defined('DX_CMS')) exit;
// 이후에 원하는 코드 작성
⚠️ 경고: if (!defined('DX_CMS')) exit; 를 파일 첫 줄에 반드시 작성하세요.
이 한 줄이 없으면 외부에서 파일을 직접 호출하여 보안 위협이 발생할 수 있습니다.
파일명 네이밍 규칙
| 패턴 | 예시 | 설명 |
|---|---|---|
| NN_설명.php | 01_maintenance.php | 숫자 2자리 + 언더스코어 + 설명 |
| 비활성화 | 99_example.php.disabled | .disabled 추가 시 PHP가 인식 불가 |
| 그룹화 | security/01_ip_block.php | 하위 폴더로 논리 그룹화 가능 |
3. extend/ 실전 예제
3.1 점검 모드 구현 (top/)
사이트 점검 시 관리자를 제외한 모든 사용자에게 점검 안내 페이지를 보여줍니다.파일: extend/top/01_maintenance.php
<?php
if (!defined('DX_CMS')) exit;
// ── 점검 모드 설정 ──────────────────────────
$maintenance = array(
'enabled' => true, // true: 점검 ON
'message' => '서비스 점검 중입니다.', // 안내 메시지
'end_time' => '2026-05-01 03:00', // 점검 종료 예정 시각
'allow_ip' => array('127.0.0.1'), // 허용 IP 목록
);
if (!$maintenance['enabled']) return; // 점검 OFF면 즉시 종료
// 관리자는 통과
if (function_exists('dx_is_admin') && dx_is_admin()) return;
// 허용 IP는 통과
$clientIp = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
if (in_array($clientIp, $maintenance['allow_ip'], true)) return;
// 점검 페이지 출력
http_response_code(503);
header('Retry-After: 3600');
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>서비스 점검중</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 80px; }
h1 { color: #1E3A5F; }
.time { color: #2563EB; font-weight: bold; }
</style>
</head>
<body>
<h1>🔧 서비스 점검중</h1>
<p>더 나은 서비스를 위해 잠시 점검 중입니다.</p>
<p class="time">점검 종료 예정: <?php echo $maintenance['end_time']; ?></p>
</body>
</html>
<?php exit;
3.2 IP 차단 (top/)
특정 IP 또는 IP 대역을 차단합니다.파일: extend/top/02_ip_block.php
<?php
if (!defined('DX_CMS')) exit;
$blockedIps = array(
'192.168.1.100', // 특정 IP
'10.0.0.', // 이 문자열로 시작하는 모든 IP
'203.0.113.', // 예: 특정 기관 IP 대역
);
$clientIp = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
foreach ($blockedIps as $blocked) {
if (strpos($clientIp, $blocked) === 0) {
http_response_code(403);
dx_log('[IP Block] 차단: ' . $clientIp, 'warning');
exit('403 Forbidden');
}
}
3.3 전역 변수 주입 (top/)
모든 페이지에서 공통으로 필요한 설정값이나 변수를 전역으로 주입합니다.파일: extend/top/03_global_vars.php
<?php
if (!defined('DX_CMS')) exit;
// 전역 변수 정의
define('MY_APP_VERSION', '2.0.0');
define('MY_CDN_URL', 'https://cdn.example.com');
// PHP $_GLOBALS에 배열로 주입 (테마/플러그인에서 접근 가능)
$GLOBALS['my_app'] = array(
'version' => MY_APP_VERSION,
'cdn' => MY_CDN_URL,
'start_time' => microtime(true),
);
// 세션 기반 사용자 커스텀 데이터 초기화
if (!isset($_SESSION['my_prefs'])) {
$_SESSION['my_prefs'] = array(
'lang' => 'ko',
'theme' => 'light',
);
}
3.4 방문자 통계 로깅 (middle/)
라우트 확정 후 방문자 정보를 기록합니다. DXCMS 내장 extend/middle/01_visit_tracker.php는 이미 고성능으로 구현되어 있으며, 아래는 간단한 커스텀 접근 로그 예제입니다.파일: extend/middle/02_custom_access_log.php
<?php
if (!defined('DX_CMS')) exit;
// 라우트 정보 가져오기 (middle에서만 사용 가능)
$route = isset($GLOBALS['dx_route']) ? $GLOBALS['dx_route'] : array();
$type = isset($route['type']) ? $route['type'] : 'unknown';
// admin, api 요청은 로그에서 제외
if (in_array($type, array('admin', 'api'), true)) return;
// 봇 제외
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
if (preg_match('/bot|crawl|spider/i', $ua)) return;
// 로그 기록 (응답 후 처리로 성능 영향 최소화)
register_shutdown_function(function() use ($route, $ua) {
$log = date('Y-m-d H:i:s')
. ' | ' . dx_ip()
. ' | ' . json_encode($route)
. ' | ' . substr($ua, 0, 100);
dx_log($log, 'info');
});
3.5 HTML 버퍼 조작으로 JS 주입 (top/ + bottom/ 조합)
다크모드 FOUC 방지와 같이 최종 HTML에 스크립트를 삽입해야 하는 경우, ob_start를 top/에서 등록하고 bottom/에서 JS를 출력합니다.파일: extend/top/01_darkmode_early.php (내장 예제 요약)
<?php
if (!defined('DX_CMS')) exit;
// ob_start 콜백으로 최종 HTML 버퍼를 가로챔
ob_start(function($buffer) {
// <head> 바로 뒤에 인라인 스크립트 삽입
$script = '<script>
(function(){
var t = localStorage.getItem("theme");
if (t === "dark") document.body.classList.add("dark");
})();
</script>';
$buffer = preg_replace('/<head([^>]*)>/i', '<head$1>' . $script, $buffer, 1);
return $buffer;
});
4. Hook 시스템 (HookManager)
Hook 시스템은 "특정 시점에 콜백 함수를 등록하고 실행"하는 방식입니다. extend/ 폴더보다 더 세밀하게 실행 시점과 우선순위를 제어할 수 있습니다.
4.1 Hook의 종류
| 종류 | 함수 | 역할 | 반환값 |
|---|---|---|---|
| Action Hook | dx_add_hook() / dx_run_hook() | 특정 시점에 코드 실행 | 없음 (side effect) |
| Filter Hook | dx_add_filter() / dx_apply_filter() | 값을 가공하여 반환 | 가공된 값 |
4.2 전역 헬퍼 함수
| 함수 | 매개변수 | 설명 |
|---|---|---|
| dx_add_hook($name, $callback, $priority) | name: 훅 이름 / callback: 실행함수 / priority: 우선순위(기본 10) | Action 훅 등록. 낮은 priority가 먼저 실행 |
| dx_run_hook($name, $args) | name: 훅 이름 / args: 전달 인자 배열 | 등록된 Action 훅 실행 |
| dx_add_filter($name, $callback, $priority) | 위와 동일 | Filter 훅 등록 (내부적으로 add와 동일) |
| dx_apply_filter($name, $value, $args) | name: 훅 이름 / value: 원본값 / args: 추가 인자 | 등록된 Filter 실행 후 최종값 반환 |
| dx_remove_hook($name, $callback) | name: 훅 이름 / callback: 제거할 콜백(null=전체) | 등록된 훅 제거 |
| dx_has_hook($name) | name: 훅 이름 | 해당 훅이 등록되어 있는지 확인 |
4.3 내장 훅 포인트
DXCMS는 페이지 렌더링 시 자동으로 다음 훅을 발화합니다.
4.3.1 전역 훅 (모든 페이지)
| 훅 이름 | 발화 시점 | 주요 용도 |
|---|---|---|
| dx_top | 모든 페이지 상단 | 공통 헤더 요소 삽입, 스크립트 주입 |
| dx_middle | 모든 페이지 중간 | 콘텐츠 영역 내 삽입 |
| dx_bottom | 모든 페이지 하단 </body> 직전 | JS 로드, 분석 코드, 채팅 위젯 등 |
4.3.2 타입별 훅 (자동 생성)
현재 라우트 타입이 board라면 dx_board_top, dx_board_middle, dx_board_bottom 훅이 자동으로 발화됩니다.
// 라우트 타입: 'board', 'page', 'admin', 'api' 등
dx_add_hook('dx_board_top', function($context) {
// 게시판 페이지 상단에만 실행되는 코드
echo '<div class="board-notice">게시판 공지</div>';
});
dx_add_hook('dx_page_top', function($context) {
// 일반 페이지(type=page) 상단에만 실행
});
4.3.3 슬러그별 훅 (자동 생성)
라우트 slug가 notice라면 dx_page_notice_top 등 슬러그 전용 훅이 발화됩니다.
// slug='notice'인 페이지 상단에만 실행
dx_add_hook('dx_page_notice_top', function($context) {
echo '<p>공지사항 전용 배너</p>';
});
4.3.4 extend/ 슬롯 훅
| 훅 이름 | 발화 위치 |
|---|---|
| dx_extend_top | extend/top/ 파일 실행 완료 직후 |
| dx_extend_middle | extend/middle/ 파일 실행 완료 직후 |
| dx_extend_bottom | extend/bottom/ 파일 실행 완료 직후 |
4.4 우선순위(Priority) 제어
여러 훅 콜백이 동일한 훅에 등록된 경우, priority 값이 낮을수록 먼저 실행됩니다.
// 1번 먼저 실행 (priority 1)
dx_add_hook('dx_bottom', function() {
echo '<!-- 첫 번째 -->';
}, 1);
// 2번 나중에 실행 (priority 10, 기본값)
dx_add_hook('dx_bottom', function() {
echo '<!-- 두 번째 -->';
}, 10);
// 3번 맨 마지막 실행 (priority 999)
dx_add_hook('dx_bottom', function() {
echo '<!-- 맨 마지막 -->';
}, 999);
4.5 Filter Hook 실용 예제
Filter 훅은 값을 받아서 변환한 후 반환합니다. Action 훅과 달리 반환값이 중요합니다.
// 게시글 내용 필터: 특정 단어 마스킹
dx_add_filter('dx_post_content', function($content, $args) {
// 금칙어 필터링
$content = str_replace('금칙어1', '***', $content);
$content = str_replace('금칙어2', '***', $content);
return $content; // 반드시 반환!
}, 10);
// 코어에서 필터 적용 (코어가 이미 이렇게 구현되어 있음)
$content = dx_apply_filter('dx_post_content', $rawContent, array('post_id' => $id));
5. Plugin 시스템 (PluginRegistry)
플러그인 시스템은 에디터, 결제 모듈, CAPTCHA, SMS 등 "교체 가능한 기능 모듈"을 위한 고급 확장 방법입니다. PluginRegistry에 등록된 플러그인은 관리자 페이지에서 선택하고 활성화할 수 있습니다.
5.1 지원 플러그인 타입
| 타입 | 키워드 | 설명 | 설정 키 |
|---|---|---|---|
| 에디터 | editor | 게시글 작성 에디터 (TinyMCE, CKEditor 등) | active_editor |
| 결제 | payment | PG사 결제 모듈 (KG이니시스, 토스 등) | active_payment |
| CAPTCHA | captcha | 스팸 방지 (reCAPTCHA, hCaptcha 등) | active_captcha |
| SMS | sms | 문자 발송 (NCP, 알리고 등) | active_sms |
| 소셜 로그인 | social_login | 카카오, 네이버, 구글 로그인 | active_social_login |
| 소켓 | socket | WebSocket 실시간 기능 | active_socket |
| 커스텀 | 자유 지정 | 개발자가 임의 타입 추가 가능 | active_{타입} |
5.2 플러그인 폴더 구조
plugins/
├── my-plugin/ ← 플러그인 폴더명 = 플러그인 ID
│ ├── plugin.php ← 플러그인 메인 파일 (훅 등록 등)
│ ├── manifest.php ← 플러그인 메타 정보
│ └── admin/ ← 관리자 설정 페이지 (선택)
│ └── settings.php
│
├── example-plugin/
│ ├── plugin.php
│ └── manifest.php
│
└── dx-payment-helper.php ← 공용 결제 헬퍼 (내장)
5.3 플러그인 개발 3단계
Step 1: manifest.php 작성
manifest.php는 플러그인의 메타 정보를 담습니다. 마켓 등록 시 이 파일이 사용됩니다.
<?php
// plugins/my-editor/manifest.php
return array(
'name' => 'My Custom Editor',
'version' => '1.0.0',
'description' => '커스텀 마크다운 에디터입니다.',
'author' => '홍길동',
'author_url' => 'https://example.com',
'type' => 'editor', // 플러그인 타입
'min_version' => '8.0.0', // 최소 CMS 버전
'tags' => '에디터,마크다운',
'license' => 'MIT',
);
Step 2: plugin.php 작성
plugin.php는 플러그인의 핵심 파일입니다. 플러그인을 PluginRegistry에 등록하고, 훅을 통해 동작을 구현합니다.
<?php
if (!defined('DX_CMS')) exit('Direct access not allowed.');
// ── 1. 플러그인 등록 (PluginRegistry에 등록) ─────
dx_register_plugin(array(
'id' => 'my-editor',
'type' => 'editor',
'name' => 'My Custom Editor',
'version' => '1.0.0',
'description' => '커스텀 마크다운 에디터',
'author' => '홍길동',
'priority' => 20, // 낮을수록 목록 상단
'settings' => array( // 플러그인 전용 설정 필드
'theme' => array('label' => '테마', 'type' => 'select',
'options' => array('light'=>'라이트', 'dark'=>'다크')),
'height' => array('label' => '에디터 높이(px)', 'type' => 'number'),
),
));
// ── 2. 에디터 렌더링 훅 구현 ──────────────────────
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');
echo '<div class="my-editor-wrap">';
echo '<textarea id="my-editor-' . $name . '" name="' . $name . '">'
. $value . '</textarea>';
echo '<script src="/plugins/my-editor/editor.js"></script>';
echo '</div>';
}, 10);
Step 3: 활성 플러그인 확인 및 호출
테마나 다른 코드에서 현재 활성화된 플러그인을 확인하고 사용합니다.
// 현재 활성 에디터 ID 확인
$editorId = dx_active_plugin('editor'); // 예: 'my-editor'
// 에디터 렌더링 (모든 에디터 공통 인터페이스)
dx_render_editor('content', $existingContent, array(
'height' => 400,
'board' => $board, // 게시판별 에디터 오버라이드 가능
));
// 결제 요청
dx_request_payment(array(
'order_id' => 'ORD-2026-001',
'amount' => 29000,
'product_name' => 'DXCMS Pro',
'buyer_name' => '홍길동',
'buyer_email' => 'hong@example.com',
'return_url' => 'https://example.com/payment/result',
));
6. Hook 활용 실전 패턴
6.1 디버그 패널 삽입 (dx_bottom)
DX_DEBUG 모드에서 실행 시간과 쿼리 수를 페이지 우하단에 표시합니다.
// extend/top/01_debug_panel.php 또는 plugin.php 내에서
dx_add_hook('dx_bottom', function($context) {
if (!defined('DX_DEBUG') || !DX_DEBUG) return;
$time = round((microtime(true) - DX_START_TIME) * 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(0,0,0,.85);color:#fff;
padding:8px 14px;border-radius:8px;font-size:11px;
font-family:monospace;z-index:9999;line-height:1.8">';
echo '⚡ ' . $time . 'ms | 🗄 ' . $queries . ' queries';
echo '<br>📍 ' . htmlspecialchars($route, ENT_QUOTES);
echo '</div>';
}, 999);
6.2 특정 페이지에만 CSS/JS 삽입
특정 슬러그의 페이지에만 에셋을 추가합니다.
// 'gallery' 슬러그 페이지 상단에만 CSS 삽입
dx_add_hook('dx_page_gallery_top', function($context) {
echo '<link rel="stylesheet" href="/assets/css/gallery-custom.css">';
});
// 'gallery' 슬러그 페이지 하단에만 JS 삽입
dx_add_hook('dx_page_gallery_bottom', function($context) {
echo '<script src="/assets/js/gallery-lightbox.js"></script>';
});
6.3 관리자 전용 사이드바 메뉴 추가
dx_add_hook('dx_admin_sidebar', function($context) {
echo '<li class="nav-item">';
echo ' <a href="/admin/my-module" class="nav-link">';
echo ' <i class="icon">📊</i> 내 모듈';
echo ' </a>';
echo '</li>';
});
6.4 훅 우선순위를 이용한 실행 순서 보장
// ① 가장 먼저: 보안 체크 (priority 1)
dx_add_hook('dx_top', 'my_security_check', 1);
// ② 다음: 사용자 권한 확인 (priority 5)
dx_add_hook('dx_top', 'my_permission_check', 5);
// ③ 기본: 일반 처리 (priority 10, 기본값)
dx_add_hook('dx_top', 'my_normal_process');
// ④ 맨 마지막: 로깅 (priority 999)
dx_add_hook('dx_top', 'my_access_log', 999);
function my_security_check($ctx) { /* ... */ }
function my_permission_check($ctx) { /* ... */ }
function my_normal_process($ctx) { /* ... */ }
function my_access_log($ctx) { /* ... */ }
7. extend/ vs Hook vs Plugin 선택 가이드
세 가지 확장 방법 중 어떤 것을 선택해야 하는지 기준을 정리합니다.| 비교 항목 | extend/ 폴더 | Hook 시스템 | Plugin 시스템 |
|---|---|---|---|
| Plugin 시스템 | ⭐ (쉬움) | ⭐⭐ (중간) | ⭐⭐⭐ (어려움) |
| PHP 코드 필요 | 최소 (파일 생성만) | 필요 (콜백 함수) | 필요 (등록+구현) |
| 실행 시점 제어 | 슬롯 3개로 제한 | 훅 이름으로 정밀 제어 | 훅 기반으로 구현 |
| 우선순위 제어 | 파일명으로만 제어 | priority 매개변수로 제어 | priority 매개변수 지원 |
| 에러 격리 | 자동 (파일 단위) | 수동 try-catch 필요 | 수동 try-catch 필요 |
| 관리자 UI | 없음 | 없음 | 관리자 설정 화면 자동 생성 |
| 교체 가능성 | 없음 | 없음 | 동일 타입 중 선택/전환 가능 |
| 권장 사용 사례 | 점검모드, IP차단, 로그 | 콘텐츠 삽입, 값 필터링 | 에디터, 결제, SMS 교체 |
7.1 의사결정 트리
커스터마이징이 필요한가?
│
├─ 특정 시점에 코드만 실행하면 되는가?
│ ├─ YES, 복잡한 제어 불필요 → extend/ 폴더 사용
│ └─ YES, 실행 시점/순서를 정밀 제어해야 함 → Hook 사용
│
├─ 에디터/결제/SMS 같은 기능을 교체/추가하려는가?
│ └─ YES → Plugin 시스템 사용
│
└─ 코어 파일을 수정하고 싶은 유혹이 드는가?
└─ 절대 NO! → 위 세 가지 중 하나로 반드시 해결 가능
7.2 혼합 사용 예제
실제 운영 환경에서는 세 가지를 혼합하여 사용합니다.
// plugins/my-plugin/plugin.php
// ① Plugin Registry에 등록 (Plugin 시스템)
dx_register_plugin(array(
'id' => 'my-plugin', 'type' => 'editor', 'name' => 'My Plugin'
));
// ② Hook으로 전역 동작 추가 (Hook 시스템)
dx_add_hook('dx_bottom', function() {
echo '<script src="/plugins/my-plugin/main.js"></script>';
}, 10);
// ③ extend/top/에 전역 변수 주입 (extend/ 폴더)
// → extend/top/01_my_plugin_vars.php 파일로 분리
8. 보안 및 주의사항
8.1 DxExtend의 보안 메커니즘
DxExtend 엔진은 다음과 같은 보안 장치를 내장하고 있습니다.
🔒 DxExtend 내장 보안 장치
1. realpath() 검증: 심볼릭 링크 등 경로 조작을 통해 extend/ 외부 파일을 실행하는 것을 차단합니다.
2. 에러 격리: set_error_handler()로 각 파일의 에러를 가로채어 로그에만 기록하고 다음 파일을 계속 실행합니다.
3. 직접 접근 차단: DX_CMS 상수 미정의 시 즉시 종료됩니다.
4. EXTR_SKIP: extract($context, EXTR_SKIP)으로 기존 변수를 덮어쓰지 않습니다.
8.2 개발 시 주의사항
⚠️ 주의 1: extend/ 파일에서 exit/die를 무분별하게 호출하면 안 됩니다.
점검 모드처럼 의도적인 경우는 OK이지만, 일반 로직에서 exit하면 이후 파일이 실행되지 않습니다.
⚠️ 주의 2: Filter 훅에서 반드시 값을 return 해야 합니다.
dx_apply_filter()를 통해 값이 전달되는데, 콜백에서 return을 빠뜨리면 null이 반환되어
데이터가 사라집니다.
⚠️ 주의 3: bottom/ 슬롯에서 헤더를 변경할 수 없습니다.
bottom은 렌더링 완료 후 실행되므로 header() 함수 호출이 무시됩니다.
헤더 변경이 필요하면 top/ 슬롯을 사용하세요.
⚠️ 주의 4: DB 집중 작업은 register_shutdown_function()으로 처리하세요.
방문자 로그, 통계 집계 등은 응답 후 처리해야 사용자 체감 속도가 향상됩니다.
내장 visit_tracker는 이 패턴을 사용합니다.
8.3 파일 권한 설정
| 대상 | 권한 | 설명 |
|---|---|---|
| extend/ 폴더 | 755 | 읽기/실행 가능, 웹서버 쓰기 불필요 |
| extend/*.php | 644 | 웹서버 실행 가능, 타인 쓰기 불가 |
| plugins/ 폴더 | 755 | 읽기/실행 가능 |
| plugins/**/plugin.php | 644 | 안전한 권한 |
9. 빠른 참조 (Quick Reference)
9.1 가장 자주 쓰는 패턴
// ① 모든 페이지 하단에 스크립트 삽입
dx_add_hook('dx_bottom', function() {
echo '<script src="/my-script.js"></script>';
}, 10);
// ② 점검 모드 (extend/top/01_maint.php)
if (!dx_is_admin()) { http_response_code(503); exit('점검중'); }
// ③ 현재 라우트 타입 확인 (middle/)
$type = isset($GLOBALS['dx_route']['type']) ? $GLOBALS['dx_route']['type'] : '';
// ④ 에디터 렌더링
dx_render_editor('content', $value, array('height' => 400));
// ⑤ 현재 활성 결제 플러그인 ID
$paymentId = dx_active_plugin('payment');
// ⑥ 훅 등록 확인 후 실행
if (dx_has_hook('my_custom_hook')) {
dx_run_hook('my_custom_hook', array('key' => 'value'));
}
// ⑦ 훅 제거
dx_remove_hook('dx_bottom', 'my_callback_function');
9.2 내장 전역 함수 참조
| 함수 | 설명 |
|---|---|
| dx_is_admin() | 현재 사용자가 관리자인지 확인 |
| dx_is_logged_in() | 로그인 여부 확인 |
| dx_ip() | 클라이언트 IP 반환 (Cloudflare 우선) |
| dx_config($key, $default) | CMS 설정값 조회 |
| dx_log($message, $level) | 로그 기록 (info/warning/error) |
| dx_base_url($path) | 기본 URL + 경로 조합 |
| dx_request_uri() | 현재 요청 URI 반환 |
| dx_error($message, $code) | 에러 페이지 출력 후 종료 |
9.3 체크리스트: extend 파일 작성 시
2. 파일명은 숫자 접두사로 실행 순서 지정 (예: 01_my_feature.php)
3. 에러가 발생해도 안전하게 처리 (try-catch 또는 @ 연산자)
4. 불필요한 전역 변수는 unset()으로 정리
5. DB 작업은 register_shutdown_function()으로 응답 후 처리
6. 비활성화 시 .disabled 확장자 추가 (삭제보다 안전)
7. 테스트: DX_DEBUG 모드에서 에러 로그 확인