애드온 기초

애드온 기초

라이믹스 애드온 개발의 기초 개념과 구조를 학습합니다.

애드온이란?

XE구조의 비밀 그 네번째 - 애드온은 살아있어

애드온은 라이믹스의 핵심 확장 메커니즘으로, 시스템의 다양한 지점에서 실행되어 기능을 확장하거나 수정할 수 있습니다.

<?php
// 애드온의 기본 구조
if(!defined("__XE__")) exit();

/**
 * 애드온은 전역적으로 실행되는 PHP 스크립트입니다.
 * 모든 페이지 요청에서 실행되므로 성능을 고려해야 합니다.
 */

switch($called_position) {
    case 'before_module_proc':
        // 모듈 실행 전
        break;

    case 'after_module_proc':
        // 모듈 실행 후
        break;

    case 'before_display_content':
        // 화면 출력 전
        break;
}
?>

애드온 생명주기

실행 지점(Called Position)

<?php
// 애드온 실행 지점별 상세 설명

/**
 * 1. before_module_proc
 * - 모듈이 실행되기 전에 호출
 * - 요청 데이터 수정, 권한 검사 등에 사용
 * - Context 데이터를 수정할 수 있음
 */
if($called_position == 'before_module_proc') {
    $logged_info = Context::get('logged_info');

    // 특정 모듈 접근 제한
    if($module == 'admin' && !$logged_info->is_admin) {
        return new BaseObject(-1, '관리자만 접근 가능합니다.');
    }

    // 요청 데이터 로깅
    $request_log = array(
        'module' => $module,
        'act' => $act,
        'mid' => $mid,
        'ip' => $_SERVER['REMOTE_ADDR'],
        'timestamp' => time()
    );

    // 로그 저장
    $this->saveRequestLog($request_log);
}

/**
 * 2. after_module_proc
 * - 모듈 실행 후, 템플릿 처리 전에 호출
 * - 모듈 결과 데이터 수정
 * - 추가 데이터 Context에 설정
 */
if($called_position == 'after_module_proc') {
    // 게시판 목록에 추가 정보 설정
    if($module == 'board' && $act == 'dispBoardContent') {
        $document_list = Context::get('document_list');

        if($document_list) {
            foreach($document_list as $document) {
                // 문서별 추가 정보 설정
                $extra_info = $this->getDocumentExtraInfo($document->document_srl);
                $document->addExtraVars($extra_info);
            }
        }
    }

    // 사용자 정보 확장
    if($logged_info) {
        $user_stats = $this->getUserStatistics($logged_info->member_srl);
        Context::set('user_stats', $user_stats);
    }
}

/**
 * 3. before_display_content
 * - HTML 출력 직전에 호출
 * - 최종 HTML 수정
 * - JavaScript/CSS 추가
 */
if($called_position == 'before_display_content') {
    // 모바일 감지 스크립트 추가
    if(Mobile::isMobile()) {
        $mobile_script = '<script src="/addons/my_addon/mobile.js"></script>';
        Context::addHtmlHeader($mobile_script);
    }

    // 페이지 로딩 시간 측정
    $load_time = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
    $debug_info = sprintf('<!-- Page loaded in %.3f seconds -->', $load_time);
    Context::addHtmlFooter($debug_info);
}
?>

애드온 파일 구조

기본 파일 구성

addons/
├── my_addon/
│   ├── my_addon.addon.php     # 메인 애드온 파일 (필수)
│   ├── conf/
│   │   └── info.xml           # 애드온 정보 파일 (필수)
│   ├── schemas/
│   │   └── install.xml        # 데이터베이스 스키마
│   ├── queries/
│   │   ├── insertData.xml     # XML 쿼리 파일들
│   │   └── selectData.xml
│   ├── lang/
│   │   ├── ko.lang.php        # 다국어 파일
│   │   └── en.lang.php
│   ├── js/
│   │   └── script.js          # JavaScript 파일
│   ├── css/
│   │   └── style.css          # CSS 파일
│   └── tpl/
│       └── config.html        # 설정 템플릿

info.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<addon version="0.2">
    <title xml:lang="ko">나의 애드온</title>
    <title xml:lang="en">My Addon</title>

    <description xml:lang="ko">라이믹스용 커스텀 애드온입니다.</description>
    <description xml:lang="en">Custom addon for Rhymix.</description>

    <version>1.0.0</version>
    <date>2024-01-01</date>

    <author email_address="author@example.com" link="https://example.com">
        <name xml:lang="ko">개발자</name>
        <name xml:lang="en">Developer</name>
    </author>

    <!-- 의존성 -->    
    <dependency>
        <module name="member" />
        <addon name="mobile" />
    </dependency>

    <!-- 설정 페이지 -->
    <extra_vars>
        <var name="enable_logging" type="select" default="Y">
            <title xml:lang="ko">로깅 활성화</title>
            <title xml:lang="en">Enable Logging</title>
            <options value="Y">Y</options>
            <options value="N">N</options>
        </var>

        <var name="max_log_entries" type="text" default="1000">
            <title xml:lang="ko">최대 로그 개수</title>
            <title xml:lang="en">Max Log Entries</title>
        </var>

        <var name="excluded_modules" type="textarea">
            <title xml:lang="ko">제외할 모듈 (콤마로 구분)</title>
            <title xml:lang="en">Excluded Modules (comma separated)</title>
        </var>
    </extra_vars>
