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

Hook 시스템 활용 사례

A Administrator
2026.04.21 00:59(수정됨) 90 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

로그인 후 댓글을 작성할 수 있습니다.
3.7 Hook 시스템 Hook 시스템 활용 사례 2026.04.21 3.7 Hook 시스템 실행 타이밍 2026.04.21 3.7 Hook 시스템 Hook 개념 2026.04.21
30
전체 회원
269
전체 게시글
144
전체 댓글
181
오늘 방문
28,530
전체 방문
1
현재 접속
인기글 7일 이내
최신글
최신댓글
목록