회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
3.7 Hook 시스템

Hook 시스템 활용 사례

D DX
2026.04.21 00:59(수정됨) 130 0

1. Hook 시스템 개요

DXCMS의 Hook 시스템은 코어 소스를 직접 수정하지 않고도 CMS의 동작을 확장하거나 변경할 수 있는 이벤트 기반 확장 메커니즘입니다. WordPress의 훅 시스템에서 영감을 받아 DXCMS 환경에 맞게 설계되었으며, PHP 5.6+를 지원합니다.


1.1 핵심 개념

HookManager는 싱글턴 패턴으로 구현된 중앙 관리 클래스로, 모든 훅의 등록•실행•제거를 담당합니다. 전역 헬퍼 함수를 통해 코드 어디서나 간편하게 사용할 수 있습니다.
 
Action 훅 (dx_run_hook) Filter 훅 (dx_apply_filter)
부수 효과 실행 목적 값을 변환하여 반환하는 목적
반환값 없음 (void) 변환된 값을 반환 (return)
HTML 출력, DB 기록, 이메일 발송 등 컨텐츠 필터링, 데이터 가공 등
dx_add_hook() + dx_run_hook() dx_add_filter() + dx_apply_filter()


1.2 전역 헬퍼 함수 레퍼런스

DXCMS는 HookManager 클래스를 직접 사용하는 대신 전역 함수를 통해 더 간결하게 훅을 사용할 수 있습니다.
 
// ── Action 훅 등록 ──────────────────────────────
dx_add_hook($name, $callback, $priority = 10);
 
// ── Action 훅 실행 ──────────────────────────────
dx_run_hook($name, $args = array());
 
// ── Filter 훅 등록 ──────────────────────────────
dx_add_filter($name, $callback, $priority = 10);
 
// ── Filter 훅 실행 (변환된 값 반환) ──────────────
$result = dx_apply_filter($name, $value, $args = array());
 
// ── 훅 제거 ────────────────────────────────────
dx_remove_hook($name, $callback = null);  // null이면 전체 제거
 
// ── 훅 등록 여부 확인 ──────────────────────────
$exists = dx_has_hook($name);

1.3 우선순위(Priority) 동작 방식
우선순위는 정수값이며 낮을수록 먼저 실행됩니다. 기본값은 10입니다.

// 우선순위 1 → 가장 먼저 실행
dx_add_hook("dx_bottom", function($ctx) {
    echo "<!-- 첫 번째 실행 -->";
}, 1);
 
// 우선순위 10 (기본값)
dx_add_hook("dx_bottom", function($ctx) {
    echo "<!-- 두 번째 실행 -->";
}, 10);
 
// 우선순위 999 → 가장 나중에 실행
dx_add_hook("dx_bottom", function($ctx) {
    echo "<!-- 마지막 실행 -->";
}, 999);


2. 표준 훅 포인트 전체 목록

DXCMS가 기본 제공하는 모든 훅 포인트입니다. 플러그인과 extend 파일에서 이 훅들을 구독하여 기능을 확장합니다.


2.1 레이아웃 렌더링 훅

훅 이름 실행 위치 전달 인자 주요 용도 타입
dx_head <head> 태그 내부 $context 배열 CSS/JS 추가, 메타 태그 Action
dx_top 모든 페이지 최상단 (body 시작) $context 배열 공지사항, 배너, 전역 UI Action
dx_middle 컨텐츠 영역 내부 $context 배열 컨텐츠 삽입, 광고 Action
dx_bottom 모든 페이지 최하단 (body 끝) $context 배열 JS 삽입, 추적 코드 Action
dx_body_bottom </body> 직전 없음 채팅 위젯, 팝업, 모달 Action
dx_footer_scripts 푸터 스크립트 영역 빈 배열 추가 스크립트 파일 로드 Action


2.2 페이지 타입별 훅

컨텍스트 내  type  값에 따라 자동으로  dx_{type}_top ,  dx_{type}_middle ,  dx_{type}_bottom  훅이 실행됩니다.
 
// 페이지 타입별 훅 자동 생성 예시
// type = "board" 인 경우:
//   dx_board_top     ← 게시판 페이지 상단
//   dx_board_middle  ← 게시판 페이지 중간
//   dx_board_bottom  ← 게시판 페이지 하단
 
// type = "page" 이고 slug = "about" 인 경우:
//   dx_page_about_top     ← about 페이지 상단만
//   dx_page_about_middle  ← about 페이지 중간만
//   dx_page_about_bottom  ← about 페이지 하단만


2.3 게시판(Board) 훅

훅 이름 실행 위치 전달 인자 주요 용도 타입
dx_board_before 게시판 핸들러 진입 직전 board, action, skin, id 접근 제어, 사전 처리 Action
dx_board_after 게시판 핸들러 완료 직후 board, action, skin 후처리, 로그 Action
dx_board_list_context 목록 컨텍스트 확정 직후 &$context, board 목록 데이터 가공 Action
dx_board_view_context 뷰 컨텍스트 확정 직후 &$context, board, post 뷰 데이터 가공 Action
dx_board_write_context 쓰기 컨텍스트 확정 직후 &$context, board 쓰기 폼 데이터 가공 Action
dx_board_before_save 글 저장 직전 (쓰기/수정) &$data, board, action 데이터 검증•변환 Action
dx_board_after_save 글 저장 완료 직후 post_id, board, &$redirect 알림 발송, 포인트 지급 Action
dx_board_before_delete 글 삭제 직전 post, board_key 삭제 전 백업, 검증 Action
dx_board_after_delete 글 삭제 완료 직후 post_id, board_key 관련 데이터 정리 Action