</addon>

애드온 설정 관리

설정값 읽기/쓰기

<?php
// 애드온 설정 관리 클래스
class MyAddonConfig
{
    private $config;
    private $addon_name = 'my_addon';

    public function __construct()
    {
        $this->loadConfig();
    }

    // 설정 로드
    private function loadConfig()
    {
        $oAddonModel = getModel('addon');
        $this->config = $oAddonModel->getAddonConfig($this->addon_name);

        // 기본값 설정
        if(!$this->config) {
            $this->config = new stdClass();
        }

        $this->setDefaults();
    }

    // 기본값 설정
    private function setDefaults()
    {
        $defaults = array(
            'enable_logging' => 'Y',
            'max_log_entries' => 1000,
            'excluded_modules' => ''
        );

        foreach($defaults as $key => $value) {
            if(!isset($this->config->{$key})) {
                $this->config->{$key} = $value;
            }
        }
    }

    // 설정값 가져오기
    public function get($key, $default = null)
    {
        return isset($this->config->{$key}) ? $this->config->{$key} : $default;
    }

    // 설정값 설정
    public function set($key, $value)
    {
        $this->config->{$key} = $value;
    }

    // 설정 저장
    public function save()
    {
        $oAddonController = getController('addon');
        return $oAddonController->setAddonConfig($this->addon_name, $this->config);
    }

    // 로깅 활성화 여부
    public function isLoggingEnabled()
    {
        return $this->get('enable_logging') === 'Y';
    }

    // 제외된 모듈 목록
    public function getExcludedModules()
    {
        $excluded = $this->get('excluded_modules', '');
        return array_filter(array_map('trim', explode(',', $excluded)));
    }

    // 모듈이 제외되었는지 확인
    public function isModuleExcluded($module)
    {
        return in_array($module, $this->getExcludedModules());
    }
}

// 사용 예제
$config = new MyAddonConfig();

if($config->isLoggingEnabled() && !$config->isModuleExcluded($module)) {
    // 로깅 실행
    $this->writeLog($request_data);
}
?>

데이터베이스 사용

애드온 전용 테이블 생성

<!-- schemas/install.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<schema version="0.3">
    <table name="addon_logs">
        <column name="log_srl" type="number" size="11" notnull="notnull" primary_key="primary_key" />
        <column name="module" type="varchar" size="250" index="idx_module" />
        <column name="act" type="varchar" size="250" />
        <column name="mid" type="varchar" size="250" index="idx_mid" />
        <column name="member_srl" type="number" size="11" index="idx_member" />
        <column name="ip_address" type="varchar" size="45" />
        <column name="user_agent" type="text" />
        <column name="request_data" type="text" />
        <column name="regdate" type="date" index="idx_regdate" />
    </table>

    <table name="addon_settings">
        <column name="setting_srl" type="number" size="11" notnull="notnull" primary_key="primary_key" />
        <column name="addon_name" type="varchar" size="250" unique="unique" />
        <column name="setting_key" type="varchar" size="250" />
        <column name="setting_value" type="text" />
        <column name="regdate" type="date" />
    </table>
</schema>

데이터베이스 작업

<?php
// 데이터베이스 작업 예제
class MyAddonDatabase
{
    // 로그 저장
    public function saveLog($data)
    {
        $args = new stdClass();
        $args->module = $data['module'];
        $args->act = $data['act'];
        $args->mid = $data['mid'];
        $args->member_srl = $data['member_srl'] ?: 0;
        $args->ip_address = $data['ip_address'];
        $args->user_agent = $data['user_agent'];
        $args->request_data = serialize($data['request_data']);
        $args->regdate = date('YmdHis');

        return executeQuery('my_addon.insertLog', $args);
    }

    // 로그 조회
    public function getLogs($conditions = array())
    {
        $args = new stdClass();

        // 조건 설정
        if(isset($conditions['module'])) {
            $args->module = $conditions['module'];
        }

        if(isset($conditions['start_date'])) {
            $args->start_date = $conditions['start_date'];
        }

        if(isset($conditions['end_date'])) {
            $args->end_date = $conditions['end_date'];
        }

        // 페이징
        $args->list_count = $conditions['list_count'] ?: 20;
        $args->page = $conditions['page'] ?: 1;

        return executeQuery('my_addon.getLogList', $args);
    }

    // 오래된 로그 정리
    public function cleanupOldLogs($days = 30)
    {
        $args = new stdClass();
        $args->cutoff_date = date('YmdHis', strtotime("-{$days} days"));

        return executeQuery('my_addon.deleteOldLogs', $args);
    }

