[카테고리:] 워드프레스 개발

  • 플러그인 엔진 컨셉 잡소리: PSR

    플러그인 엔진 컨셉 중 가장 먼저 언급하고 싶은 것들.

    1. PSR 코딩 스탠다드

    워드프레스는 나름의 Best Practice코딩 스탠다드가 있습니다. 그러나 플러그인 엔진은 PSR 1, 2의 코딩 스탠다드를 더 선호합니다. 사실 이렇게 작성해서 플러그인 제출에 탈락할지 말지는 잘 모르겠습니다만, 엄연히 오픈소스에 커뮤니티 기반의 프로젝트인 워드프레스가 단지 자신들의 스탠다드를 지키지 않았다고 해서 플러그인 제출을 막을 것 같지는 않을 거라 생각합니다.

    파일 이름과 클래스 이름 모두 PSR을 따르는데, 이는 Autoloading을 사용할 때 이 쪽의 이름 규칙이 보다 편리하기 때문입니다.

    2. Autoloading, Composer

    PSR-1, PSR-2에 이어 PSR-4 또한 적극적으로 도입하여 사용합니다. 메인 파일 처음에 들어가는 “require_once vendor/autoload.php” 구문 이외에는 클래스나 함수를 부르기 위해서는 require, include 구문을 쓰지 않습니다. 이렇게 autoloading을 사용하면 코드 작성과 유지가 매우 쉬워집니다.

    물론 autoloading을 위하여 컴포저 (composer)를 도입합니다. 컴포저가 있는 만큼 외부 라이브러리 관리도 한결 편리해집니다.

    물론 기존의 워드프레스식 파일 이름과 워드프레스식 이름 관례를 쓰더라도 autoloading을 사용하지 못하는 것은 아닙니다. 그러나 워드프레스식 클래스 이름과 파일 이름의 명명법이 다르기 때문에 일일이 파일 이름과 클래스 이름에 대해 문자열 처리를 해야 하는 불필요한 비용이 발생합니다.

    예를 들어 클래스 이름이 WP_Member_Contacts 라는 클래스를 만들었다고 합니다. 그러면 이 클래스의 파일 이름은 ‘class-wp-member-contacts.php’ 정도로 될 것입니다.  그러면 WP_Member_Contacts가 class-wp-member-contacts.php 파일에 대응된다는 사항은 ‘composer dump-autoload’ 명령을 통해 autoloading 라이브러리가 미리 잘 캐싱해 둡니다. 여기까지는 좋습니다. 아무 문제가 없습니다.

    그러나 엔진을 제작하면서는 약간 고민거리가 생깁니다. 엔진에서는 class-wp-member-contacts.php 파일을 만나게 되면 이 파일은 어떤 클래스를 가지고 있는지를 알아야 할 때가 생깁니다. 아까의 반대입니다. 물론 규칙이 있으니 쉽게 변환할 수 있습니다. class- 접두를 빼고, 모든 하이픈을 언더바로 바꾼 다음 ucfirst를 적용하면 됩니다(사실 PHP FQCN(fully-qualified class name)은 대소문자를 가리지 않으므로 꼭 하지는 않아도 됩니다). 그러나 일일이 파일 이름에 대해 문자열을 계산해야 하는 것이 번거롭습니다.

    PSR-4와 PSR-1 규칙에 따라 클래스 이름과 파일이름을 CamelCase로 통일하면 그런 문자열 계산 비용이 사라집니다. 그리고 namespace의 한 마디가 디렉토리 깊이 하나로 매핑되는 구조 또한 매우 합리적입니다.

  • 플러그인 엔진의 컨셉에 대하여

    이번 포스트에서는 만들고자 하는 플러그인 엔진에 대한 개략적인 컨셉을 적고자 합니다.

    워드프레스 플러그인 제작을 시작한 것은 2014년입니다. 그동안 몇몇 플러그인을 제작하였고, 몇 번의 플러그인 강의를 한 적이 있습니다. 몇 번의 플러그인 개발을 해 보며 느낀 것이 있습니다. 워드프레스는 훌륭한 CMS이지만, 훌륭한 웹 프레임워크는 아닌 것 같다는 겁니다. 다른 웹 프레임워크와 비교하면 솔직히 좀 아쉬운 것들이 많은 것이 사실이죠.

    사실 워드프레스를 위한 개발 프레임워크들이 여럿 있지만, 저는 그냥 제 플러그인 엔진을 만들기로 결심했습니다. 네, 바퀴를 재발명하는 것 같은 어리석은 일일 수도 있겠습니다. 사실 거창한 이유는 아닙니다. 워드프레스 플러그인 개발자로 지내며 제 나름대로 고민한 것을 정리하고 싶었고, 제가 저의 결과물을 얼마만큼 체계적으로 정리할 수 있는지 궁금했습니다.

    프레임워크 비슷한 것을 만드는 것은 사실 처음이 아닙니다. 아마 아무도 모르시겠지만 “Axis”라는 프레임워크라는 이름을 붙이기도 민망한 것을 만들고 처참히 실패가 적이 있었습니다. 지금도 마찬가지입니다. 제가 무슨 깊은 경험이 있어서 프레임워크를 운운하겠습니까. 그저 해보면서 성장하기를 바라는 것이겠죠. 여전히 민망하고 또 어려움을 느낍니다.

    실무에서 배움

    현재 워드프레스 개발자로 지내고 있으며, 실무를 접하며 겪게 되는 문제들은 상당히 흥미롭습니다.  어떤 일들은 지엽적이지만, 때때로 클라이언트들은 지금껏 접해보지 못한 상황을 문제로 가져옵니다. 저는 제가 흥미로운 다양한 경험을 해 볼 수 있는, 나름대로 좋은 환경에 있다고 생각합니다.  그리고 이런 문제들은 제가 상상할 수 있는 가장 우아한 방법으로 해결하고 싶습니다.

    물론 당장 그런 최상의 결과를 내는 것은 매우 어려운 일입니다. 그런 좋은 코드는 거저 만들어지는 것이 아닙니다. 꾸준히, 정말 꾸준히 여러 이슈를 거쳐 제련되어야 합니다.

    불행히도 클라이언트들을 위해 만드는 소위 ‘남을 위해 짜는 코드’들은 그렇게 지속적인 관심을 쏟기 어렵습니다. 빠른 시간에 만들어야 하고, 또 한 번 데드라인에 맞춰 완성된 결과는 적절히 유지보수하는 것 말고는 지속적으로 업데이트하여 쓰기가 어렵습니다. 클라이언트에게 최상의 코드를 제공해 드리지 못하는 점 죄송하게 생각합니다. 그러나 저도 사람인지라, 만들고 나서야 결과를 아니까요. 사람의 최선에는 늘 한계가 있더군요.

    엔진은 우선 저희 경험을 최대한 완성도 있게, 재활용될 수 있도록 레거시 코드를 정돈한다는 느낌으로 작성할 것입니다. 자주 반복되는 요소들은 그 다음에는 더욱 효율적인 방법으로, 간결하게, 이전보다 더욱 잘 작성될 것이라는 희망을 가지고 코드를 작성합니다.

    MTIV 패턴의 도입

    ‘MTIV’는 제가 생각한 플러그인을 위한 MVC류의 개발 패턴입니다.

    저는 Axis라는 구작을 만들 때부터 워드프레스 플러그인에 최대한 MVC 패턴을 도입하려고 애를 썼습니다. 솔직히 지금도 MVC의 정수를 이해하지는 못한다고 생각하며, 타 프로그래머에 비해 오히려 많이 부족하다고 생각합니다. 그럼에도 불구하고 이것을 고집하는 이유는, 이것이 더 나은 방법이라고 생각하기 때문입니다.

    개발을 하면서 코드를 이렇게 되도록 노력하고 있습니다. “처음에는 힘들고 귀찮지만, 한 번 짜 둔 코드는 유지보수하기 극도로 쉽게.”

    확실히 MVC 류의 프로그래밍 패턴이 뛰어난 유연성을 가지고 있고, 유지보수하기 좋은 구조를 가지고 있습니다. 많은 웹 프레임워크들도 MVC, 또는 이에서 파생된 패턴을 사용하고 있습니다.

    그런데 이 MVC를 바로 워드프레스 플러그인이나 테마 개발에 사용하려고 하면, 그다지 쉽지 않을 것입니다. 적어도 저는 그랬습니다. 이 부분은 저의 개발 엔진에 있어 가장 중요한 부분이기 때문에, 몇 가지 이유를 들어 그 이유를 자세히 설명해 보고자 합니다.

    우선 워드프레스 코어 자체가 이런 패턴을 직접적으로 사용하지 않기 때문에,  좋은 코드 교범을 보기가 힘듭니다.

    그리고 다른 웹 프레임워크와는 다른 워드프레스의 특성에서도 기인한다고 생각합니다. 다른 웹 프레임워크들은 사용자가 전달한 request 부터 서버가 도로 보낼 response까지의 작업을 온전히 개발자에게 맡깁니다. 웹 프레임워크들은 그 작업을 보다 편리하게 지원해주는 역할을 맡았죠. 설계도와 지휘봉은 모두 개발자에게 있습니다.

    그러나 워드프레스는 그렇지 않습니다. 이미 코어가 request를 받아 response를 대부분 처리하는 상태입니다. 플러그인, 테마 개발자는 코어에 이미 코어 개발자가 만들어 둔 고리, 훅(hook)에 자신의 콜백을 등록하는 방법으로, 즉 이미 완성된 코어를 확장하는 방법으로 개발을 진행합니다. 이것이 워드프레스식 웹 개발입니다.

    통상적인 웹 프레임워크들의 동작

    여기서의 예는 조금 복잡하니, 어려운 분들은 뛰어넘어도 됩니다.

    보통 웹 프레임워크는 URL 라우팅 규칙을 설정하고, 그 라우팅 규칙에 따라 하나의  request를 하나의 함수로 대응하는 구조로 되어 있습니다.

    여기서 제가 자주 사용하던 웹 프레임워크인 Django를 예를 들면, 장고는 MVC의 변종인 MTV 패턴을 사용합니다. Django의 URL 라우팅은 주로 각 앱의 urls.py 라는 파일에서 정합니다. 아래 코드는 그 예입니다.

    from django.conf.urls import url
    
    from . import views
    
    urlpatterns = [
        url(r'^$', views.index, name='index'),
    ]

    어떤 도메인의 ‘/’ 경로로 접근한 request는 views.index라는 View 함수가 처리를 합니다.

    from django.http import HttpResponse
    
    from .models import Question
    
    
    def index(request):
        latest_question_list = Question.objects.order_by('-pub_date')[:5]
        output = ', '.join([q.question_text for q in latest_question_list])
        return HttpResponse(output)

    index라는 View 쪽 함수 예입니다. 모델을 불러와 request가 원하는 방식으로 자료를 가공하여 response를 만들어내는 것입니다.

    워드프레스 코어의 동작

    워드프레스의 주 진입점은 워드프레스 디렉토리 최상위의 index.php와 wp-admin 디렉토리의 index.php, edit.php, post.php 등등이 있습니다. 일반적인 프론트 화면의 진입점인 최상위의 index.php를 보면 wp-blog-header.php 파일을 읽어들이는 코드가 있습니다.

    wp-blog-header.php 에서 WP 클래스의 main() 메소드가 실행됩니다. 이 메인 메소드는 아래처럼 되어 있습니다.

    public function main($query_args = '') {
        $this->init();
        $this->parse_request($query_args);
        $this->send_headers();
        $this->query_posts();
        $this->handle_404();
        $this->register_globals();
    
        /**
         * Fires once the WordPress environment has been set up.
         *
         * @since 2.1.0
         *
         * @param WP &$this Current WordPress environment instance (passed by reference).
         */
        do_action_ref_array( 'wp', array( &$this ) );
      }

     사용자의 request를 받아서 일괄적으로 처리하는 구조입니다. 여기서 우리가 코드를 함부로 변경할 수 없는 대신 action, filter를 사용하여 코어의 동작을 살짝살짝 변경시킵니다.

    MTIV의 I (initiator, 전수자)

    장고의 MTV라는 컨셉이 플러그인 개발에는 더 적절하다고 생각했습니다. M은 모델(Model)입니다. 데이터를 다루는 영역입니다. T는 템플릿(template)을 의미합니다. 화면을 어떻게 보여줄 지를 결정하는 시각적인 영역입니다. V는 뷰(View)로서 request를 받아 reponse를 생성하는 역할을 맡습니다. 뷰는 모델과 템플릿사이에 위치하며, 모델이 전하는 자료를 요청에 맞게 처리 가공하여 템플릿에게 넘겨주는 역할을 합니다. 템플릿은 뷰가 전해준 자료를 바탕으로 HTML 문서 등을 결과로 찍어냅니다.

    물론 MTV 같은 패턴을 워드프레스에 도입할 수 없는 것은 아니지만, 그것이 약간은 껄끄럽습니다. 앞서 이야기하였듯 워드프레스는 타 웹프레임워크처럼 request 부터 자연스럽게 로직을 처리하는 구조가 아니기 때문이라고 생각합니다.

    워드프레스 플러그인은 그 특성이 있습니다. 계속 이야기하였듯, 훅과 콜백입니다. 통상의 웹 개발자는 request에 초점을 맞추고 request에 맞춰 response를 작성하는데 온 힘을 기울인다면, 플러그인 개발자들은 코어에서 제공하는 (혹은 타 플러그인이 정의한) 훅에 관심을 두고, 원하는 기능을 작성하기 위해 적절한 훅을 탐색하고, 그 훅에 맞춰 적절한 콜백을 작성하는 데 노력을 기울입니다.

    저는 Axis 때무터 이 훅과 콜백에 천착해 왔습니다. 이 훅과 콜백의 특성상 기존의 MVC나 MTV들을 여타 웹 프레임워크 처럼 플러그인에 쓰기 쉽지 않다고 생각하였습니다. 그 대안이 “MTIV“입니다. ‘I’가 하나 추가된 것이죠. 저는 이 ‘I’를 전수자, 또는 이니시에이터(initiator)라고 부릅니다. 

    전수자의 가장 기본적인 역할은 콜백을 문맥에 맞게 정리하는 것입니다. 그리고 전수자가 잘 정돈한 콜백 함수에서 비로소 MTV의 패턴 도입이 이루어집니다.

    보통 어떤 구성 요소(component)들은 적어도 하나 이상의 액션, 필터를 조합하여 이뤄집니다. 그런데 이 액션과 필터들은 “어떤 상황에 대한 대응”을 설정하는 것이지 그 자체로 어떠한 구성 요소를 설명하는 것이 아니라, 차후 유지보수시 기능 분석이 약간 힘든 측면이 있습니다.

    예를 들어

    add_action( 'a_action', 'my_a_action_callback');
    function my_a_action_callback () {
    ...
    }
    
    add_action( 'b_action', 'my_b_action_callback');
    function my_b_action_callback () {
    ...
    }
    
    add_action( 'c_action', 'my_c_action_callback');
    function my_c_action_callback () {
    ...
    }
    
    ...

     이런 식으로 평면적인 액션 선언 및 콜백 함수로만 코드를 짜게 된다고 생각해 보세요. 또 구성 요소에 쓰이는 훅들이 겹치지 않으리란 보장이 있나요? 어떤 훅이 어떨 때 쓰이는지 쉽게 추적이 가능할까요?

    전수자는 이런 문제를 해결하기 위해 생각해 내었습니다. 한 구성 요소에 필요한 훅과 콜백을 의미 있게 하나의 클래스로 묶어 관리합니다. 훅, 콜백 선언이 한 클래스에 의미 있게 관리되므로 코드 유지보수에 더욱 편리합니다.

    물론 전수자를 쓰는 것도, 또 전수자의 콜백에서 MTV를 사용해야 하는 것은 강제 사항이 아닙니다. 예를 들어 워드프레스에서 메일을 보낼 때 본문에 html 코드를 삽입하고 싶다면 아래 같은 코드를 쓰는데, 아무리 봐도 이런 콜백 때문에 뷰를 쓰는 건 닭 잡는 칼에 소 잡는 칼 쓰는 격입니다.

    add_filter( 'wp_mail_content_type', 'set_content_type' );
    function set_content_type( $content_type ) {
      return 'text/html';
    }

    이럴 때는 그냥 전수자 콜백에서 그냥 처리해도 무방합니다.

    또 이미 많은 사람들이 저와 같이 생각을 하고 있고 비슷하게 의미 있는 훅과 콜백을 클래스로 묶거나 적절히 모듈화 하는 것으로 알고 있습니다. 그것과 전수자는 거의 동일합니다. 그러나 제가 생각하는 전수자 개념의 핵심은 “모듈화”만을 위한 것이 아닙니다. 워드프레스 플러그인 구조에서 원할 때 제대로 MTV를 사용할 수 있도록 길을 연다는 점, 이것이 중요합니다.

    제 경험에 미루어 보면 MTV 패턴은 플러그인 개발자가 원한다면 어떻게든 구현하여 사용할 수 있는 부분인 것 같더군요. 그러나 구현을 하게 되면 도대체 어디에서부터 뷰를 생성하고 그 로직을 전개할지 난감해질 때가 있었습니다. 모든 콜백에 대해 ‘뷰’라는 패턴을 가진 객체를 생성해야만 할까요?  애초에 훅, 콜백과 뷰는 잘 어울릴 수 있는 구조인걸까요? 설령 가능하다면 어떻게 뷰를 정돈된 형태로 유지할 수 있을까요?

    다행히 MTIV 구조에서는 전수자가 가장 먼저 기능별로 훅과 콜백을 정리해 두고 있습니다. 그 콜백을 처리하는 것이 뷰이기 때문에, 자연스럽게 뷰의 생김새도 전수자의 파일 구성을 따라 갑니다. 그래서 기능별로 만들어진 전수자 파일 구조 및 코드를 따라가면 자연스럽게 해당 심화 로직인 뷰 코드를 추적할 수 있습니다. 코드의 흐름을 쫓아가기 매우 쉬우며, 따라서 유지보수도 어렵지 않게 될 수 있습니다.

    전수자 예제

    아주 간단한 전수자 예제 인터페이스 코드를 작성해 보았습니다.

    interface Initiator
    {
        public function init_hooks($main);
    }

     $main으로는 플러그인 메인 파일이 입력됩니다. 훅 작성시 때때로 저 메인 파일이 필요하거든요.

    플러그인 활성화 때 어떤 역할(role)을 생성해야 한다고 가정합니다. 그러면 이 때 필요한 전수자를 작성한다면

    class Roles_Caps implements Initiator {
    
      public function init_hooks( $main = '' ) {
        register_activation_hook( $main, array( $this, 'callback_activation_hook' ) );
        register_deactivation_hook( $main, array( $this, 'callback_deactivation_hook' ) );
      }
    
      public function callback_activation_hook() {
        $this->add_role();
      }
    
      public function callback_deactivation_hook() {
        $this->remove_role();
      }
    
      private function add_role() {
        add_role(...);
      }
    
      private function remove_role() {
        remove_role(...);
      }
      // (이하생략)
    }

     이렇게 클래스를 작성하고, 메인 파일에서는

    $initiator = new Roles_Caps();
    $initiator->init_hooks(__FILE__);

    처럼 실행할 수 있습니다.

    보다 뷰와 템플릿 로직이 강화되어야 하는 경우를 생각해 봅시다. 아주 간단히 전수자 클래스의 내부 코드를 이렇게 작성할 수 있습니다.

    class Menu_Initiator implements Initiator {
    
      public function init_hooks($main = '') {
        $this->view = new Menu_View();
        add_action('admin_menu', array($this, 'callback_admin_menu'));
       ...
      }
    
      public function callback_admin_menu()
      {
        add_menu_page('title', 'title', 'manage_options', array($this, 'dispatch'));
      }
    }

    뷰 코드를 간단하게 설명하자면 이렇습니다.

    class Menu_View {
      public function dispatch() {
        $context = array(
          'title' => 'Hello!';
          ...
        );
        ...
        render('menu.php', $context);
      }
    }

     menu.php 는 템플릿 파일입니다. 콘텍스트에 따라 내용을 찍어냅니다.

    <div class="wrapper">
    ...
    <h2><?php echo esc_html($title); ?></h2>
    ...
    </div>
    

    이렇게 전수자부터 그 틀을 잡고 시작하면 이 이후의 MTV는 보다 형태를 잡기 쉬워집니다.

    이밖에도 전수자만이 가진 독특한 기능들이 있습니다. 이걸 설명하고 싶어 손이 근질거리만, 분량이 너무 많아지니 다음 포스팅으로 미루도록 할께요.

    MU Plugin, Plugin, 그리고 3rd Party Library

    이제는 이런 개발 프레임워크가 더 등장할지 모르겠지만, 개발을 위한 프레임워크가 또다른 형태의 플러그인으로 제공되는 것이 있습니다. Piklist나 Genesis 같은 것들이 그 예입니다.

    저의 플러그인 엔진은 사이트 동작을 위해 반드시 필요하고, 운영시 절대 비활성화 되어서는 안 되는 경우가 대부분이라 기본적으로는 엔진은 MU Plugin 형태로 동작되기를 권장합니다. 그러나 MU 플러그인이나 일반 플러그인이나 큰 차이가 없기 때문에 제작 자체는 일반 플러그인처럼 하고 있습니다.

    그러나 개발 엔진 같은 라이브러리 같은 코드들을 플러그인으로 제공하게 되면 버전 충돌 문제를 피할 수 없습니다.

    가령 제 플러그인 엔진이 굉장히 유명해져서 많이 사용된다는… 아주 즐거운 가정을 해 보자구요. A 플러그인은 제 엔진 1.8 버전을 사용합니다. B 플러그인은 제 엔진 2.0 버전을 사용한다고 해요. 그런데 1.8과  2.0버전은 엄청난 차이가 있어서 이 둘을 같이 사용할 수 없어요. 1.8 방식으로 코드를 쓰면 2.0에서는 오류를 뿜어내고, 그 반대로도 마찬가지입니다. 이 사이트는 A, B 플러그인을 둘 다 필요로 하는데… 어쩌죠?

    이런 경우에 엔진은 서드파티 라이브러리처럼 쓸 수 있도록 할 것입니다. 해당 플러그인이 각 버전의 플러그인을 충돌하지 않도록 각자 독자적인 라이브러리로 내장할 수 도 있게 만들어 저의 엔진을 사용할 수 있도록 만들 것입니다.

    여기까지! 이만 줄임!

    개발에 관해 심각하게 글을 쓰려고 한 건 아닌데… 내용이 많이 길어집니다. 저도 가볍게 제 플러그인 엔진에 대한 컨셉을 정리하고, 기왕 정리한 거 여럿에게 보여 주는 것이 어떨까 싶었습니다. 딱딱하고 지루하게 컨셉을 쓰다 보면 아예 글을 작성할 수 없게 될 것만 같아 설렁설렁 대충대충 블로그 포스팅으로 남깁니다. 플러그인 엔진 버전이 거듭되어 제련되듯, 이러한 설명 자료들도 차차 탄탄하게 기록되었으면 좋겠습니다.

  • 당신이 워드프레스 관리자 화면을 어색해 하는 이유

    들어가며

    본 포스트는 UI 디자인 등에 대한 전문적인 의견이 아니라, 워드프레스 관리자 화면이 어색하다는 사람들을 설득하기위해 써 본 글임을 알려 드립니다. 올바르지 못한 점이 있다면  피드백 부탁드립니다.

    처음 쓰는 툴이니 어색한 것은 당연하겠지만, 워드프레스를 초심자들은 많은 부분들을 어색해합니다. 그 중은 상당히 오랜 기간 학습을 필요로 하지요. 그 중 대표적인 하나로 “관리자 화면”을 들 수 있습니다.

    대부분의 초심자들은 관리자 화면에 익숙해지기까지 상당한 인내심을 지불합니다. 약간 과정하면, 마치 웅녀가 곰에서 사람이 되기 위해 동굴에서 쑥과 마늘 먹으며 괴로움을 참아야 했던 것처럼요. 그 괴로움을 끝까지 참아내기 어려운 경우도 많고, “도대체 왜 도구 때문에 내가 바뀌어야 하는가?”라고 생각할 수도 있습니다. 참지 못한 몇몇 ‘호랑이’들은 참지 못하고 다른 CMS로 갈아탈 것이고, 이 분들 중에는 결국 워드프레스에 강한 거부감을 보일 수도 있을 것입니다.

    반면 잘 적응한 사람들은 대부분 워드프레스 관리자 화면은 합리적이고 쓰기 편하다고 합니다. 아무리 적응하기 나름이라고는 하나, 단순히 적응도 때문에 불편함과 편함이 생기는 것은 아닐 겁니다. 그러면 과연 무엇 때문에 어떤 이들은 불편하다고 느끼고, 어떤 이들은 편하다고 생각하는 걸까요?

    워드프레스 관리자 화면이 익숙하지 않은 이유.

    우선 워드프레스의 관리자 화면이 어색하고 불편하다면, 친숙하고 편한 스타일의 관리자 스타일이 존재할 것입니다. 그것을 무엇이라 칭하기 어렵지만 “기존 스타일”이라고 뭉뚱그려 표현해 보고자 합니다. 네, 우리 한국 사람에게 익숙한 문법과 문맥을 가진 바로 그 스타일입니다. 이 문법과 문맥을 거칠게나마 특성을 몇가지 나열해 보지요.

    1.  메뉴는 위계를 이루는데, 최상단에서부터 최하단까지 상당히 깊은  깊이, 적어도 3단 이상을 가지고 있습니다.
    2. 메뉴의 레이블은 “XX 설정”, 혹은 “XX 관리”라는 스타일로 되어 있어 세부적으로 디테일한 설정 및 관리를 하는 스타일을 가지고 있습니다.
    이런 UI가 익숙하신가요?

    반면 워드프레스 관리자 화면을 보면 “~관리”라든지, “~설정”이라는 레이블을 찾을 수 없습니다. 특별한 플러그인을 설치하지 않은 이상은 말이죠. 그냥 알림판, 글, 미디어, 페이지, 댓글, 외모, 플러그인, 사용자, 도구, 설정 (아!), … 이렇게만 있을 뿐입니다.

    워드프레스 메뉴들은 우리한테 뭔가 “관리”하라거나, “설정”하라는 요구를 하지 않습니다. 아마 기존의 스타일을 고수하셨던 분들은 여기서 혼란스러움을 느낄 수 있을 겁니다.

    워드프레스의 관리자 영역. 메뉴도 심플하고, 깊이도 2단으로 제한되어 있다.

    사실 “관리”나 “설정”이라는 말이 없다는 것만으로는 어색함을 극복하기는 어렵습니다. 기존 스타일과 워드프레스 스타일, 그 작은 차이를 이해한 것 가지고는 워드프레스식의 관리자 스타일을 납득하기란 어렵습니다. 그냥 관리, 설정 식으로 메뉴를 꾸며 주면 안되나요? 좋은 게 좋은 거고, 편한 게 편한 거 아닌가요?

    그러나 그렇지 않습니다. 제가 이해한 바로는 이렇습니다. 기존의 스타일이 가진 ~~관리, ~~설정 같은 메뉴 구성은 그다지 좋은 메뉴 구성이 아닙니다. 저는 앞으로는 그런 스타일은 버리시기를 강력히 권하는 입장입니다. 앞으로의 글은 왜 기존의 스타일이 나쁜지, 그리고 왜 워드프레스 스타일이 권장할만한지에 대해 나열할 생각입니다. 만일 기존의 방법에서 더 이동할 생각이 없으시다면 이제 이 글을 그만 읽으셔도 됩니다.

    관리자 화면 자체가 관리를 위한 장소이다.

    관리자 화면 자체가 “관리(administration)”을 하기 위해 들어오는 장소입니다. 그런데 그 메뉴 하나하나에 죄다 그놈의 관리가 붙어야 하는 이유는 대체 뭔가요? 요즘은 데스크탑 뿐만 아니라 모바일 등등 여러 기기에서 웹에 접근합니다. 작은 화면에서는 집약된 정보가 중요합니다. 관리자 화면에서 관리라는 단어를 쓰는 것은 낭비입니다.

    관리의 본질은 모델이지 액션이 아니다.

    콘텐츠 관리 시스템(Contents Management System, CMS)에서 가장 중요한 것은 콘텐츠입니다. 그리고 콘텐츠가 존재해야 콘텐츠의 관리가 거론될 수 있는 법이죠. 그러나 기존 스타일은 콘텐츠가 무엇인지 명확히 알 수도 없는 상황에서 관리 내지 설정을 이야기합니다. 제가 기존의 스타일을 나쁘다고 말하는 이유 중 가장 주된 이유입니다.

    사용자가 우선 관리자 메뉴에서 익숙해져야 하는 것은 무엇인가요? 관리 메뉴를 습득하는 것? CMS에서 가장 중요한 것은 다름아닌 콘텐츠입니다. 관리자는 자신이 관리하는 콘텐츠를 먼저 잘 파악해야 합니다. 그 다음에 그 콘텐츠를 어떻게 관리할까 하는 문제가 생기겠죠. 그러나 기존의 시스템은 다짜고짜 관리부터 하려 듭니다. 사용자가 그 관리 대상이 무엇인지 파악하지 못한 상태에서도요. 제 관점에서는 그런 메뉴들이 오히려 달갑지 않습니다.

    CMS의 본질은 콘텐츠라고 반복하여 말씀드리고 있습니다. 먼저 콘텐츠라는 “모델”이 정의되어야 합니다. 그리고 나서 그 “모델”을 어떻게 처리해야 할지에 대한 “액션”을 논할 수 있습니다. 물론 거의 모든 경우 이 모델과 모델에 대한 행위는 붙어 있기 때문에 그 개념을 다 포괄하여 생각하는 경우가 많습니다만, 문법에서 명사와 동사가 다른 것처럼 모델과  액션은 엄연히 다른 개념입니다.

    기존 스타일이 나쁜 예

    기존의 스타일이 나쁘다고 하는 이유를 모델과 액션, 혹은 명사와 동사의 측면으로 바꾸어 다시 강조해 볼까요? 행위는 행위의 대상이 존재해야 그 의의가 있습니다. 모델이 명확하게 인지되지 않은 상태에서 액션을 취하면, 그 액션은 올바르지 못할 때도 있습니다.

    메뉴 설계적인 측면에서도 액션 위주의 메뉴는 비합리성을 가지기 쉽습니다. 행위는 행위의 대상인 모델이 존재해야 그 의의가 있습니다. 그리고 만일 모델의 구조가 변화하면 행위에도 영향을 끼칠 수 있습니다. 설계적으로 보면 행위는 모델에 종속되어 있는 셈입니다.

    “A 관리 메뉴”를 들어가면 “A 관리 페이지”를 보여줍니다. 여기서 “A 모델”을 조작합니다. 그런데 A 모델은 B 모델에 의존하기에, A 관리 페이지에서 암시적으로 B 모델을 편집하게 설계합니다.

     예를 들어 “A 관리”라는 메뉴를 만들었다고 생각해 보죠. 그리고 A 관리 메뉴의 실대상인 A 모델은 꽤나 복잡하기 때문에 B 모델 또한 같이 생각하여 “관리”되어야 합니다.  그러나 B 모델은 상대적으로 복잡하지 않습니다. 그래서 암시적으로 A관리 메뉴에서만 B 모델이 보이도록, 즉 관리되도록 설계됩니다.

    “C 관리”가 등장했습니다. “C 모델”을 관리하는데, 이것도 B 모델을 의존합니다. B모델의 관리는 A와 C 둘다 부담해야 할 입장에 처했습니다. 어느 한 쪽만 처리하기에도 애매하지요.

    그런데 말이죠, 프로젝트가 진행되다가 “C 관리”라는 메뉴를 또 추가되어야만 상황이 생겼다고 보죠. 그런데 대상이 되는 C 모델 또한 B 모델을 포함합니다. 자, 그러면 C 관리 메뉴에서도 B 모델에 대한 관리가 들어가야 하나요? 그러면 메뉴가 중복되겠죠? 그렇지만 C 메뉴도 B 모델을 처리하는 부분이 있는데…? 어떡하죠?

    앞으로 이런 식으로 개발하면서 “행동”에 대한 요구는 점점 늘어갈 것입니다. 그러면 이 행동에 대한 메뉴를 만들어 나가게 되면 메뉴는 자연스럽게 복잡해집니다. 그리고 이 복잡함을 감추기 위해 “위계질서”를 도입합니다. 메뉴가 깊어지겠죠. 그러나 동족방뇨입니다. 당장은 깔끔해 보일지는 모르지만, 나중에는 원하는 메뉴가 어디에 붙어 있는지 찾기 어려울 정도로 복잡해지는 경우가 생깁니다.

    물론 위 경우 사용자의 편의를 위해 A, C 메뉴 둘 다 B 모델을 편집할 수 있는 UI를 주는 것도 나쁘지는 않습니다. 그러나 B 모델이 간단해서 망정이지, B 모델 또한 상당히 복잡한 구조였다면 개발자도 괴롭고, 쓰는 사람도 괴로울 것입니다. (여기 예에서는 “B 관리 페이지를 넣어!”라고 하시겠죠? 그게 제가 의도한 바입니다. 단, 그놈의 “관리”라는 접미사는 좀 빼 주는 게 어떨까요?)

    제 나름대로의 생각으로는 대기업 홈페이지나 금융권들의 메뉴가 쓸데없이 어려운 이유에도 이런 이유가 있다고 생각합니다. 아, 물론 그들의 업무 범위는 상상외로 크기 때문에 그것만으로도 머리가 터질 만큼 복잡한 게 사실입니다. 그러나 애초에 복잡한 것을 더 복잡하게 만드는 이유 중 얼추 이러한 요소도 있을 것이라는 개인적인 짐작입니다.

    워드프레스의 메뉴가 합리적인 이유

    대개의 콘텐츠는 CRUD (Creation: 생성, Retrieval: 검색, Update: 수정, Deletion: 삭제) 이 네 가지 동작을 기본으로 합니다. 복잡한 동작도 알고 보면 이 4가지의 연장선이죠. 그런데 이 단순한 행위가 모델에 따라 여러 동선을 만들어낼 수 있습니다. 그러니 동선을 먼저 생각하기 전에 모델부터 생각해야 하는 것이 맞는 것이 아닐까요?

    워드프레스의 관리자 화면이 심플하면서 직관적이라는 이유는 바로 이런 이유입니다. 메뉴는 행위에 초점을 맞추어 구성되어 있지 않습니다. 관리하고자 하는 콘텐츠에 보다 초점이 되어 있죠. 여러분이 어떤 플러그인을 설치해서 새로운 콘텐츠 형태를 추가한다 하더라도 이는 거의 변하지 않습니다.

    예를 들어 Gravity Form을 설치하면 Form이라는 명사형의 메뉴가 등장하지, “Manage Form” 같은 동사형의 메뉴가 등장하지 않습니다. 우커머스 플러그인을 설치해도 마찬가지입니다. 상품, 주문 같은 명사형의 메뉴가 생깁니다. 설정 메뉴의 하위 메뉴도 마찬가지입니다. 대개 명사형의 하위 메뉴가 있죠. 그 메뉴에 들어가서야 비로소 추가, 수정, 삭제, 검색 등의 행동을 담은 페이지가 나오죠.

    이제 워드프레스의 메뉴에 익숙해진 분들이 직관적이고 편하다는 이유가 무엇인지 이해하실 수 있으신가요? 이 분들은 “행동”을 먼저 생각하는 것이 아니라 관리하는 “모델”에 더 초점을 맞추고 메뉴를 보기 때문에 메뉴가 더 쉬운 것입니다. 이 관점에서 메뉴를 구성하면 그다지 메뉴가 깊어질 이유도 없습니다.

    우커머스 설정화면. 상거래는 복잡한 만큼 많은 설정을 가지고 있습니다 그러나 그 복잡함이 관리 메뉴까지 간섭하지는 않습니다. 또한 메뉴들은 관리 대상과 모델에만 집중하고 있습니다.

    물론 우커머스 같은 상당히 복잡한 데이터를 다루는 플러그인은 실제로 메뉴가 많이 복잡합니다. 그러나 이런 복잡성이 좌측 주 메뉴에까지 슬슬 기어나온다면, 그건 문제겠죠. 위 그림처럼 워드프레스 관리자에서도 탭이라든지, 매니지 영역 등등을 사용해 더 깊은 메뉴를 처리할 수 있도록 하고 있습니다.

    마치며

    여기까지 읽어주셔서 감사합니다. ‘신문물’이란 게 대개 그렇습니다 . 새로운 것을 받아 들이려면, 기존의 것과 상충되는 것은 극복해야 합니다. 처음은 괴롭지만, 더 나은 방법임은 우리는 알고 있습니다. 애초에 그것을 더 낫게 하기 위해 도입하는 것이거든요. 물론 기존의 관성이 만만찮게 강력하기 때문에 변하는 것은 쉽지 않습니다.

    그러나 본질을 놓치지는 말자구요. “왜 변해야 하는가?”, “이것을 변화하면 무엇이 나아지는거?”를 잘 이해하면 그 변화를 더 잘 받아들일 수 있습니다. “워드프레스의 관리자는 왜 그 모양새를 가지고 있는가?”를 더 이해하면 보다 워드프레스를 친숙하게 받아들일 수 있을 것 같아 글을 써 보았습니다. 전문적인 UI 디자이너의 의견이 아니라 틀린 점도 많고, 제가 제대로 이해하지 못한 점이 많을 것입니다. 틀린 점이 있다면 댓글로 피드백을 부탁드립니다.

  • 테마를 별도의 디렉토리로 옮기기

    개발 의도상 테마를 별도의 디렉토리에 두고 싶은 생각이 들었다. 플러그인을 놓고 쓰는 것처럼 말이다. 그런데 wp-config.php 코덱스를 참고해도 플러그인의 경로를 바꾸는 일은 허용되나, 테마에 대해서는 이런 설정이 공식적으로는 존재하지 않는다. 테마를 별도로 쓰려면,  wp-contents 디렉토리를 벗어나지 않는 한에서 변경하기를 권장하기도 한다.

    사실 실사용 서버에 이런 일을 할 필요는 없다. 단지 개발 서버상에서만 편하자고 하는 일이다.

    나는 코어와 플러그인의 디렉토리를 분리해서 사용한다. 이렇게 해서 여러 사이트에 대해 개발을 할 때 단 한 벌의 코어로 대응할 수 있다. 이렇게 사용하는 법은 다른 포스트를 통해 소개한 바 있다.

    테마 디렉토리는 플러그인과 같은 레벨에 둔다고 가정한다.  그리고 MU 플러그인을 하나 만들고 거기에 그냥 다음과 같이 적으면 된다.

    register_theme_directory( dirname(__DIR__) . '/themes' );

     이렇게 하면 이동한 테마 디렉토리에 있는 테마 목록이 워드프레스 ‘외모’메뉴에서 보이게 된다.

    그런데 이렇게만 하면 문제가 발생한다. 왜냐하면 이 시점에서 정적 자원들을 웹서버로 접근할 때의 URL이 정해지지 않았기 때문이다. 테마가 나오지만, 이미지나 자바스크립트는 404 에러를 내면서 죽을 것이다. 당연히 웹서버에 현재 /themes 디렉토리에 대해 URL로 접근할 수 있도록 처리해야 한다. 플러그인의 WP_PLUGIN_DIR과 WP_PLUGIN_URL 설정이 두 개로 나뉘어져 있는 것과 동일한 이치다.

    이 때는 아래와 같은 필터를 통해 해결할 수 있다.

    add_filter( 'theme_root_uri', function( $theme_root_uri, $site_url, $stylesheet_or_template ) {  
      $theme_root_path = dirname( __DIR__ );
      if( strpos( $theme_root_uri, $theme_root_path ) === 0 ) {
        return substr( $theme_root_uri, strlen( $theme_root_path ) );
      }
      return $theme_root_uri;
    }, 10, 3 );

     합쳐진 MU 플러그인의 레시피는 아래와 같다. 단, 완벽한 동작을 보장할 수 없으니 개발용으로만 참고하시라.

    <?php
    
    add_filter( 'theme_root_uri', function( $theme_root_uri, $site_url, $stylesheet_or_template ) {  
      $theme_root_path = dirname( __DIR__ );
      if( strpos( $theme_root_uri, $theme_root_path ) === 0 ) {
        return substr( $theme_root_uri, strlen( $theme_root_path ) );
      }
      return $theme_root_uri;
    }, 10, 3 );
    
    register_theme_directory( dirname(__DIR__) . '/themes' );
    

     

  • 특정 포스트의 권한을 제어하는 레시피

    User Role Editor 보다 더욱 세밀한 권한 체크를 진행시킬 수 있는 레시피. ‘user_has_cap’ 필터를 잘 활용하면 된다.

    좀 더 구체적인 예로 설명을 하자. 만일 내가 포스트 아이디 1141번을 임시 글로 등록해 두었다고 가정하자. 그리고 단지 이 포스트에 대해서만은 editor들은 편집을 허용하지 않게 만들고 싶다. 그렇다면 다음처럼 코드르 만들 수 있다. 커스텀 포스트 타입은 ‘music_collection’이고 적절히 이에 따라 권한 세트를 생성했다.

    add_filter( 'user_has_cap', 'my_user_has_cap', 10, 4 );
    
    function my_user_has_cap( $all_caps, $caps, $args, \WP_User $user ) {
    
      if ( sizeof( $caps ) && in_array( $caps[0], array( 'edit_music_collections', 'edit_others_music_collections' ) ) ) {
    
        if ( sizeof( $args ) >= 3 && $args[2] == 1141 ) {
    
          if ( $user->has_cap( 'editor' ) ) {
    
            unset( $all_caps[ $caps[0] ] );
          }
        }
      }
    
      return $all_caps;
    
    }

     콜백 함수는 4개의 인자를 가지고 있으며, 이 인자의 확인이 은근히 복잡하다. 디버깅 및 설명의 편의를 위해 하나의 if로 처리하지 않고 3개의 if로 잘라 넣었다. 실전 코드에서는 당연히 하나로 붙이도록 하자.

    첫번째 if는 현재 요구되는 primitive 권한을 체크한다. 어떤 커스텀 포스트에서 요구하는 권한이 내가 제어하고 싶은 범위의 것인지를 검사한다.

    두번째 if는 특정 단일 포스트에 대한 권한 요구인지, 아니면 전반적인 복수의 포스트에 대한 요구인지를 체크한다. args에 인자로 0번째는 매핑되기 전의 권한 템플릿 이름, 1번째는 유저의 ID, 2번째로 post의 ID가 있다. 이 부분이 매우 중요하다. 단일 포스트의 권한 검사이므로 args 길이는 3이상이어야 한다.

    세번째 if에서 다시 사용자의 권한 체크를 한다. 지금 권한 체크에 대한 콜백 함수인데, 다시 권한 체크를 재귀적으로 부르고 있다. 권한 필터링을 실제 구현하는 것이 은근히 쉽지 않음을 짐작할 수 있다. 물론 익숙해지면 이야기는 다르겠지만, 주 역할과 메타 역할의 개념 등등을 잘 이해하지 못하면 이 권한 체크에 번번히 실패할 가능성이 높다.  재귀 함수의 오버헤드를 줄이려면 바로 $user->caps 를 활용할 수도 있다.

    이렇게 설정하면,  edtor들은 1141번 포스트에 대해 다음과 같은 화면을 만나게 된다.

    특정 포스트를 특정 권한에게 접근 제한했을 때의 결과 예제 스크린샷

    글에는 두 가지 draft가 있다. 첫번째 draft는 편집이 가능하지만, 두번째 draft는 접근이 막혀 있다. 이렇게 단일 포스트 단위로 접근을 제어하는 방법은 우리가 흔히 접할 수 있는 user role editor로도 쉽게 구현하기 어려운 기능이다. 게다가 코어의 자연스러운 접근 권한 체크를 하기 때문에 엉뚱한 곳에서 적당히 땜빵 코드로 접근을 막는 것보다 더욱 확실하고 자연스러운 제어가 가능하다. 스크린샷으로도 보이듯 접근 제어가 안 되는 항목은 확실하게 UI적으로도 막혀 있는 것이 보인다. 이렇게 구현하면 예상치 못한 URL로 접근하더라도 코어가 확실히 접근을 차단해 줌을 기대할 수 있는 것이다.

    물론 보통은 커스텀 포스트에 이 정도로 세밀한 접근 제어 기능을 구현하지는 않으나, 이와 유사한 요구 사항은 실무에서 많이 발생할 수 있을 것이라 생각한다. 이걸 자유자재로 사용할 수 있다면 정말 훌륭한 플러그인 구현이 되리라 생각한다.

    워드프레스의 역할과 권한은 잘 이해하는 사람도 흔치 않을 것 같다 코드가 지저분하든 더럽든 어쨌든 간에, 나는 워드프레스가 훌륭한 CMS라고 생각하며 그 근거 중의 하나로 이 강력하고 유연한 역할과 권한 시스템을 든다.  이걸 사용자가 쉽고 간편하게 쓸 수 있는 UI가 없는 것은 아쉽지만 (아니, 그런 UI를 쉽게 사용하게 만드는 것 자체가 미친 난이도지만) 이러한 시스템이 기저에 있다는 것 자체가 놀라움이다.

    덧글 ) 권한 체크는 상당히 어렵다. 비활성화된 항목에 마우스를 가져다 대어 보자.

    어이쿠, 이게 뭔가. Trash? 편집은 못하지만 지울 수는 있다. 편집자 역할은 휴지통에 있는 글도 영구 삭제 가능하다. 물론 같은 스태프끼리 그럴 일은 없겠지만… 아, 권한 체크는 세심해야 함을 강조한다. 해당 권한 목록을 꼼꼼하게 살펴서 이런 구멍이 없도록 잘 대비하기를 권한다.

  • 어드민 화면의 열 수를 1개로 고정하는 레시피

    add_filter( 'screen_layout_columns', function ( $columns ) {
      $screen = get_current_screen();
      $columns[ $screen->id ] = 1;
      return $columns;
    } );
    
    add_filter( 'get_user_option_screen_layout_kpm_paper', function ( $value ) {
      return 1;
    } );
    
    add_action( 'in_admin_header', function () {
      $screen = get_current_screen();
      if ( $screen->id == 'kpm_paper' ) {
        $screen->remove_option( 'layout_columns' );
      }
    } );

     커스텀 포스트에 활용할 수 있다.