2.4 회원 인증 훅

훅 이름 실행 위치 전달 인자 주요 용도 타입
dx_after_login 로그인 성공 직후 user (회원 정보 배열) 마지막 접속 기록, 알림 Action
dx_after_logout 로그아웃 처리 직후 user (회원 정보 배열) 세션 정리, 로그 Action
dx_after_register 회원가입 완료 직후 user_id, data (가입 데이터) 환영 이메일, 포인트 지급 Action


2.5 댓글•좋아요•포인트 훅

훅 이름 실행 위치 전달 인자 주요 용도 타입
dx_after_comment 댓글 작성 완료 직후 post_id, comment_id, member_id 댓글 알림, 포인트 Action
dx_after_like 좋아요 처리 직후 post_id, owner_id 좋아요 알림 Action
dx_after_point 포인트 증감 직후 member_id, type, point, balance 포인트 이력 로그 Action
dx_levelup 레벨업 발생 직후 member_id, old_level, new_level 레벨업 축하 알림 Action
dx_add_friend 친구 추가 직후 member_id, target_id 친구 알림 Action


2.6 결제•에디터•기타 훅

훅 이름 실행 위치 전달 인자 주요 용도 타입
dx_payment_request 결제창 렌더링 시 amount, product_name, order_id… PG사별 결제창 처리 Action
dx_shop_after_purchase 구매 완료 직후 payment 관련 데이터 구매 이력 기록 Action
dx_editor_render 에디터 HTML 생성 시 name, value, options 커스텀 에디터 렌더 Action
dx_editor_init 에디터 초기화 시 config 배열 에디터 옵션 주입 Action
dx_mailer_drivers 메일 드라이버 등록 시 없음 커스텀 메일 드라이버 Action
dx_sms_drivers SMS 드라이버 등록 시 빈 배열 커스텀 SMS 드라이버 Action
dx_captcha_drivers Captcha 드라이버 등록 시 없음 커스텀 Captcha 추가 Action
dx_admin_top 관리자 페이지 상단 action (현재 관리 액션) 관리자 UI 확장 Action
dx_admin_bottom 관리자 페이지 하단 action 관리자 스크립트 Action
dx_admin_dashboard_widgets 대시보드 위젯 영역 없음 커스텀 대시보드 위젯 Action


3. 활용 사례


사례 1 — 전역 스크립트•스타일 삽입

모든 페이지에 공통 CSS나 JS를 삽입할 때  dx_head  /  dx_body_bottom  훅을 사용합니다. Google Tag Manager, 채팅 위젯, 공통 CSS 오버라이드 등에 활용합니다.


예제 A — Google Tag Manager 삽입

// plugins/my-analytics/plugin.php
 
// <head> 안에 GTM 스크립트 삽입
dx_add_hook("dx_head", function($context) {
    $gtmId = dx_config("gtm_id", "");
    if (!$gtmId) return;
    echo '<script>(function(w,d,s,l,i){w[l]=w[l]||[];',
         'w[l].push({\'gtm.start\':',
         'new Date().getTime(),event:\'gtm.js\'});',
         'var f=d.getElementsByTagName(s)[0],',
         'j=d.createElement(s),dl=l!=\'dataLayer\'?\'&l=\'+l:\'\';',
         'j.async=true;j.src=\'',
         'https://www.googletagmanager.com/gtm.js?id=\'+i+dl;',
         'f.parentNode.insertBefore(j,f);',
         '})(window,document,\'script\',\'dataLayer\',\'' . $gtmId . '\');</script>';
}, 1);  // 우선순위 1 → head 안에서 가장 먼저
 
// <body> 최상단에 GTM noscript 삽입
dx_add_hook("dx_top", function($context) {
    $gtmId = dx_config("gtm_id", "");
    if (!$gtmId) return;
    echo '<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=',
         $gtmId, '" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>';
}, 1)


예제 B — 채팅 위젯 (Floating Button) 삽입

// extend/bottom/05_chat_widget.php
// </body> 직전에 채팅 위젯 삽입
 
dx_add_hook("dx_body_bottom", function() {
    // 로그인 회원에게만 채팅 위젯 표시
    if (!Auth::getInstance()->isLoggedIn()) return;
 
    echo '<div id="chat-widget" style="position:fixed;bottom:20px;right:20px;z-index:9999;">';
    echo '  <button style="background:#1E3A5F;color:#fff;';
    echo '          border:none;border-radius:50%;width:56px;height:56px;';
    echo '          font-size:24px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.3)">';
    echo '    💬</button>';
    echo '</div>';
    echo '<script src="' . dx_base_url("assets/js/chat-widget.js") . '"></script>';
});


사례 2 — 로그인•로그아웃 후처리

