회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
3.9 공통 함수 / 유틸

재사용 방식

D DX
2026.04.21 01:01(수정됨) 140 0
 

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는 함께 동작한다.

댓글0

로그인 후 댓글을 작성할 수 있습니다.
5. 관리자 기능 사용법 회원 랭킹 2026.04.21 5. 관리자 기능 사용법 포인트샵 2026.04.21 5. 관리자 기능 사용법 레벨 관리 2026.04.21 5. 관리자 기능 사용법 포인트 관리 2026.04.21 5. 관리자 기능 사용법 문자 서비스 2026.04.21 5. 관리자 기능 사용법 메일 보내기 2026.04.21 5. 관리자 기능 사용법 회원 관리 2026.04.21 5. 관리자 기능 사용법 메뉴 관리 2026.04.21 5. 관리자 기능 사용법 인기글 2026.04.21 5. 관리자 기능 사용법 카테고리 2026.04.21 5. 관리자 기능 사용법 게시판 그룹 2026.04.21 5. 관리자 기능 사용법 페이지 관리 2026.04.21 5. 관리자 기능 사용법 전체 공지 2026.04.21 5. 관리자 기능 사용법 팝업 관리 2026.04.21 5. 관리자 기능 사용법 게시판 관리 2026.04.21 4.2 관리자 시스템 구조 관리자 UI 구조 2026.04.21 4.2 관리자 시스템 구조 관리자 라우팅 2026.04.21 4.1 CMS 아키텍처 데이터 흐름 연결 2026.04.21 4.1 CMS 아키텍처 DX 위에 CMS가 올라가는 구조 2026.04.21 3.10 모듈 로딩 구조 자동 로딩 구조 2026.04.21
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록