본문

예제를 통해 쉽게 읽어보는 XE모듈개발 시작하기

xpressEngine에서의 모듈을 개발하기 위해 여러 예제를 보았지만 쉽게 와닿는것이 없어 XE위키에 있던, 가장 간략해 보이는 모듈인 북마크 모듈(제목과 설명을 단순히 저장하고 불러오는)을 분석해 보기로 하였다. 이 예제는 XE Core부분이 어떻게 사용되고 있느냐 하는데에 중점이 맞춰져 있으므로 실제로 사용되는 XE모듈과는 비교하면 많은부분이 구현이 되지 않은 상태이다. 하지만 앞에서 말했다 시피, 모듈개발에서 가장 기초적인 부분을 공부하는데에 괜찮은 교보재로 느껴져 이 글을 작성한다. 원 글은 이곳(http://xe.xpressengine.net/wiki/18180602)에서 확인할 수 있고, 마찬가지로 이 글의 원소스를 다운받을 수 있다. 이 글은 PHP MVC모델을 이미 알고 있는 가운데 처음으로 모듈개발을 착수하려는 사람들에게 모듈개발의 흐름을 알기쉽게함에 목적이 있다.

XE
모듈개발은 Model, View, Controller로 나뉘어 지는 MVC모델을 기반으로 한다여기서 잠시 MVC모델에 대해 말해보자면이는 개발상에서 View--Controller--Model 으로 서로를 구분되는 개발형태를 말한다. View에서는 사용자에게 실제로 보이는 부분을 구현하고, Model은 데이터를 관리하는 부분, 그리고 Controller Model에서 데이터를 끌어와 View에 보여줄 자료를 처리하거나, View쪽에서 사용자의 입력을 받아 Model쪽에 저장할 데이터를 보내는 작업을 한다동일한 개념은 아니지만 php소스로 간략히 나타내보자면 다음과 같다. main.php template.htm라는 두개의 파일을 생각해 보자.

* main.php

<?php
If(isset($_GET[mode])) setBirthYear($_GET[year]);
Function getAge(){
 Return $currentYear-$_SESSION[birthYear];
}
Function setBirthYear($year){
 $_SESSION[birthYear]=$year;
}
?>


* template.htm

<?include(main.php);?>
<?=
‘당신은.getAge().’년동안 산소를 섭취하셨습니다. 잘못되셨으면  아래 폼을 기입하세요’;?>
<form><input type=hidden name=mode value=input /><input type=text name=year /><input type=submit /></form>


이 소스는 두개의 함수와 birthYear라는 세션변수, 그리고 html소스로 구성되어있다. 우선 아래의 html소스는 사용자에게 보여지는 부분이므로  MVC모델에서 View라는 측면으로 이해될 수 있다.   그리고 getAge()라는 함수는 내재된 데이터들을 가공하여 사용자가 필요한 정보를 처리하는 부분이므로 Controller로 이해될 수 있을것이다. 그리고 setBirthYear()라는 함수는 (비록 이 소스에서는 View쪽에서 바로 처리되는것으로 이해될 수 있겠지만) HTML이라는 View에서 PHP라는 Controller를 거쳐 Session이라는 Model으로 다다른다는 점에서 Model적 부분이 강하다고 (억지로)이해될 수 있다.

XE
 흐름을 자세히 보자면, MVC가 변형된 모델을 사용한다는것을 알 수 있다. 이 글을 끝까지 읽은 후에 다시 이를 보면 어떤것인지 흐름이 잡힐것이다. XE를 사용하다 보면 기존에 알던 단어와 의미가 충돌하는 것을 볼 수 있는데 이해하려는것보다 그냥 외우는게 좋을 것 같다. 아래에 임의로 아래 소스설명에 맞추어 MVC 모델을 세분화 해봤는데 만족스럽지는 않다.

Template(HTML) -- View(PHP) -- Filter(XML) -- Controller(PHP) -
Scheme(XML)
             -----VIEW-----------------------CONTROLLER---------MODEL----

그럼 아래 소스를 보면서 이것이 어떤 의미인지 확인해보자. 소스인 Bookmark를 다운받아 압축을 해제하면 바로 보이는 3개의 php파일이 있는데 이것을 먼저 분석하자. XE에서 모듈을 개발하자면 가장 상위클래스부터 해서 아래로 내려가는 하향식 방향으로 개발이 된다이 글도 가장 윗부분부터 아래쪽으로 내려가는 방향을 취할것이다.

우선 가장 상위 클래스를 PHP로 만들고, 이를 바탕으로 View Controller로 된  PHP파일을 만든다여기에서는 Model부분이 빠져있는데, 이것은 DB를 접근하기 위해 PHP를 직접 사용하는 부분이 없이 XML으로 된 Query만을 사용하기 때문에 이 부분은은 일단 제외한다그럼 다음과 같은 파일을 작성해야 한다는 것을 알 수 있다. bookmark.class.php / bookmark.controller.php / bookmark.view.php bookmark라는 클래스 이름으로 다음과 같은 뼈대를 만드는 것이다.

bookmark
라는 가장 상위클래스를 만들기 위해 ModuleObject를 상속하는 bookmark.class.php라는 파일을 만든다.  checkUpdate()라는 함수는 ModuleObject에서 기본적으로 호출되는 함수이므로 비워놓아서라도 반드시 추가해 준다. 이렇게 하면 가장 기초가 되는 bookmark라는 클래스가 만들어 졌다.

* bookmark.class.php

<?PHP
    class bookmark extends ModuleObject     {
        function checkUpdate()   {
            return false;
        }
    }
?>


이 다음으로 View를 구성하기 위해bookmark.view.php를 작성한다. 역시나 가장 기본이 되는 bookmark클래스를 상속하여  view를 만든다.

* bookmrk.view.php

<?PHP
    class bookmarkView extends bookmark    {
        function dispBookmarkList()        {
            $args->page = Context::get('page'); //현재의 (템플릿에서 사용되고 있는)page라는 값을 불러와서 args라는 변수아래의 page에 저장한다.
            $output = executeQueryArray("bookmark.getBookmarkList", $args); //bookmark.getBookmarkList라는 Model(아까 말했듯이 XML상에 저장되어있는 쿼리를 나타낸다)을 실행시킨다 이때 $args라는 변수를 인자로 넘겨서 쿼리를 실행하고, 그에대한 값을 $output이라는 값에 넣어준다.
            if(!$output->data) $output->data = array(); //만약에 아무런 값도 얻지 못했다면 데이터는 그냥 비어있는 배열로 넣어준다. (템플릿에서 foreach를 사용하는데, data가 없으면 오류가 발생하므로 빈배열이라도 지정해준다.)
            Context::set('bookmark_list', $output->data); //$output->data의 값을(템플릿에서 사용되는)bookmark_list라는 변수에 저장한다.
            Context::set('total_count', $output->total_count);
            Context::set('total_page', $output->total_page);
            Context::set('page', $output->page);
            Context::set('page_navigation', $output->page_navigation);
            $this->setTemplatePath($this->module_path.'tpl'); //사용자에게 보여지게 될 템플릿이 위치한 폴더를 지정해준다. 여기에서는 현재 모듈이 위치한 경로내의 tpl이라는 폴더를 지정한것이다.
            $this->setTemplateFile('bookmark_list'); //위에서 지정한 폴더안에 있는 bookmark_list.html을 템플릿으로 지정한다.
        }
    }
?>


그러면 bookmark_list.html에는 어떻게 템플릿이 설정되어 있을까? 다음을 확인해 본다.

* tpl/bookmark_list.html

<!--%import("filter/insert_bookmark.xml")--> //filter라는것은 템플릿에서 Controller에게 자료를 전달하는데에 있어 효율적으로 관여하도록 하는 장치이다.
<!--%import("js/bookmark.js")--> //
위의 filter에서 Controller에게 자료를 전달하면 Controller에서 다시 View쪽으로 처리결과를 알려준다(Callback) 이를 처리하는 함수를 역시 Filter에서 지정해 놓았는데, 이에 대한 함수구현을 bookmark.js 이쪽에다가 저장해 놓았기 떄문에 import 하는것이다. 참고로 import  위치가 어디있든 XE전처리기에 의해서 자동으로 적절한 위치에 위치시켜준다.

<table>
   
<col width="200" />
   
<col width="500" />
   
<tr> <th>제목</th> <th> 설명 </th> </tr>
   
<!--@foreach($bookmark_list as $val)--> //위의 View에서 Context::set()함수로bookmark_list라는 변수를 설정해 놓았다. [Context::set('bookmark_list', $output->data);] 이를 @end라는 scope까지 하여 반복한다.
    <tr> <td align="center"><a href="{$val->link}">{$val->title}</a>  //$output->data view에서의executeQueryArray의 결과값을 저장했는데, 사실 DB의 테이블 구조에 link, title, description 컬럼이 있었다. 그래서 다음과 같이 사용할 수 있는것이다.
         <td align="center">{$val->description}</td></tr>
   
<!--@end-->

    <tr> <td colspan="2" align="center">
   
<a href="{getUrl('page','','module_srl','')}" class="goToFirst"><img src="../../admin/tpl/images/bottomGotoFirst.gif" alt="{$lang->first_page}" width="7" height="5" /></a>
   
<!--@while($page_no = $page_navigation->getNextPage())-->//페이지 네비게이션 기능을 사용한다. 위에서부터 계속 페이지네비게이션에 필요한 사항들을 사용하였지만 이 게시물에서는 다른 설명을 하지 않는다
        <!--@if($page == $page_no)-->
            <span class="current">{$page_no}</span>
       
<!--@else-->
            <a href="{getUrl('page',$page_no,'module_srl','')}">{$page_no}</a>
       
<!--@end-->
    <!--@end-->
    <a href="{getUrl('page',$page_navigation->last_page,'module_srl','')}" class="goToLast"><img src="../../admin/tpl/images/bottomGotoLast.gif" alt="{$lang->last_page}" width="7" height="5" /></a>
   
</td></tr>

   
<tr> <td colspan="2" align="center">
   
<form action="./" method="POST" onsubmit="return procFilter(this, insert_bookmark);"> //사용자가 submit을 하면 procFilter가 호출된다. 이는 위에 import했던 filter를 호출하는것으로, insert_bookmark라는 필터에 폼을 첨부하여 호출한다.
       
<label>link<input type="text" name="link" /></label>&nbsp;
       
<label>title<input type="text" name="title" /></label>&nbsp;
        <label>description<input type="text" name="description" /></label>&nbsp;
        <input type="submit" value="&#51077;&#47141;"/>
   
</td></tr>
</table>


그럼 필터란 무엇일까? 다음을 확인해보자.

* tpl/filter/insert_bookmark.xml

<filter name="insert_bookmark" module="bookmark" act="procBookmarkInsertBookmark" confirm_msg_code="confirm_submit"> // procFilter(this, insert_bookmark)에 의해 insert_bookmark필터가 호출된다. 이는 bookmark모듈에 속한 procBookmarkInsertBookmark라는 함수를 호출하는데, 이는 Controller에 위치해 었다. 이를 어떻게 찾았냐 하면 conf/module.xml내의 있는 actionname으로 찾을 수 있다
   
<form />
   
<parameter />
   
<response callback_func="completeInsertBookmark">
       
<tag name="error" />
       
<tag name="message" />
   
</response>
</filter>

 

그럼 결국 Controller까지 와보자.

* bookmark.controller.php

<?PHP
    class bookmarkController extends bookmark    {
        function procBookmarkInsertBookmark()        {
            $obj = Context::getRequestVars(); //form에 있는 정보를 $obj에 저장한다..
            $obj->bookmark_srl = getNextSequence(); //getNextSequence  모델테이블내의 고유한 sequential number를 얻어낸다.
            executeQuery("bookmark.insertBookmark", $obj);  //입력정보를 가지고 insertBookmark라는 쿼리를 호출한다
        }
    }
?>


아까전부터 쿼리를 호출하는데 이 쿼리들은 어디서 나오는 것일까? 답은 모듈 폴더내의 queries 폴더에서 찾을 수 있다. insertBookmark.xml을 살펴보면 다음과 같다.

*queries/insertBookmark.xml

<query id="insertBookmark" action="insert">
   
<tables>
        <table name="bookmark" /> //테이블 이름을 정한다
   
</tables>
    <columns>
        <column name="bookmark_srl" var="bookmark_srl" notnull="notnull" />
       
<column name="title" var="title" notnull="notnull" /> //title이라는 폼네임을 근거하여 title이라는 column에 저장한다.
       
<column name="link" var="link" notnull="notnull" />
       
<column name="description" var="description" />
   
</columns>
</query>

그럼 최종적으로(이걸 문서의 맨 처음에 놓을까 맨 나중에 놓을까 고민을 했었다),,, 모듈의 진입점은(가장 처음에 시작되는 함수)는 어떻게 정의가 될까? 이는 conf/module.xml이라는 파일에 정의가 되어있는데, 이 파일의 역할은 MVC 모델간에 있어서 외부에서 보이는(마치 Public선언이라 할까?)함수를 정의한것이다. 아래에서 dispBookmarkList라는 action을 보면 index라는 항목이 설정되어있는데, 이는 이 모듈이 호출되면 가장 기본적으로 호출될 함수라는 것을 의미한다.

* conf/module.xml

<?xml version="1.0" encoding="utf-8"?>
<module>
    <grants />
   
<actions>
    <action name="dispBookmarkList" type="view" index="true" standalone="true" />
   
<action name="procBookmarkInsertBookmark" type="controller" standalone="true" />
   
</actions>
</module>

결국 위에 죽 이어온것같이 흐름이 진행된다. 물론 프로그램의 View적인 관점부터 내려왔기 때문에 스키마 설정이라든가 하는 사항등이 빠져있지만, 소설과 같이 자연스레 흘려읽을 수 있도록 하기위하여 세세한것은 넣지 않았다. 

사실 개인적으로 MVC 모델은 GUI IDE환경에서나 어울린다고 생각한다. 이때 개발자는 단지 화면을 끄적여서 VIEW를 쉽게 만들고, View내의 객체를 선택하여 그에 맞는 Controller를 제작(이때 당연히 View와 분리가 되어있으니 개발이 쉽다) 하고 결국에 데이터와의 효율적인 통신을 위해서 Model을 제작하는것이다. 이는 충분히 효율적이다. 하지만 PHP의 경우는 이와는 다른 경우라고 생각한다. 이전에 JAVA로 된 MVC기반 프로젝트를 PHP로 마이그레이션하는 작업했었을때 느꼈던것은, 역시 작업환경과 언어속성이 언어마다 다르구나... 하는것이었다. 내가 보기에는 XE가 이렇게 진행되어가는것은 이렇게 교육받고 자란 세대가 맹목적으로 MVC를 따르고 이것이 진리라고 생각하여 진행하고 있는건 아닌가, 아님 다른 선형적인 방법보다 입체적인 MVC 개발방식을 따라 있어보이려는?? 효과를 얻으려는것은 아닌가 하는 생각이 든다. 물론 코드량도 늘리고!(경험상). 뭐 이렇게 말하면 다른 사람의 성과를 공으로 얻어쓰는 주제에 말이 많다고 할 수 있겠는데, 그래도 나는 XE가 확장성과 범용성이 넓다는 점은 인정하니까 괜찮다? 그리고 아이러닉하게 이런 장점은 위의 단점에 기인한다!!

기존에 생각만 하다가 오늘 처음 XE 모듈을 분석한것이라 깊이도 없고, 어디서부터 시작할지 잘 몰랐었다. 아니 공식 개발 가이드라는 위키에서는 샘플 두개 던져놓고 알아서 개발하라니, 하다못해 api레퍼런스라도 제공이 되면 거기서부터 밀고 내려오면 되니까 괜찮은데, 이건 뭐 샘플에 있던 답글에서 지식을 얻어야 하니.... 하지만 끝까지 분석을 통해 흐름을 파악한 이상, 다음은 조금 복잡한 모듈에 대한 설명을 해볼 참이다 왜냐하면 이미 XE로 프로젝트를 진행하고자 마음을 먹었기 때문에 적어도 모듈만큼은 능숙하게 다뤄야 하겠다고 생각했기 때문이다. 그래 인정하자, 처음 접근하기는 어렵지만 체계적인 개발론을 기반으로 한 XE, 사용할 수 밖에 없다!!!

댓글

Holic Spirit :: Tistory Edition

design by tokiidesu. powerd by kakao.