Auth 클래스가 로그인/로그아웃 처리 완료 직후  dx_after_login  /  dx_after_logout  훅을 실행합니다. 접속 기록, 알림, 리워드 지급 등에 활용합니다.


예제 A — 마지막 접속 일시 업데이트

// extend/top/02_login_tracker.php
 
dx_add_hook("dx_after_login", function($args) {
    $userId = isset($args["user"]["id"]) ? (int)$args["user"]["id"] : 0;
    if (!$userId) return;
 
    $db = Database::getInstance();
    $db->query(
        "UPDATE " . $db->table("members") . " SET last_login_at = NOW() WHERE id = ?",
        [$userId]
    );
 
    // 접속 로그 기록
    $ip = $_SERVER["REMOTE_ADDR"] ?? "unknown";
    $db->query(
        "INSERT INTO " . $db->table("login_logs") .
        " (member_id, ip, logged_in_at) VALUES (?, ?, NOW())",
        [$userId, $ip]
    );
});


예제 B — 연속 로그인 포인트 지급

// plugins/attendance/plugin.php
 
dx_add_hook("dx_after_login", function($args) {
    $user = $args["user"] ?? [];
    $userId = (int)($user["id"] ?? 0);
    if (!$userId) return;
 
    $db = Database::getInstance();
    $cache = DxCache::get("login_today:" . $userId);
 
    // 오늘 이미 출석했으면 스킵
    if ($cache) return;
 
    // 오늘 처음 로그인 → 출석 포인트 지급
    DxCache::set("login_today:" . $userId, 1, 86400);
 
    $point = new DxPoint();
    $point->add($userId, "attendance", 10, "일일 출석 포인트");
 
    // 알림 발송
    $notify = new DxNotification();
    $notify->send($userId, "출석 체크 완료! +10 포인트 지급되었습니다.", "/mypage");
});


예제 C — 로그아웃 시 세션 토큰 무효화

// plugins/security-plus/plugin.php
 
dx_add_hook("dx_after_logout", function($args) {
    $user = $args["user"] ?? [];
    $userId = (int)($user["id"] ?? 0);
    if (!$userId) return;
 
    // 해당 회원의 모든 "기억하기" 토큰 삭제
    $db = Database::getInstance();
    $db->query(
        "DELETE FROM " . $db->table("remember_tokens") . " WHERE member_id = ?",
        [$userId]
    );
 
    // Redis 캐시에서도 세션 삭제
    DxCache::delete("session:" . $userId);
    DxCache::delete("login_today:" . $userId);
});


사례 3 — 회원가입 후처리 (환영 이메일 + 포인트)

dx_after_register  훅은 회원가입 절차가 모두 완료된 후 실행됩니다. user_id와 data(sanitize된 가입 데이터 배열)가 전달됩니다.


예제 — 환영 이메일 + 가입 포인트 지급 통합

// plugins/welcome/plugin.php
 
dx_add_hook("dx_after_register", function($args) {
    $userId = (int)($args["user_id"] ?? 0);
    $data   = $args["data"] ?? [];
    if (!$userId || empty($data["email"])) return;
 
    // 1) 환영 이메일 발송
    $mailer = new DxMailer();
    $mailer->to($data["email"])
           ->subject("[" . dx_site_name() . "] 회원가입을 환영합니다!")
           ->html(
               "<h2>안녕하세요, " . htmlspecialchars($data["nickname"] ?? "회원") . "님!</h2>"
             . "<p>가입해 주셔서 감사합니다.</p>"
             . "<p>가입 기념으로 <strong>100 포인트</strong>를 드립니다.</p>"
             . "<p><a href=\"" . dx_base_url() . "\">사이트 바로가기</a></p>"
           )
           ->send();
 
    // 2) 가입 기념 포인트 지급
    $point = new DxPoint();
    $point->add($userId, "register", 100, "회원가입 기념 포인트");
 
    // 3) 관리자에게 새 회원 알림 (Slack Webhook 예시)
    $webhookUrl = dx_config("slack_webhook_url", "");
    if ($webhookUrl) {
        $payload = json_encode(["text" =>
            "✅ 새 회원 가입: " . ($data["nickname"] ?? "미정") .
            " (" . $data["email"] . ") — " . date("Y-m-d H:i:s")
        ]);
        // 비동기 처리 (응답 속도 영향 없음)
        register_shutdown_function(function() use ($webhookUrl, $payload) {
            $ch = curl_init($webhookUrl);
            curl_setopt_array($ch, [
                CURLOPT_POST => 1,
                CURLOPT_POSTFIELDS => $payload,
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
                CURLOPT_TIMEOUT => 5,
            ]);
            curl_exec($ch);
            curl_close($ch);
        });
    }
});


사례 4 — 게시판 글 저장 전•후 처리

dx_board_before_save는 참조(&$data)로 데이터를 전달하므로 실제로 저장될 데이터를 수정할 수 있습니다. dx_board_after_save / dx_after_write는 저장 완료 후 알림, 포인트 지급 등 후처리에 사용합니다.


예제 A — 저장 전 금지어 필터 (before_save)

// plugins/content-filter/plugin.php
 
