애드온 기초¶
라이믹스 애드온 개발의 기초 개념과 구조를 학습합니다.
애드온이란?¶
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();
?>
모범 사례¶
- 성능: 불필요한 실행 피하기
- 메모리: 큰 데이터 처리 시 배치 처리
- 캐싱: 반복 조회 데이터 캐싱
- 에러 처리: 적절한 예외 처리
- 보안: 사용자 입력 검증
다음 단계¶
애드온 기초를 이해했다면, 데이터베이스 접근을 학습하세요.