1. 재사용 방식 전체 개요
DXCMS는 "코어를 절대 수정하지 않고" 기능을 추가•교체•재사용하는 6가지 메커니즘을 제공합니다. 각 메커니즘은 목적이 다르며 상황에 따라 단독 또는 조합하여 사용합니다.
1.1 재사용 메커니즘 전체 지도
| # | 메커니즘 | 핵심 API | 난이도 | 주 사용 목적 |
| ① | Hook (훅) | dx_add_hook / dx_run_hook | ⭐⭐ | 특정 시점에 코드 삽입·값 변환 |
| ② | Plugin (플러그인) | dx_register_plugin / dx_render_editor | ⭐⭐⭐ | 에디터·결제·SMS 등 교체 가능 모듈 |
| ③ | DI Container | dx_app()->singleton / dx_make | ⭐⭐⭐ | 서비스 객체 등록·주입·교체 |
| ④ | Extend 폴더 | extend/top·middle·bottom/*.php | ⭐ | 파일만 넣으면 자동 실행 |
| ⑤ | Theme 파셜 | dx_include_part / dx_theme_file | ⭐⭐ | 테마 부분 템플릿 재사용 |
| ⑥ | Cache 레이어 | DxCache::set / DxCache::get | ⭐⭐ | 연산 결과 재사용(성능 최적화) |
1.2 선택 기준 요약
무엇을 하려는가?
│
├─ 특정 시점에 HTML•JS를 끼워 넣고 싶다 → ① Hook (dx_add_hook)
│
├─ 에디터•결제 등 통째로 교체하고 싶다 → ② Plugin (dx_register_plugin)
│
├─ 여러 곳에서 동일 객체(API 클라이언트 등)를 → ③ DI Container (dx_singleton)
│ 공유하고 싶다
│
├─ PHP 파일을 폴더에 넣기만 해서 자동 실행 원한다 → ④ Extend 폴더
│
├─ 테마의 반복 HTML 조각을 분리하고 싶다 → ⑤ Theme 파셜 (dx_include_part)
│
└─ 무거운 DB 쿼리•계산 결과를 재사용 원한다 → ⑥ Cache 레이어 (DxCache)
2. ① Hook — 시점 기반 코드 재사용
훅(Hook)은 CMS가 미리 지정해 둔 시점(포인트)에 콜백 함수를 등록하고, 그 시점이 되면 등록된 콜백들을 우선순위 순으로 실행하는 메커니즘입니다. 코어 파일을 수정하지 않고 어떤 시점에도 코드를 삽입하거나 값을 변환할 수 있습니다.
2.1 Action 훅 — 코드 실행
반환값이 없고 부수 효과(출력, DB 저장, 이메일 발송 등)를 목적으로 합니다.| Action Hook | dx_add_hook() 등록 → dx_run_hook() 발화 → 등록된 콜백 우선순위 순 실행 |
// ① 등록 (plugins/my-plugin/plugin.php 또는 extend/top/*.php)
dx_add_hook('dx_bottom', function($context) {
echo '<script src="/my.js"></script>';
}, 10); // priority=10 (기본값)
// ② 발화 (테마 layout/main.php — 코어가 자동 호출)
dx_hook_bottom($context);
// 내부: dx_run_hook('dx_bottom', $context)
// dx_run_hook('dx_board_bottom', $context) // type=board인 경우
// dx_run_hook('dx_page_notice_bottom', $context) // slug=notice인 경우
2.2 Filter 훅 — 값 변환
값을 받아 가공한 뒤 반환합니다. 반드시 return을 써야 합니다. 여러 콜백이 체인처럼 연결되어 순서대로 값을 변환합니다.| Filter Hook | dx_add_filter() 등록 → dx_apply_filter() 발화 → 콜백 체인으로 값 변환 후 반환 |
// ① 필터 등록 (원본값을 받아 가공 후 반환)
dx_add_filter('dx_post_content', function($content, $args) {
// 금칙어 마스킹
$content = str_replace('금칙어', '***', $content);
return $content; // 반드시 return!
}, 10);
// ② 두 번째 필터 (체인: 앞 필터의 결과를 받아 다시 가공)
dx_add_filter('dx_post_content', function($content, $args) {
// URL 자동 링크화
$content = preg_replace('/(https?://[^s]+)/', '<a href="$1">$1</a>', $content);
return $content;
}, 20);
// ③ 발화 (코어 또는 스킨 파일에서)
$content = dx_apply_filter('dx_post_content', $rawContent, array('post_id'=>$id));
// 결과: 금칙어 마스킹 → URL 링크화 순으로 처리된 최종 content
2.3 우선순위(Priority) 제어
같은 훅 이름으로 여러 콜백이 등록되면 priority 값이 낮을수록 먼저 실행됩니다.
dx_add_hook('dx_top', 'security_check', 1); // 1번째 실행 (보안 체크)
dx_add_hook('dx_top', 'auth_check', 5); // 2번째 실행 (인증 확인)
dx_add_hook('dx_top', 'general_process', 10); // 3번째 실행 (일반 처리, 기본값)
dx_add_hook('dx_top', 'access_log', 999); // 맨 마지막 실행 (로그)
2.4 내장 훅 포인트 전체 목록
| 훅 이름 패턴 | 발화 함수 | 발화 시점 | 사용 예 |
| dx_top | dx_hook_top() | 모든 페이지 상단 | 공통 배너, 전역 JS 변수 주입 |
| dx_middle | dx_hook_middle() | 모든 페이지 중간 | 광고 배너, 중간 알림 |
| dx_bottom | dx_hook_bottom() | 모든 페이지 하단 </body> | Analytics, 채팅 위젯 JS |
| dx_{type}_top | dx_hook_top() | 특정 타입 페이지 상단 | 게시판 전용 배너 |
| dx_{type}_bottom | dx_hook_bottom() | 특정 타입 페이지 하단 | 게시판 전용 JS |
| dx_page_{slug}_top | dx_hook_top() | 특정 슬러그 페이지 상단 | notice 전용 배너 |
| dx_extend_top | DxExtend::runTop() | extend/top/ 완료 직후 | extend간 순서 제어 |
| dx_extend_middle | DxExtend::runMiddle() | extend/middle/ 완료 직후 | 라우트 기반 처리 |
| dx_extend_bottom | DxExtend::runBottom() | extend/bottom/ 완료 직후 | 정리 작업 |
| dx_editor_render | dx_render_editor() | 에디터 출력 시 | 에디터 HTML 생성 |
| dx_payment_request | dx_request_payment() | 결제 요청 시 | 결제창 호출 |
| dx_after_write | _dx_register_point_hooks | 게시글 작성 완료 | 포인트·경험치 지급 |
| dx_after_comment | _dx_register_point_hooks | 댓글 작성 완료 | 포인트·경험치 지급 |
| dx_after_login | _dx_register_point_hooks | 로그인 완료 | 출석 포인트 지급 |
2.5 훅 재사용 패턴 모음
패턴 A: 모든 페이지 하단에 JS 삽입
// plugins/my-analytics/plugin.php
dx_add_hook('dx_bottom', function($ctx) {
$trackId = dx_config('my_track_id', '');
if (!$trackId) return;
echo '<script async src="https://analytics.example.com/a.js?id=' . dx_esc($trackId) . '"></script>';
}, 50);
패턴 B: 특정 게시판에만 기능 삽입
// 공지사항 게시판 상단에만 배너 삽입
dx_add_hook('dx_board_top', function($ctx) {
if (!isset($ctx['slug']) || $ctx['slug'] !== 'notice') return;
echo '<div class="notice-banner">📢 공지사항 게시판입니다.</div>';
}, 10);
// slug 기반 훅 직접 사용 (더 간결)
dx_add_hook('dx_page_notice_top', function($ctx) {
echo '<div class="notice-banner">📢 공지사항 게시판입니다.</div>';
}, 10);
패턴 C: 게시글 내용 필터 체인
// 여러 플러그인이 동일 필터에 각자 기능을 추가
// 플러그인 A: 금칙어 필터 (priority 10)
dx_add_filter('dx_post_content', 'my_badword_filter', 10);
// 플러그인 B: URL 링크화 (priority 20)
dx_add_filter('dx_post_content', 'my_url_linker', 20);
// 플러그인 C: 이모지 처리 (priority 30)
dx_add_filter('dx_post_content', 'my_emoji_parser', 30);
// 스킨 파일에서 발화 — 위 세 필터가 순서대로 자동 적용
$content = dx_apply_filter('dx_post_content', $rawContent, array('post_id'=>42));
패턴 D: 훅으로 관리자 메뉴 확장
dx_add_hook('dx_admin_sidebar', function() {
echo '<li class="nav-item">';
echo ' <a href="/admin/my-module" class="nav-link">';
echo ' <i class="fa fa-chart-bar"></i> 내 모듈';
echo ' </a>';
echo '</li>';
}, 10);
패턴 E: 훅 제거로 기본 동작 비활성화
// 내장 기능의 훅을 제거하여 동작 끄기
// 예: 특정 플러그인의 JS 삽입을 선택적으로 비활성화
dx_remove_hook('dx_bottom', 'my_plugin_js_callback');
// 조건부 제거 (특정 경로에서만)
if (strpos(dx_request_uri(), '/admin') === 0) {
dx_remove_hook('dx_bottom', 'user_facing_widget');
}
3. ② Plugin — 교체 가능 기능 모듈 재사용
플러그인(Plugin)은 에디터, 결제, SMS 등 "통째로 교체 가능한 기능 모듈"을 위한 재사용 메커니즘입니다. 같은 타입의 플러그인이 여러 개 설치되어 있어도 관리자가 활성 플러그인을 선택하면 나머지는 자동으로 비활성화됩니다.
3.1 플러그인 등록•활성화•사용 전체 흐름
┌─────────────────────────────────────────────────────────────────┐
│ 플러그인 개발자 │
│ plugins/my-editor/plugin.php │
│ │
│ ① dx_register_plugin(...) → PluginRegistry에 등록 │
│ ② dx_add_hook('dx_editor_render', function...) → 훅 등록 │
└──────────────────────────────┬──────────────────────────────────┘
│ load_plugins() 호출 시 자동 실행
┌──────────────────────────────▼──────────────────────────────────┐
│ 관리자 │
│ /admin/settings → 에디터 설정에서 'My Editor' 선택 │
│ → dx_settings.active_editor = 'my-editor' DB 저장 │
└──────────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────────┐
│ 게시판 스킨 파일 │
│ ③ dx_render_editor('content', $value) 호출 │
│ → dx_active_plugin('editor') = 'my-editor' │
│ → dx_run_hook('dx_editor_render', args) │
│ → my-editor의 콜백 실행 → 에디터 HTML 출력 │
└─────────────────────────────────────────────────────────────────┘
3.2 플러그인 개발 완성 예제
Step 1: manifest.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',
'tags' => '에디터,마크다운',
'license' => 'MIT',
);
Step 2: plugin.php — 등록 및 훅 구현
// plugins/my-editor/plugin.php
if (!defined('DX_CMS')) exit;
// ① 플러그인 등록
dx_register_plugin(array(
'id' => 'my-editor',
'type' => 'editor',
'name' => 'My Custom Editor',
'version' => '1.0.0',
'description' => '마크다운 에디터',
'author' => '홍길동',
'priority' => 10,
'settings' => array(
'height' => array('label'=>'에디터 높이(px)','type'=>'number'),
'theme' => array('label'=>'테마','type'=>'select','options'=>array('light'=>'라이트','dark'=>'다크')),
),
));
// ② 에디터 렌더링 훅 구현
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');
$height = dx_config('plugin_my-editor_height', 400);
echo '<div class="my-editor-wrap">';
echo '<textarea id="my-ed-' . $name . '" name="' . $name . '" style="height:' . $height . 'px">'
. $value . '</textarea>';
echo '<script src="' . dx_base_url('plugins/my-editor/editor.js') . '"></script>';
echo '</div>';
}, 10);
// ③ 에디터 JS/CSS 모든 페이지 하단 로드 (글쓰기 페이지만)
dx_add_hook('dx_bottom', function($ctx) {
if (!isset($ctx['action']) || $ctx['action'] !== 'write') return;
echo '<link rel="stylesheet" href="' . dx_base_url('plugins/my-editor/editor.css') . '">';
}, 10);
Step 3: 스킨에서 사용
// themes/default/board/write.php (게시글 작성 스킨)
dx_render_editor('content', $post['content'] ?? '', array(
'height' => 500,
'board' => $board, // 게시판별 에디터 오버라이드 지원
));
// 내부 동작:
// 1. $board['editor']가 있으면 해당 에디터 ID 사용 (게시판별 오버라이드)
// 2. 없으면 dx_active_plugin('editor') = 'my-editor' 사용
// 3. dx_run_hook('dx_editor_render', args) 발화
// 4. my-editor의 콜백이 실행되어 에디터 HTML 출력
// 5. 훅 미등록이거나 에디터 미지정이면 textarea로 자동 폴백
3.3 플러그인 타입별 진입 훅
| 타입 | 진입 훅 | 발화 함수 | 구현해야 할 내용 |
| editor | dx_editor_render | dx_render_editor() | 에디터 HTML/JS 출력. $args['editor'] 확인 필수. |
| payment | dx_payment_request | dx_request_payment() | 결제창 HTML/JS 출력. 결제 완료 후 return_url로 리다이렉트. |
| captcha | dx_captcha_render | 직접 발화 | CAPTCHA HTML 출력. |
| sms | dx_sms_send | 직접 발화 | SMS 발송 처리. to·message 파라미터 수신. |
| social_login | dx_social_login | 직접 발화 | 소셜 로그인 버튼 HTML 출력. |
| socket | dx_bottom | dx_hook_bottom() | 소켓 클라이언트 JS 태그 출력. |
3.4 커스텀 타입 플러그인
내장 6가지 타입 외에 개발자가 임의 타입을 자유롭게 추가할 수 있습니다. 관리자 설정 화면에는 타입 정의 목록에 있는 타입만 자동 표시됩니다.
// 커스텀 타입 'pdf_converter' 플러그인
dx_register_plugin(array(
'id' => 'my-pdf',
'type' => 'pdf_converter', // 내장 타입 목록에 없어도 OK
'name' => 'My PDF Converter',
));
// 훅 구현
dx_add_hook('dx_pdf_convert', function($args) {
// PDF 변환 로직
}, 10);
// 사용 시
$activeConverter = dx_active_plugin('pdf_converter'); // 'my-pdf'
dx_run_hook('dx_pdf_convert', array('file'=>$filePath));
4. ③ DI Container — 서비스 객체 재사용
DI 컨테이너(Dependency Injection Container)는 서비스 객체를 한 번 생성하고 어디서든 꺼내 쓸 수 있게 하는 메커니즘입니다. PHP 5.6으로 구현된 경량 버전으로, 라라벨 Service Container와 동일한 철학을 따릅니다.
4.1 3가지 바인딩 방식
| singleton | 최초 1회만 생성, 이후 동일 인스턴스 반환 — 상태를 유지해야 하는 서비스 |
// 등록: 최초 dx_make() 호출 시에만 팩토리 실행
dx_app()->singleton('sms', function() {
return new AlimtalkSMS(dx_config('alimtalk_key'));
});
// 사용: 항상 같은 인스턴스
$sms1 = dx_make('sms');
$sms2 = dx_make('sms');
// $sms1 === $sms2 (동일 객체)
| bind | make() 호출마다 새 인스턴스 생성 — 상태가 없어야 하는 서비스 |
// 등록: make() 호출마다 새 인스턴스
dx_app()->bind('mail', function() {
return new MailSender(dx_config('smtp_host'));
});
// 사용: 매번 새 인스턴스
$mail1 = dx_make('mail');
$mail2 = dx_make('mail');
// $mail1 !== $mail2 (다른 객체)
| instance | 이미 생성된 객체를 직접 등록 — 항상 싱글턴 |
// 이미 만들어진 객체를 바로 등록
$myConfig = new MyConfig('/path/to/config.json');
dx_app()->instance('config', $myConfig);
// 사용
$config = dx_make('config');
4.2 핵심 서비스 자동 등록
index.php의 초기화 완료 후 registerCoreServices()가 호출되어 CMS 핵심 서비스들이 자동 등록됩니다. 이 서비스들은 별칭으로도 접근할 수 있습니다.| 서비스 키 | 별칭 | 실제 객체/클래스 | 접근 방법 |
| 'db' | 'database' | Database::getInstance() | dx_make('db') |
| 'auth' | - | Auth::getInstance() | dx_make('auth') |
| 'secure' | - | Secure::getInstance() | dx_make('secure') |
| 'cache' | - | 'DxCache' (클래스명 문자열) | dx_make('cache') |
| 'hook' | 'hooks' | HookManager::getInstance() | dx_make('hook') |
| 'site' | - | DxSite::getInstance() | dx_make('site') |
| 'theme' | - | DxTheme::getInstance() | dx_make('theme') |
4.3 실전 활용 패턴
패턴 A: 플러그인에서 외부 API 클라이언트 등록
// plugins/kakao-pay/plugin.php
dx_app()->singleton('kakao_pay', function() {
return new KakaoPayClient(dx_config('kakao_pay_key'));
});
// 결제 처리 훅에서 꺼내 쓰기
dx_add_hook('dx_payment_request', function($args) {
if ($args['payment'] !== 'kakao-pay') return;
$client = dx_make('kakao_pay'); // 항상 동일 인스턴스 (연결 재사용)
$client->request($args['amount'], $args['order_id']);
});
패턴 B: 별칭으로 추상화 (교체 용이)
// 등록 시 추상 이름 사용
dx_app()->singleton('storage', function() {
if (dx_config('storage_driver') === 's3') {
return new S3Storage(dx_config('s3_key'), dx_config('s3_secret'));
}
return new LocalStorage(DX_ROOT . '/data/uploads');
});
// 사용 시: 드라이버가 바뀌어도 코드 변경 없음
$storage = dx_make('storage');
$storage->upload($file, 'uploads/2026/05/');
패턴 C: 컨트롤러 자동 로드 및 호출
// controllers/BoardController.php 자동 로드 + 호출
dx_app()->call('BoardController@index', array(
'board_id' => $boardId,
'page' => $page,
));
// controllers/BoardController.php
class BoardController {
public function index($board_id, $page = 1) {
// DB, Cache 등 컨테이너에서 자동 주입
$db = dx_make('db');
$cache = dx_make('cache'); // === 'DxCache' 클래스명 문자열
// ...
}
}
패턴 D: 테스트•목업을 위한 인스턴스 교체
// 운영 환경
dx_app()->singleton('mailer', function() {
return new SmtpMailer(dx_config('smtp_host'));
});
// 테스트 환경 (instance로 목업 교체)
class MockMailer {
public function send($to, $subject, $body) {
file_put_contents('/tmp/mail.log', $to . ': ' . $subject . PHP_EOL, FILE_APPEND);
}
}
dx_app()->instance('mailer', new MockMailer());
// 이제 dx_make('mailer')는 MockMailer를 반환
5. ④ Extend 폴더 — 파일 기반 자동 실행 재사용
extend/ 폴더에 PHP 파일을 넣기만 하면 CMS가 자동으로 실행하는 가장 단순한 재사용 방식입니다. 훅 등록, DI 등록 등 어떤 코드도 작성할 수 있습니다.
5.1 3개 슬롯의 역할과 재사용 전략
| 슬롯 | 파일 위치 | 실행 시점 | 재사용 전략 |
| top/ | extend/top/*.php | 모든 초기화 완료 직후, 라우팅 전 | ob_start 등록, 훅 등록, 전역 변수 주입, 점검 모드 |
| middle/ | extend/middle/*.php | 라우트 확정 직후, 핸들러 전 | 라우트별 분기, 방문자 통계, A/B 테스트 |
| bottom/ | extend/bottom/*.php | 렌더링 완료 후, ob_end_flush 전 | 캐시 저장, 성능 로그, JS 태그 추가 |
5.2 파일 실행 규칙
extend/
├── top/
│ ├── 01_maintenance.php ← 01번: 항상 맨 먼저 (점검 모드)
│ ├── 02_ip_block.php ← 02번: 두 번째
│ ├── 10_global_vars.php ← 10번: 앞 번호 여유 확보
│ └── security/ ← 하위 폴더: 상위 파일 실행 후
│ └── 01_waf_custom.php
│
├── middle/
│ └── 01_visit_tracker.php ← 내장 파일 (방문자 통계)
│
└── bottom/
└── 02_darkmode_engine.php ← 내장 파일 (다크모드 JS)
※ 실행 순서: 파일명 오름차순 (사전식 정렬)
※ 01_, 02_, 10_, 11_ 처럼 자릿수를 맞추는 것이 중요
※ 파일명.php.disabled 로 변경하면 비활성화 (삭제 없이)
※ if (!defined('DX_CMS')) exit; 첫 줄 필수
5.3 내장 extend 파일 분석
extend/top/01_darkmode_early.php — ob_start 콜백 재사용ob_start에 콜백을 등록하여 렌더링이 완료된 HTML을 가공합니다. top/ 슬롯이라 렌더링 전에 등록되고, 실제 실행은 ob_end_flush() 시점(bottom/ 이후)에 자동으로 이루어집니다.
if (!defined('DX_CMS')) exit;
// ob_start 콜백 등록 (실행은 ob_end_flush() 시점)
ob_start(function($buffer) {
// ① FOUC 방지 인라인 스크립트를 <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>';
$buffer = preg_replace('/<head([^>]*)>/i',
'<head$1>' . $earlyScript, $buffer, 1);
// ② dx-dark-early 클래스를 body.dark로 교체하는 스크립트를 </body> 직전 추가
$lateScript = '<script>(function(){
var h=document.documentElement;
if(h.classList.contains("dx-dark-early")){
h.classList.remove("dx-dark-early");
document.body.classList.add("dark");
}
})();</script>';
$buffer = preg_replace('/<\/body>/i', $lateScript . '</body>', $buffer, 1);
return $buffer;
});
// ↑ 이 콜백은 ob_end_flush() 시점에 자동 실행됨
extend/middle/01_visit_tracker.php — register_shutdown_function 재사용
응답 후 비동기 처리 패턴의 대표 사례입니다. 사용자에게 응답을 먼저 보내고, 무거운 DB 작업은 shutdown 시점에 처리합니다.
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;
// 봇 판별 (40여 개 패턴 체크)
if ($_vt_isBot) return;
// DxCache로 순방문자 판단 (DB 조회 없음)
$cacheKey = 'vt_u:' . date('Y-m-d') . ':' . substr(md5($ip), 0, 12);
if (!DxCache::get($cacheKey, false)) {
DxCache::set($cacheKey, 1, 86400);
$_vt_isUnique = true;
}
// ★ 핵심: 응답 후 DB 기록 (사용자 체감 속도 무영향)
register_shutdown_function(function() use ($_vt_data) {
// PHP-FPM: 사용자에게 응답 먼저 전송
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// 이후 DB INSERT — 사용자는 이미 브라우저에서 페이지 렌더링 중
$db->query("INSERT INTO dx_visits...", $_vt_data);
});
5.4 extend 파일 작성 완성 패턴
패턴: 전역 헬퍼 함수 추가
// extend/top/05_my_helpers.php
if (!defined('DX_CMS')) exit;
// 이 파일에서 정의한 함수는 이후 모든 파일에서 사용 가능
function my_format_phone($phone) {
$phone = preg_replace('/[^0-9]/', '', $phone);
if (strlen($phone) === 11) {
return substr($phone,0,3).'-'.substr($phone,3,4).'-'.substr($phone,7);
}
return $phone;
}
function my_get_settings($key, $default = null) {
static $_settings = null;
if ($_settings === null) {
$_settings = DxCache::get('my_settings', false);
if ($_settings === false) {
$_settings = Database::getInstance()->row("SELECT * FROM my_settings LIMIT 1");
DxCache::set('my_settings', $_settings, 300);
}
}
return isset($_settings[$key]) ? $_settings[$key] : $default;
}
6. ⑤ Theme 파셜 — 테마 부분 템플릿 재사용
테마 파셜(Partial)은 반복되는 HTML 조각을 별도 파일로 분리하고 필요한 곳에서 재사용하는 방식입니다. DxTheme의 폴백 체인으로 테마별로 파셜을 오버라이드할 수 있습니다.
6.1 파셜 관련 전역 헬퍼 함수
| 함수 | 역할 | 사용 위치 |
| dx_include_part($name, $vars) | 파셜 파일 include. 현재테마→default 폴백. | 레이아웃, 스킨 파일 |
| dx_theme_file($relPath) | 테마 파일 절대경로 반환. | require/include 시 경로 해석 |
| dx_theme_asset($path) | 테마 에셋 URL 반환. | CSS/JS href·src 생성 |
| dx_theme_option($key, $default) | 테마 options.json 값 반환. | 테마 설정 값 읽기 |
6.2 파셜 파일 구조
themes/
default/
parts/
pagination.php ← dx_include_part('pagination')
breadcrumb.php ← dx_include_part('breadcrumb')
social-share.php ← dx_include_part('social-share')
comment-form.php ← dx_include_part('comment-form')
sidebar.php ← dx_include_part('sidebar')
my-theme/
parts/
pagination.php ← 오버라이드 (내 테마 전용 페이지네이션)
// breadcrumb.php 없음 → default/parts/breadcrumb.php 자동 사용
6.3 파셜 작성 및 사용 완성 예제
파셜 파일: themes/default/parts/pagination.php
// parts/pagination.php — $total, $page, $perPage, $urlPattern 변수 수신
if (!defined('DX_CMS')) exit;
$totalPages = (int)ceil($total / $perPage);
if ($totalPages <= 1) return;
$prev = $page > 1 ? str_replace('{page}', $page-1, $urlPattern) : '';
$next = $page < $totalPages ? str_replace('{page}', $page+1, $urlPattern) : '';
echo '<nav class="dx-pagination">';
if ($prev) echo '<a href="' . dx_safe_url($prev) . '" class="dx-page-btn">‹</a>';
for ($i = max(1, $page-2); $i <= min($totalPages, $page+2); $i++) {
$url = str_replace('{page}', $i, $urlPattern);
$class = $i === $page ? 'dx-page-btn dx-page-active' : 'dx-page-btn';
echo '<a href="' . dx_safe_url($url) . '" class="' . $class . '">' . $i . '</a>';
}
if ($next) echo '<a href="' . dx_safe_url($next) . '" class="dx-page-btn">›</a>';
echo '</nav>';
스킨 파일에서 파셜 재사용
// themes/default/board/list.php
// 게시글 목록 출력...
// 페이지네이션 파셜 재사용
dx_include_part('pagination', array(
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'urlPattern' => dx_base_url($boardKey) . '?page={page}',
));
// → my-theme/parts/pagination.php 있으면 그 파일
// → 없으면 default/parts/pagination.php 자동 사용
// 소셜 공유 버튼 파셜
dx_include_part('social-share', array(
'url' => dx_current_url(),
'title' => $post['title'],
));
테마 에셋 URL 재사용
<!-- themes/default/layout/main.php -->
<!-- 테마 에셋 URL (현재 테마 디렉토리 기준) -->
<link rel="stylesheet" href="<?php echo dx_theme_asset('css/style.css'); ?>">
<script src="<?php echo dx_theme_asset('js/app.js'); ?>"></script>
<!-- 출력 예 -->
<!-- https://example.com/themes/my-theme/css/style.css -->
<!-- 테마 옵션 사용 (options.json 또는 DB에서 로드) -->
<style>
:root {
--primary: <?php echo dx_esc(dx_theme_option('primary_color', '#3b82f6')); ?>;
--font-size: <?php echo dx_esc(dx_theme_option('font_size', '16px')); ?>;
}
</style>
7. ⑥ Cache 레이어 — 연산 결과 재사용
캐시(Cache)는 무거운 DB 쿼리, 복잡한 계산, 외부 API 호출 결과를 저장해 두고 같은 요청이 다시 들어올 때 재사용하는 성능 최적화 메커니즘입니다.
7.1 캐시 드라이버 자동 선택
// DxCache는 환경에 따라 최적 드라이버를 자동 선택
// Redis → APCu → 파일 → none 우선순위
echo DxCache::getDriver(); // 'redis' | 'apcu' | 'file' | 'none'
// 성능 비교 (SSD 기준)
// Redis: < 1ms (네트워크 경유)
// APCu: < 0.1ms (공유 메모리)
// 파일: 2~5ms (SSD I/O)
// none: DB 쿼리 실행 (~10ms+)
7.2 핵심 캐시 재사용 패턴
패턴 A: Cache-Aside (기본 패턴)
// ① 캐시 확인 → ② 없으면 DB 쿼리 → ③ 결과 캐싱
$cacheKey = 'board_list_v2';
$boards = DxCache::get($cacheKey, false);
if ($boards === false) {
$boards = Database::getInstance()->rows(
"SELECT * FROM dx_boards WHERE status=1 ORDER BY sort_order ASC"
);
DxCache::set($cacheKey, $boards, 600); // 10분 TTL
}
// $boards는 캐시(DB 쿼리 없음) 또는 DB 쿼리 결과
패턴 B: 접두어 기반 그룹 무효화
// 게시판 설정 캐시 (board_ 접두어)
DxCache::set('board_1_config', $config1, 300);
DxCache::set('board_2_config', $config2, 300);
DxCache::set('board_1_posts_page1', $posts, 60);
// 게시판 설정 변경 시 관련 캐시 전체 삭제
DxCache::deletePrefix('board_1_'); // board_1_ 로 시작하는 캐시 전체 삭제
// 사이트 전체 캐시 초기화 (관리자 설정 변경 시)
DxCache::flush();
패턴 C: 순방문자 판단 (DB 없이)
// 오늘 이 IP의 첫 방문인지 캐시로 판단 (DB 조회 없음)
$cacheKey = 'vt_u:' . date('Y-m-d') . ':' . substr(md5(dx_ip()), 0, 12);
$isUnique = false;
if (!DxCache::get($cacheKey, false)) {
DxCache::set($cacheKey, 1, 86400); // 오늘 자정까지 TTL
$isUnique = true;
}
// Redis: 원자적 연산으로 동시 요청에도 정확
// 파일: LOCK_EX 잠금으로 경쟁 조건 방지
패턴 D: 무거운 연산 결과 캐싱
// 전체 메뉴 트리 생성 (재귀 쿼리 대신 캐싱)
function my_get_menu_tree($group = 'main') {
$cacheKey = 'menu_tree_' . $group;
$tree = DxCache::get($cacheKey, false);
if ($tree !== false) return $tree;
$db = Database::getInstance();
$menus = $db->rows(
"SELECT * FROM dx_menus WHERE menu_group=? AND status=1 ORDER BY sort_order",
array($group)
);
// 재귀 트리 생성 (CPU 비용)
$tree = my_build_tree($menus, 0);
DxCache::set($cacheKey, $tree, 300); // 5분 캐싱
return $tree;
}
// 메뉴 변경 시 캐시 삭제
DxCache::deletePrefix('menu_tree_');
패턴 E: 외부 API 응답 캐싱
// 날씨 API 응답 캐싱 (1시간)
function my_get_weather($city) {
$cacheKey = 'weather_' . md5($city);
$data = DxCache::get($cacheKey);
if ($data !== null) return $data;
$response = file_get_contents('https://api.weather.com/?city=' . urlencode($city));
$data = json_decode($response, true);
DxCache::set($cacheKey, $data, 3600); // 1시간 TTL
return $data;
}
8. 6가지 메커니즘 조합 패턴
실제 운영 환경에서는 여러 메커니즘을 조합하여 사용합니다. 대표적인 조합 패턴을 분석합니다.
8.1 조합 패턴 A: 플러그인 + 훅 + 캐시
소셜 로그인 플러그인: 등록(Plugin) → 로그인 버튼 출력(Hook) → 사용자 정보 캐싱(Cache)
// plugins/kakao-login/plugin.php
// ① Plugin 등록
dx_register_plugin(array('id'=>'kakao-login','type'=>'social_login','name'=>'카카오 로그인'));
// ② Hook: 로그인 폼에 카카오 버튼 삽입
dx_add_hook('dx_auth_login_form', function($ctx) {
$key = dx_config('kakao_app_key', '');
if (!$key) return;
echo '<a href="' . dx_base_url('auth/kakao') . '" class="btn-kakao">카카오로 로그인</a>';
}, 10);
// ③ Hook: 콜백 처리 후 사용자 정보 캐싱
dx_add_hook('dx_after_social_login', function($args) {
$userId = $args['user_id'];
$profile = fetch_kakao_profile($args['access_token']);
// Cache: 프로필 정보 1시간 캐싱 (API 호출 재사용)
DxCache::set('kakao_profile_' . $userId, $profile, 3600);
// Hook 재사용: 로그인 후처리
dx_run_hook('dx_after_login', array('user_id'=>$userId));
});
8.2 조합 패턴 B: Extend + 훅 + DI Container
extend/top/ 파일에서 커스텀 서비스를 컨테이너에 등록하고, 훅으로 특정 시점에 실행합니다.
// extend/top/03_my_services.php
if (!defined('DX_CMS')) exit;
// ① DI Container에 서비스 등록
dx_app()->singleton('push_notifier', function() {
return new FirebasePushNotifier(dx_config('firebase_key'));
});
// ② 훅으로 게시글 작성 시 푸시 알림
dx_add_hook('dx_after_write', function($args) {
$notifier = dx_make('push_notifier');
$notifier->sendToAll('새 글이 등록되었습니다.', array(
'post_id' => $args['post_id'],
));
}, 20);
8.3 조합 패턴 C: 테마 파셜 + 캐시 + 훅
홈페이지 최신글 위젯: 파셜 템플릿 + 캐싱된 DB 쿼리 + 커스텀 훅
// themes/default/parts/latest-posts.php
if (!defined('DX_CMS')) exit;
// 변수: $boardKey, $limit, $title, $skin
// ① Cache로 DB 쿼리 재사용
$cacheKey = 'latest_' . $boardKey . '_' . $limit;
$posts = DxCache::get($cacheKey, false);
if ($posts === false) {
$posts = dx_board_posts($boardKey, $limit);
DxCache::set($cacheKey, $posts, 120); // 2분 캐싱
}
// ② 출력 전 훅으로 데이터 가공 허용
$posts = dx_apply_filter('dx_latest_posts', $posts, array('board'=>$boardKey));
// ③ 파셜 내 출력
echo '<section class="latest-' . dx_esc($skin) . '">';
echo '<h2>' . dx_esc($title) . '</h2>';
foreach ($posts as $post) {
echo '<a href="' . dx_esc($post['url']) . '">' . dx_esc($post['title']) . '</a>';
}
echo '</section>';
// 홈 페이지에서 호출
dx_include_part('latest-posts', array(
'boardKey' => 'notice',
'limit' => 5,
'title' => '공지사항',
'skin' => 'list',
));
9. 재사용 메커니즘 빠른 참조
| 목적 | 메커니즘 | 핵심 코드 |
| 모든 페이지 하단에 JS 삽입 | Hook (Action) | dx_add_hook('dx_bottom', fn, 10) |
| 게시글 내용 필터링 | Hook (Filter) | dx_add_filter('dx_post_content', fn) |
| 에디터 교체 | Plugin | dx_register_plugin(['type'=>'editor']) |
| 결제 모듈 교체 | Plugin | dx_register_plugin(['type'=>'payment']) |
| API 클라이언트 공유 | DI Container | dx_app()->singleton('sms', fn) |
| 서비스 꺼내 쓰기 | DI Container | dx_make('sms') |
| 파일만 넣어 자동 실행 | Extend 폴더 | extend/top/01_my.php 파일 생성 |
| 응답 후 비동기 처리 | Extend (middle) | register_shutdown_function(fn) |
| HTML 전체 가공 | Extend (top) | ob_start(callback) |
| 반복 HTML 조각 분리 | Theme 파셜 | dx_include_part('pagination', $vars) |
| 테마 에셋 URL | Theme | dx_theme_asset('css/style.css') |
| DB 쿼리 결과 캐싱 | Cache | DxCache::set($key, $val, $ttl) |
| 캐시 조회 | Cache | DxCache::get($key, false) |
| 그룹 캐시 삭제 | Cache | DxCache::deletePrefix('board_1_') |
| 훅 존재 확인 | Hook | dx_has_hook('dx_editor_render') |
| 훅 제거 | Hook | dx_remove_hook('dx_bottom', fn) |
재사용 방식 핵심 원칙 10가지
1. 코어 수정 금지. 모든 커스터마이징은 6가지 메커니즘 중 하나로 해결한다.2. Hook은 시점 기반. 특정 시점에 코드를 끼워 넣거나 값을 변환할 때 사용.
3. Filter 훅은 반드시 return을 써야 한다. 빠뜨리면 null이 반환되어 데이터 소멸.
4. Plugin은 타입 기반 교체. 에디터•결제 등 통째로 바꿔야 할 때 사용.
5. DI Container는 객체 공유. 동일 인스턴스가 여러 곳에서 필요할 때 singleton.
6. Extend는 파일 기반 자동 실행. 파일만 넣으면 된다. 순서는 파일명으로 제어.
7. Theme 파셜은 HTML 재사용. parts/ 폴더에 분리하고 dx_include_part()로 호출.
8. Cache는 연산 재사용. DB 쿼리•API 결과는 항상 캐시를 먼저 확인한다.
9. 응답 후 처리는 register_shutdown_function. 무거운 작업은 사용자 응답 후.
10. 메커니즘은 조합하여 사용. Plugin + Hook + Cache + Extend는 함께 동작한다.