dx_add_hook("dx_board_before_save", function($args) {
    // $args["data"]는 참조(&)로 전달됨 → 직접 수정 가능
    $data  = &$args["data"];
    $board = $args["board"] ?? [];
 
    // 특정 게시판에만 필터 적용
    if (($board["board_key"] ?? "") !== "free") return;
 
    // 금지어 목록
    $blocked = ["욕설1", "욕설2", "스팸키워드"];
 
    // 제목•내용 금지어 치환
    foreach ($blocked as $word) {
        if (isset($data["title"])) {
            $data["title"] = str_replace($word, str_repeat("*", mb_strlen($word)), $data["title"]);
        }
        if (isset($data["content"])) {
            $data["content"] = str_replace($word, str_repeat("*", mb_strlen($word)), $data["content"]);
        }
    }
    // 수정된 $data는 자동으로 저장 로직에 반영됨
});


예제 B — 글 작성 후 알림•포인트 지급 (after_save / after_write)

// plugins/board-rewards/plugin.php
 
dx_add_hook("dx_after_write", function($args) {
    $postId = (int)($args["post_id"] ?? 0);
    $board  = $args["board"] ?? [];
    $data   = $args["data"] ?? [];
    if (!$postId) return;
 
    $authorId = (int)($data["member_id"] ?? 0);
 
    // 1) 글 작성 포인트 지급
    if ($authorId) {
        $point = new DxPoint();
        $point->add($authorId, "write", 5, "글 작성 포인트");
    }
 
    // 2) 공지 게시판 글 → 모든 회원에게 알림
    if (($board["board_key"] ?? "") === "notice") {
        $db = Database::getInstance();
        $members = $db->query(
            "SELECT id FROM " . $db->table("members") .
            " WHERE push_notice = 1 LIMIT 200"
        );
        $notify = new DxNotification();
        $title  = $data["title"] ?? "새 공지사항";
        foreach ($members as $m) {
            $notify->send((int)$m["id"], "📢 " . $title, "/board/notice/view/" . $postId);
        }
    }
});


사례 5 — 댓글 작성 후 원글 작성자 알림

dx_after_comment  훅은 댓글이 성공적으로 저장된 직후 실행됩니다. post_id, comment_id, member_id 등 댓글 관련 정보가 전달됩니다.


예제 — 댓글 작성 시 원글 작성자에게 알림 + 이메일

// plugins/comment-notify/plugin.php
 
dx_add_hook("dx_after_comment", function($args) {
    $postId    = (int)($args["post_id"]    ?? 0);
    $commentId = (int)($args["comment_id"] ?? 0);
    $writerId  = (int)($args["member_id"]  ?? 0);  // 댓글 작성자
    if (!$postId || !$writerId) return;
 
    $db = Database::getInstance();
 
    // 원글 정보 조회
    $post = $db->query(
        "SELECT p.id, p.title, p.member_id, m.email, m.nickname" .
        " FROM " . $db->table("posts") . " p" .
        " JOIN " . $db->table("members") . " m ON m.id = p.member_id" .
        " WHERE p.id = ? LIMIT 1",
        [$postId]
    );
    if (empty($post)) return;
    $post = $post[0];
 
    $postOwnerId = (int)$post["member_id"];
 
    // 자신의 글에 단 댓글은 알림 불필요
    if ($postOwnerId === $writerId) return;
 
    // 1) 사이트 내 알림
    $notify = new DxNotification();
    $notify->send(
        $postOwnerId,
        "내 글 \"" . mb_substr($post["title"], 0, 20) . "\"에 댓글이 달렸습니다.",
        "/board/view/" . $postId . "?c=" . $commentId
    );
 
    // 2) 이메일 알림 (수신 설정 확인)
    $setting = $db->query(
        "SELECT email_comment FROM " . $db->table("member_settings") .
        " WHERE member_id = ? LIMIT 1",
        [$postOwnerId]
    );
    $emailEnabled = empty($setting) ? 1 : (int)($setting[0]["email_comment"] ?? 1);
 
    if ($emailEnabled && $post["email"]) {
        $mailer = new DxMailer();
        $mailer->to($post["email"])
               ->subject("[" . dx_site_name() . "] 댓글 알림")
               ->html("<p><strong>" . htmlspecialchars($post["nickname"]) . "</strong>님의 글에 댓글이 작성되었습니다.</p>",
                      "<p><a href=\"" . dx_base_url("board/view/" . $postId) . "\">댓글 보러가기</a></p>")
               ->send();
    }
});


사례 6 — 포인트 변동 & 레벨업 처리

DxPoint 클래스가 포인트 증감 후  dx_after_point 을, 레벨이 올랐을 때  dx_levelup  훅을 실행합니다.


예제 A — 포인트 이력 외부 DB 동기화

// plugins/point-sync/plugin.php
 
dx_add_hook("dx_after_point", function($args) {
    $memberId = (int)($args["member_id"] ?? 0);
    $type     = $args["type"]    ?? "";
    $point    = (int)($args["point"]   ?? 0);
    $balance  = (int)($args["balance"] ?? 0);
    if (!$memberId) return;
 
    // 외부 분석 DB에 이력 기록 (비동기)
    register_shutdown_function(function() use ($memberId, $type, $point, $balance) {
        $db = Database::getInstance();
        $db->query(
            "INSERT INTO " . $db->table("point_history_ext") .
            " (member_id, type, delta, balance, recorded_at) VALUES (?, ?, ?, ?, NOW())",
            [$memberId, $type, $point, $balance]
        );
    });
});