    // 통계 조회
    public function getLogStatistics($period = 'week')
    {
        $args = new stdClass();

        switch($period) {
            case 'today':
                $args->start_date = date('Ymd') . '000000';
                $args->end_date = date('Ymd') . '235959';
                break;

            case 'week':
                $args->start_date = date('Ymd', strtotime('-7 days')) . '000000';
                $args->end_date = date('Ymd') . '235959';
                break;

            case 'month':
                $args->start_date = date('Ymd', strtotime('-30 days')) . '000000';
                $args->end_date = date('Ymd') . '235959';
                break;
        }

        return executeQuery('my_addon.getLogStatistics', $args);
    }
}
?>

성능 최적화

효율적인 애드온 작성

<?php
// 성능을 고려한 애드온 작성
class OptimizedAddon
{
    private static $initialized = false;
    private static $config = null;

    // 초기화는 한 번만
    public static function initialize()
    {
        if(self::$initialized) return;

        self::$config = self::loadConfig();
        self::$initialized = true;
    }

    // 설정 캐싱
    private static function loadConfig()
    {
        $cache_key = 'my_addon_config';
        $config = Rhymix\Framework\Cache::get($cache_key);

        if($config === null) {
            $oAddonModel = getModel('addon');
            $config = $oAddonModel->getAddonConfig('my_addon');

            // 1시간 캐싱
            Rhymix\Framework\Cache::set($cache_key, $config, 3600);
        }

        return $config;
    }

    // 조건부 실행
    public static function shouldExecute()
    {
        // 관리자 페이지에서는 실행하지 않음
        if($module == 'admin') {
            return false;
        }

        // 특정 액션에서만 실행
        $allowed_acts = array('dispBoardContent', 'dispBoardWrite');
        if(!in_array($act, $allowed_acts)) {
            return false;
        }

        // 설정에서 비활성화된 경우
        if(self::$config->enabled !== 'Y') {
            return false;
        }

        return true;
    }

    // 배치 처리
    public static function processBatch($data_list)
    {
        $batch_size = 100;
        $batches = array_chunk($data_list, $batch_size);

        foreach($batches as $batch) {
            self::processBatchItems($batch);

            // 메모리 정리
            if(memory_get_usage() > 50 * 1024 * 1024) { // 50MB
                gc_collect_cycles();
            }
        }
    }

    // 지연 로딩
    public static function getLazyData($key)
    {
        static $cache = array();

        if(!isset($cache[$key])) {
            $cache[$key] = self::loadExpensiveData($key);
        }

        return $cache[$key];
    }
}

// 애드온 메인 로직
OptimizedAddon::initialize();

if(!OptimizedAddon::shouldExecute()) {
    return;
}

// 실제 애드온 로직 실행
switch($called_position) {
    case 'before_module_proc':
        // 최소한의 작업만
        break;

    case 'after_module_proc':
        // 필요한 경우에만 실행
        if($act == 'dispBoardContent') {
            OptimizedAddon::processDocumentList();
        }
        break;
}
?>

디버깅과 로깅

개발용 디버깅 도구

<?php
// 애드온 디버깅 도구
class AddonDebugger
{
    private static $debug_mode = false;
    private static $logs = array();

    public static function enable()
    {
        self::$debug_mode = true;
    }

    public static function log($message, $data = null)
    {
        if(!self::$debug_mode) return;

        self::$logs[] = array(
            'time' => microtime(true),
            'message' => $message,
            'data' => $data,
            'memory' => memory_get_usage(),
            'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)
        );
    }

    public static function dump()
    {
        if(!self::$debug_mode || !self::$logs) return;

        $html = '<div style="background:#000;color:#0f0;padding:10px;font-family:monospace;">';
        $html .= '<h3>Addon Debug Log</h3>';

        foreach(self::$logs as $log) {
            $html .= sprintf(
                '<div><strong>%.4f</strong> [%s] %s</div>',
                $log['time'],
                number_format($log['memory']),
                htmlspecialchars($log['message'])
            );

            if($log['data']) {
                $html .= '<pre>' . htmlspecialchars(print_r($log['data'], true)) . '</pre>';
            }
        }

        $html .= '</div>';

        Context::addHtmlFooter($html);
    }
}

// 개발 환경에서만 디버깅 활성화
if(__DEBUG__ || $_SERVER['SERVER_NAME'] == 'localhost') {
    AddonDebugger::enable();
}

AddonDebugger::log('Addon started', array('module' => $module, 'act' => $act));

// ... 애드온 로직 ...

AddonDebugger::log('Addon finished');
AddonDebugger::dump();
?>

모범 사례

  1. 성능: 불필요한 실행 피하기
  2. 메모리: 큰 데이터 처리 시 배치 처리
  3. 캐싱: 반복 조회 데이터 캐싱
  4. 에러 처리: 적절한 예외 처리
  5. 보안: 사용자 입력 검증

다음 단계

애드온 기초를 이해했다면, 데이터베이스 접근을 학습하세요.