회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항N
통계
로그인 회원가입
고객센터
3.9 공통 함수 / 유틸

재사용 방식

A Administrator
2026.04.21 01:01(수정됨) 103 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

로그인 후 댓글을 작성할 수 있습니다.
3.9 공통 함수 / 유틸 재사용 방식 2026.04.21 3.9 공통 함수 / 유틸 공통 클래스 구조 2026.04.21 3.9 공통 함수 / 유틸 전역 함수 구조 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록