예제 B — 레벨업 축하 알림 + 등급별 혜택 부여

// plugins/level-rewards/plugin.php
 
dx_add_hook("dx_levelup", function($args) {
    $memberId = (int)($args["member_id"] ?? 0);
    $oldLevel = (int)($args["old_level"] ?? 0);
    $newLevel = (int)($args["new_level"] ?? 0);
    if (!$memberId || $newLevel <= $oldLevel) return;
 
    // 레벨별 혜택 포인트 정의
    $rewards = [5 => 500, 10 => 2000, 20 => 10000, 30 => 50000];
    $bonusPoint = $rewards[$newLevel] ?? 50;
 
    // 보너스 포인트 지급
    $point = new DxPoint();
    $point->add($memberId, "levelup_bonus", $bonusPoint, "레벨 {$newLevel} 달성 보너스");
 
    // 사이트 내 축하 알림
    $notify = new DxNotification();
    $notify->send(
        $memberId,
        "🎉 레벨 {$newLevel}으로 레벨업! 보너스 {$bonusPoint}P 지급됨",
        "/mypage/point"
    );
 
    // 레벨 30 이상 → 특별 배지 부여
    if ($newLevel >= 30) {
        $db = Database::getInstance();
        $db->query(
            "INSERT IGNORE INTO " . $db->table("member_badges") .
            " (member_id, badge_key, awarded_at) VALUES (?, ?, NOW())",
            [$memberId, "expert"]
        );
    }
});


사례 7 — 커스텀 에디터 등록 및 렌더링

dx_editor_render  훅은 에디터 HTML을 생성하는 시점에 실행됩니다. CKEditor4 플러그인이 이 훅을 사용하여 에디터를 렌더링합니다. 동일한 방식으로 다른 에디터(SimpleMDE, Toast UI Editor 등)를 등록할 수 있습니다.


예제 — SimpleMDE 마크다운 에디터 등록

// plugins/simplemde-editor/plugin.php
 
dx_register_plugin([
    "id"          => "simplemde-editor",
    "type"        => "editor",
    "name"        => "SimpleMDE (마크다운)",
    "version"     => "1.0.0",
    "description" => "마크다운 기반 심플 에디터",
]);
 
if (dx_config("active_editor", "") !== "simplemde-editor") return;
 
// 에디터 렌더 훅 등록
dx_add_hook("dx_editor_render", function($args) {
    $name  = $args["name"]  ?? "content";
    $value = $args["value"] ?? "";
    $opts  = $args["options"] ?? [];
 
    $editorId = "simplemde_" . preg_replace("/[^a-zA-Z0-9_]/", "_", $name);
 
    echo '<link rel="stylesheet" href="' . dx_base_url("assets/simplemde/simplemde.min.css") . '">';
    echo '<textarea id="' . $editorId . '"
         name="' . htmlspecialchars($name, ENT_QUOTES) . '">';
    echo htmlspecialchars($value, ENT_QUOTES);
    echo '</textarea>';
    echo '<script src="' . dx_base_url("assets/simplemde/simplemde.min.js") . '"></script>';
    echo '<script>';
    echo 'var simplemde = new SimpleMDE({
             element: document.getElementById("' . $editorId . '"),
             spellChecker: false,
             placeholder: "마크다운으로 작성하세요...",
         });';
    echo '</script>';
});


사례 8 — 결제 PG사 플러그인 등록

DXCMS는  dx_payment_request  훅을 통해 PG사별 결제창 처리를 분리합니다. 다날(DPAY) 플러그인의 실제 코드를 기반으로 설명합니다.


예제 — 다날 DPAY 결제창 연동 (실제 소스 기반)

// plugins/danal-payment/plugin.php (간략화)
 
// 플러그인 등록
dx_register_plugin([
    "id"    => "danal-payment",
    "type"  => "payment",
    "name"  => "다날 DPAY",
    "settings" => [
        "cpid"       => ["label" => "CPID", "type" => "text"],
        "cpkey"      => ["label" => "CP KEY", "type" => "password"],
        "mode"       => ["label" => "운영 모드", "type" => "select",
                         "options" => ["test" => "테스트", "live" => "실서비스"]],
        "pay_method" => ["label" => "결제 수단", "type" => "select",
                         "options" => ["CARD" => "신용카드", "MOBILE" => "휴대폰"]],
    ],
]);
 
// 이 플러그인이 활성화된 경우에만 훅 등록
if (dx_active_plugin("payment") !== "danal-payment") return;
 
// 결제창 렌더 훅
dx_add_hook("dx_payment_request", function($data) {
    $cpid   = dx_pay_cfg("danal-payment", "cpid");
    $cpkey  = dx_pay_cfg("danal-payment", "cpkey");
    $mode   = dx_pay_cfg("danal-payment", "mode", "test");
    if (!$cpid || !$cpkey) {
        dx_payment_error_html("다날 CPID/KEY가 설정되지 않았습니다.");
        return;
    }
 
    $orderId   = $data["order_id"]    ?? dx_payment_order_id("DNL");
    $amount    = (int)($data["amount"] ?? 0);
    $timestamp = time();
    $signature = hash("sha256", $cpid . $orderId . $amount . $timestamp . $cpkey);
 
    $apiBase = $mode === "live"
        ? "https://api.danalpay.com/v2/payment"
        : "https://testapi.danalpay.com/v2/payment";
 
    // API 요청 및 결제창 URL 리다이렉트
    $body = json_encode([
        "CPID" => $cpid, "ORDERID" => $orderId, "AMOUNT" => $amount,
        "TIMESTAMP" => $timestamp, "SIGNATURE" => $signature,
    ]);
    $resp   = dx_payment_http_post($apiBase . "/ready", $body);
    $result = json_decode($resp, true);
 
    if (!isset($result["PAY_URL"])) {
        dx_payment_error_html($result["RETURNMSG"] ?? "오류");
        return;
    }
    echo '<script>window.location.href = ' . json_encode($result["PAY_URL"]) . ';</script>';
});


사례 9 — 관리자 대시보드 커스텀 위젯

 dx_admin_dashboard_widgets  훅을 사용하면 관리자 대시보드에 커스텀 통계 위젯을 추가할 수 있습니다.


예제 — 일일 매출•회원 현황 위젯

// plugins/dashboard-stats/plugin.php
 
dx_add_hook("dx_admin_dashboard_widgets", function() {
    $db = Database::getInstance();
 
    // 오늘 매출 집계
    $todaySales = $db->query(
        "SELECT IFNULL(SUM(amount), 0) as total FROM " .
        $db->table("orders") . " WHERE DATE(created_at) = CURDATE() AND status = 'paid'"
    );
    $sales = $todaySales[0]["total"] ?? 0;
 
    // 신규 회원 수
    $newMembers = $db->query(
        "SELECT COUNT(*) as cnt FROM " . $db->table("members") .
        " WHERE DATE(created_at) = CURDATE()"
    );
    $memberCnt = $newMembers[0]["cnt"] ?? 0;
 
    // 위젯 HTML 출력
    echo '<div class="adm_widget" style="background:#1E3A5F;color:#fff;padding:20px;border-radius:10px;">';
    echo '  <h3 style="margin:0 0 12px">📊 오늘의 현황</h3>';
    echo '  <div style="display:flex;gap:20px">';
    echo '    <div><div style="font-size:28px;font-weight:bold">' . number_format($sales) . '원</div>';
    echo '        <div style="font-size:12px;opacity:.8">오늘 매출</div></div>';
    echo '    <div><div style="font-size:28px;font-weight:bold">' . $memberCnt . '명</div>';
    echo '        <div style="font-size:12px;opacity:.8">신규 회원</div></div>';
    echo '  </div>';
    echo '</div>';
});


사례 10 — 개발•디버깅용 성능 측정 오버레이

예제 플러그인( plugins/example-plugin/plugin.php )에서 실제로 제공하는 디버그 오버레이입니다.  DX_DEBUG  상수가 true일 때만 활성화되어 프로덕션에서는 동작하지 않습니다.

예제 — 실행 시간 + 쿼리 카운트 오버레이 (실제 소스)

// plugins/example-plugin/plugin.php (실제 소스)
 
// 모든 페이지 하단에 실행 시간 표시 (개발용)
if (defined("DX_DEBUG") && DX_DEBUG) {
    dx_add_hook("dx_bottom", function($context) {
        $time = round((microtime(true) - DX_START_TIME) * 1000, 2);
        $db   = Database::getInstance();
        echo '<div style="position:fixed;bottom:10px;right:10px;',
             'background:rgba(0,0,0,.7);color:#fff;padding:6px 12px;',
             'border-radius:8px;font-size:11px;z-index:9999">';
        echo "⚡ {$time}ms | DB: " . $db->getQueryCount() . "쿼리";
        echo '</div>';
    }, 999);  // 우선순위 999 → 모든 훅 중 가장 마지막
}

💡 실무 팁
우선순위를 999로 설정하면 다른 모든 훅이 실행된 후 마지막에 실행됩니다. 성능 측정 오버레이처럼 "모든 처리가 끝난 뒤 결과를 보여줘야 하는" 경우에 적합합니다.
반대로, 우선순위를 1로 설정하면 가장 먼저 실행됩니다. 보안 검사, FOUC 방지 스크립트처럼 "다른 것보다 먼저 실행되어야 하는" 경우에 사용합니다.


사례 11 — 다크모드 FOUC 방지 (extend/top 활용)

실제 내장 파일  extend/top/01_darkmode_early.php 의 구현입니다. ob_start 콜백으로 출력 버퍼를 가로채어 <head> 직후에 인라인 스크립트를 삽입합니다. 이 방식은 훅( dx_add_hook )이 아닌 extend 폴더 기반이지만, extend 실행 후  dx_run_hook("dx_extend_top") 이 실행되어 상호 연동됩니다.
 
// extend/top/01_darkmode_early.php (핵심 발췌)
 
ob_start(function($buffer) {
    // 1) localStorage에서 다크모드 설정 읽기 (렌더링 전 즉시 적용)
    $earlyScript = '<script>(function(){'
        . 'try{'
        . 'var t=localStorage.getItem("dx-theme");'
        . 'var sys=window.matchMedia&&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>';
 
    // 2) FOUC 방지 인라인 CSS
    $earlyStyle = '<style>'
        . '.dx-dark-early body{background-color:#0f172a!important;color:#f1f5f9!important}'
        . '</style>';
 
    // 3) <head> 바로 뒤에 삽입
    return preg_replace('/<head([^>]*)>/i', '<head$1>' . $earlyScript . $earlyStyle, $buffer, 1);
});
 
// extend/bottom/02_darkmode_engine.php에서 엔진 JS 로드
// → visibility:hidden 해제, localStorage 동기화 완료


사례 12 — 방문자 통계 자동 기록 (extend/middle 활용)

실제 내장 파일  extend/middle/01_visit_tracker.php 의 핵심 로직입니다. register_shutdown_function을 사용하여 사용자에게 응답을 먼저 보낸 후 DB에 기록하므로 체감 속도에 영향이 없습니다.
 
// extend/middle/01_visit_tracker.php (핵심 발췌)
 
// 봇 트래픽 완전 차단 (visit_logs 테이블 용량 절약)
if ($_vt_isBot) return;
 
// DxCache로 순방문자 판단 (DB 조회 없음)
$cacheKey = "vt_u:" . date("Y-m-d") . ":" . substr(md5($ip . $browser), 0, 12);
if (!DxCache::get($cacheKey, false)) {
    DxCache::set($cacheKey, 1, strtotime("tomorrow") - time());
    $isUnique = true;
}
 
// ★ 핵심: 응답 완료 후 DB 기록 (사용자 체감 속도 향상)
register_shutdown_function(function() use ($data) {
    if (function_exists("fastcgi_finish_request")) {
        fastcgi_finish_request();  // PHP-FPM: 즉시 응답 완료
    }
    // UPSERT로 방문 집계 (쿼리 1회)
    $pdo->prepare("
        INSERT INTO dx_visits (visit_date, visit_count, unique_count)
        VALUES (?, 1, ?)
        ON DUPLICATE KEY UPDATE
            visit_count  = visit_count + 1,
            unique_count = unique_count + {$uniqueInc}
    ")->execute([$date, $uniqueInc]);
});


4. Hook vs Extend 비교

DXCMS에는 두 가지 확장 방법이 있습니다. 상황에 맞게 선택하세요.
 
Hook (dx_add_hook) Extend (extend/ 폴더)
코드에서 dx_add_hook()으로 등록 파일을 폴더에 넣으면 자동 실행
주로 플러그인(plugin.php) 내에서 사용 독립 PHP 파일로 작성
개발자에게 권장 (타입 안전, 우선순위 제어) 비개발자도 쉽게 사용 가능
특정 이벤트 포인트에 반응 3개 슬롯(top/middle/bottom)에 실행
플러그인 활성화 여부와 연계 가능 파일 존재 시 항상 실행
dx_remove_hook()으로 동적 해제 가능 파일 삭제/비활성화(.disabled)로 해제

선택 기준
• 특정 이벤트(로그인, 글 저장 등)에 반응해야 한다면 → Hook 사용
• 페이지 렌더링 전/중/후에 항상 실행되어야 한다면 → Extend 파일 사용
• 플러그인 내부에서는 → Hook이 표준
• 전역 설정, 미들웨어 성격의 코드 → Extend가 더 자연스러움


5. 심화 패턴


5.1 훅 제거 — 기존 동작 오버라이드

등록된 훅을 제거하여 기본 동작을 비활성화하거나 교체할 수 있습니다.
 
// 방법 1: 특정 콜백만 제거
function my_header_callback() { /* ... */ }
dx_add_hook("dx_head", "my_header_callback");
// ... 나중에 제거
dx_remove_hook("dx_head", "my_header_callback");
 
// 방법 2: 훅 이름의 모든 콜백 제거
dx_remove_hook("dx_head");  // dx_head에 등록된 모든 콜백 제거
 
// 방법 3: HookManager 직접 사용
$hm = HookManager::getInstance();
$hm->remove("dx_head", null);  // 전체 제거


5.2 Filter 훅으로 컨텐츠 변환

Filter 훅( dx_apply_filter )은 값을 변환하여 반환합니다. 여러 개의 필터가 체인으로 연결되어 순서대로 값을 변환합니다.
 
// ── 커스텀 필터 포인트 정의 (예: 게시글 출력 전) ──
 
// 코어 코드에서 필터 적용
$content = dx_apply_filter("dx_post_content", $rawContent, ["post_id" => $postId]);
 
// 플러그인에서 필터 등록
 
// 필터 1: HTML 이스케이프 (우선순위 5)
dx_add_filter("dx_post_content", function($content, $args) {
    // $content를 받아 변환 후 반환해야 함
    return nl2br(htmlspecialchars($content, ENT_QUOTES));
}, 5);
 
// 필터 2: URL 자동 링크화 (우선순위 10)
dx_add_filter("dx_post_content", function($content, $args) {
    return preg_replace(
        '/(https?:\/\/[\S]+)/',
        '<a href="$1" target="_blank">$1</a>',
        $content
    );
}, 10);
 
// 필터 3: 금지어 마스킹 (우선순위 20)
dx_add_filter("dx_post_content", function($content, $args) {
    return str_replace(["욕설1", "욕설2"], ["***", "***"], $content);
}, 20);
 
// 실행 결과: 원본 → 이스케이프 → 링크화 → 금지어 마스킹 순으로 처리


5.3 참조(&) 전달로 데이터 수정

일부 훅은 인자를 참조( &$data )로 전달합니다.  dx_board_before_save ,  dx_board_list_context  등이 이 방식을 사용합니다.
 
// $args["data"]가 참조(&)로 전달된 경우
dx_add_hook("dx_board_before_save", function($args) {
    // $args["data"]는 실제 저장될 데이터의 참조
    $data = &$args["data"];
 
    // 직접 수정 → 저장 로직에 자동 반영
    $data["title"]   = trim($data["title"]);
    $data["view_count"] = 0;  // 신규 글 조회수 강제 0
 
    // 주의: &$args["data"]가 아닌 $args["data"]로 받으면 수정이 반영되지 않음!
    // 잘못된 예: $data = $args["data"];  ← 복사본이라 수정이 무효
});


6. 훅 디버깅


6.1 실행된 훅 목록 확인

// HookManager에서 실행된 훅 목록 조회
$hm = HookManager::getInstance();
 
// 실행된 훅 이름 목록
$executed = $hm->getExecuted();
 
// 현재 등록된 훅 이름 목록
$registered = $hm->getAll();
 
// 특정 훅에 등록된 콜백 수
$count = $hm->count("dx_after_login");  // 예: 2
 
// 특정 훅 등록 여부 확인
$exists = dx_has_hook("dx_after_login");  // true/false
 
// 디버그 출력 (개발 환경에서만)
if (defined("DX_DEBUG") && DX_DEBUG) {
    error_log("Executed hooks: " . implode(", ", $executed));
}


6.2 훅 추적 유틸리티

// 모든 훅 실행을 로그에 기록하는 디버그 래퍼
// extend/top/99_hook_tracer.php (개발 환경에서만 활성화)
 
if (!defined("DX_DEBUG") || !DX_DEBUG) return;
 
// dx_run_hook을 감싸는 방법은 없으므로,
// 주요 훅 포인트에 로거를 선제 등록 (우선순위 0)
$hooksToTrace = ["dx_head", "dx_top", "dx_middle", "dx_bottom",
                  "dx_after_login", "dx_after_register", "dx_board_before_save"];
 
foreach ($hooksToTrace as $hookName) {
    dx_add_hook($hookName, function($args) use ($hookName) {
        $time = round((microtime(true) - DX_START_TIME) * 1000, 2);
        error_log("[HOOK TRACE] {$hookName} fired at {$time}ms");
    }, 0);  // 우선순위 0 → 가장 먼저
}


7. 베스트 프랙티스


7.1 성능 고려사항

1. 무거운 작업은 shutdown_function으로 비동기 처리 — DB INSERT, 외부 API 호출 등은 register_shutdown_function을 사용하여 응답 후 처리합니다.
2. 캐시 적극 활용 — DxCache를 사용하여 중복 DB 조회를 방지합니다. 순방문자 판단, 설정값 캐싱 등에 활용합니다.
3. 봇 트래픽 필터링 — 방문자 통계 등 비필수 처리는 User-Agent 기반 봇 판별 후 조기 return합니다.
4. 우선순위 설계 — 보안 검사(1~5), 일반 기능(10~50), 분석/로깅(100~999)으로 계층화합니다.


7.2 안전성 주의사항

5. 반드시 인자 존재 확인 — $args["key"] ?? null 패턴으로 undefined index 에러를 방지합니다.
6. 사용자 입력 검증 — before_save 훅에서 데이터를 수정할 때 반드시 sanitize를 거칩니다.
7. 무한 루프 방지 — 훅 내에서 같은 훅을 트리거하는 동작을 피합니다.
8. 플러그인 활성화 확인 — 결제/에디터 등 단일 선택 플러그인은 dx_active_plugin() 확인 후 훅을 등록합니다.


7.3 코드 구조 권장 사항

// ✅ 권장: 익명 함수 사용 (간결)
dx_add_hook("dx_after_login", function($args) {
    // 처리 로직
}, 10);
 
// ✅ 권장: 네임드 함수 사용 (제거 가능)
function my_plugin_after_login($args) { /* ... */ }
dx_add_hook("dx_after_login", "my_plugin_after_login", 10);
// 나중에 제거 가능:
dx_remove_hook("dx_after_login", "my_plugin_after_login");
 
// ✅ 권장: 클래스 메서드 사용 (대형 플러그인)
class MyPlugin {
    public function onAfterLogin($args) { /* ... */ }
    public function register() {
        dx_add_hook("dx_after_login", [$this, "onAfterLogin"], 10);
    }
}
$plugin = new MyPlugin();
$plugin->register();

댓글0

로그인 후 댓글을 작성할 수 있습니다.
1. DX 철학 / 개념 왜 DXCMS를 만들었는가 2026.04.20 1. DX 철학 / 개념 DXCMS란 무엇인가 2026.04.20 DXCMS 활용 (CMS) DXCMS 날코딩•막코딩 완전 허용 2026.04.12
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록