[작성자:] changwoo

  • 커스텀 포스트 레이블 집중분석

    커스텀 포스트 레이블은 꽤 많은 경우 사소한 문제로 넘어가게 된다. 보통 커스텀 포스트를 워드프레스 기본 포스트 타입인 ‘포스트’와 유사하게 별도의 작성자가 꾸준히 어떤 콘텐츠를 작성(주로 수동으로)해 발행하기 위한 용도로는 잘 사용해 본 적이 없기 때문이다.

    그러다 보니 레이블에 대한 인식은 많이 낮았다. 또 레이블에 별로 신경 쓰지 않아도 코어가 ‘post’나 ‘page’ 타입에 의거해 기본적인 레이블을 만들어 주니 신경 쓸 것이 거의 없었기도 했고.

    register_post_type 레퍼런스를 보면 꽤나 많은 입력 인자가 있는데, 이 중에서도 상당수를 차지하는 것이 레이블이다. 사소하기는 하나 이 레이블들이 어디에서 어떻게 쓰이고 있는지 꼭 한번은 체크해 보고 싶었다. 물론 이것은 로직과는 별개로 그저 ‘텍스트’의 취향일 뿐이라고 할 수도 있다. 그리고 대략의 문서를 보면 어떤 텍스트를 넣어야 하는 건지도 바로 이해할 수도 있다.

    그러나, 궁금했다. 이렇게 열심히 세부 옵션을 넣어 텍스트를 지정해 주면 도대체 UI 어디에서 이 레이블을 활용하고 있는 건지, 이 레이블이 실제로 어디서 어떻게 쓰이는지 조목조목 따져 보고 싶었다. 그렇게 알게 되면 각 레이블에 대한 이해도도 높아지지 않을까?

    원래 커스텀 포스트에 대해 포스팅을 하면서 한꺼번에 처리하려고 했지만, 의외로 레이블의 세부 옵션이 많아서 이것만 따로 포스팅하는 것이 낫겠다 싶어 별도로 작성한다.

    자, 그럼 지루하기는 하지만 커스텀 포스트의 레이블이 어떤 식으로 사용되나 보도록 하자.

    레이블과 관련된 옵션

    여기서 레이블(label)이란 것은 UI 상에 표시되는 텍스트를 말한다. 다들 알다시피 워드프레스는 다국어 설정이 가능하다. 그러므로 각 언어 설정에 따라 표시되는 텍스트는 달라질 수 있다. 그래서 어떤 UI 상에 어떤 텍스트를 표시하려면 두 가지 조건이 필요하다. 바로 언어 도메인과 문맥이다. 언어 도메인은 워드프레스가 어떤 언어로 표시될 지를 지시하고 문맥은 어떤 상황의 텍스트를 출력해야 하는지를 지시한다.

    레이블과 관련된 커스텀 포스트의 옵션은 크게 두 가지가 있다. label과, labels이다. 사실 두 옵션을 다 사용할 필요는 없고, 커스텀 포스트를 만들 때 둘 중 하나만 써도 된다. label은 아주 심플하게 레이블을 만들고 싶을 때 (다시 말해 레이블에 그다지 신경을 쓰지 않아도 좋을 때), 반면 labels는 각 레이블에 대해 세부적인 문맥에 맞춰 텍스트를 지정해 줄 수 있다.

    label 옵션

    가장 기본으로 사용할 수 있는 것이 label 옵션이다. 아주 간단한 용도의 커스텀 포스트라면 이 옵션만 사용해도 큰 문제는 없다. 나머지 세세한 레이블 텍스트는 기본 타입인 ‘post’를 기준으로 적절히 메꿔지기 때문이다. 왠만큼 까다로운 사람이 아니라면 이 것 때문에 불편을 호소할 일은 거의 없을 것이다.

    custom_post_label_001

    이 옵션만을 주고 ‘wphack’ 타입의 커스텀 포스트를 만들었다. 레이블 이름은 ‘HackPosts’로 정했다. 보통 복수로 레이블 이름을 주는 것이 관례이다. 메뉴는 ‘post’ 타입에 준해 적절히 보간되고 번역되었다.

    코드 내부에서 labels를 발견하지 못하고 label만 있다면 label에 의거해 모든 labels 세부 텍스트 항목을 만들어 내서 커스텀 포스트를 등록한다. 그러니까 커스텀 포스트를 만들면서 label과 labels를 동시에 쓸 필요는 없다.

    labels 옵션

    커스텀 포스트의 각 레이블을 보다 세심하게 신경 써 주어야 할 경우도 있을 수 있을 것이다. 이 경우를 위해 워드프레스는 labels라는 옵션을 추가로 제공한다. 이 옵션에서는 커스텀 포스트를 생성하면 생각할 수 있는 모든 경우에 대해 상세하게 텍스트를 지정해 줄 수 있다. 사실 본 포스팅의 주 목적.

    우선 세부 옵션부터 나열해 보자.

    • name
    • singular_name
    • add_new
    • add_new_item
    • edit_item
    • new_item
    • view_item
    • search_items
    • not_found
    • not_found_in_trash
    • parent_item_colon
    • all_items
    • archives
    • insert_into_item
    • uploaded_to_this_item
    • featured_image
    • set_featured_image
    • remove_featured_image
    • use_featured_image
    • menu_name
    • filter_items_list
    • items_list_navigation
    • items_list
    • name_admin_bar

    각 세부 옵션의 이름으로 이 텍스트가 무엇인지 추측을 할 수 있다. 그래서 적절하게 번역 텍스트를 주면 어차피 텍스트니 프로그램이 돌아가는 데는 큰 지장은 없다. 그렇지만 나는 확실히 이 텍스트가 어디서 어떻게 나오는지를 알아보고 싶었다.

    name 레이블 옵션

    포스트 타입에 대한 일반적 이름이다. 워드프레스 UI 전반에서 이 포스트 타입 단위를 지칭할 때 쓰인다. 위 설명된 label 옵션과 동일하게 복수형으로 지으면 된다. label 옵션을 쓰는 것과 labels 옵션에서 이 세부 옵션 하나만 쓰는 것은 사실 같다고 볼 수 있다.

    singular_name 레이블 옵션

    이 포스트 타입 하나의 개체를 지칭할 때 쓰이는 이름이다. 사실 UI 상에서는 이 텍스트가 보이는 일은 그리 흔치 않다. 하나 예를 들자면 아래 그림처럼 어드민 바에서 새로운 개체를 작성할 때의 레이블로 사용된다. (정확히 설명하자면 name_admin_bar 세부 옵션의 기본값이 singular_name 옵션값이기 때문이다)

    만일 name만 있다면 singular_name은 name을 따라 간다. 복수, 단수 구분 없이 무조건. 그리고 name 마저도 없다면 이보다 한 단계 위의 label 옵션에서 name을 가져 온다. 그럼 label 옵션 마저 없다면? label의 기본값은 Posts, 혹은 Pages이다. 구조적인 타입이라면 Pages고 비구조적이라면 Posts로 이름이 만들어질 것이다.

    custom_post_label_002

    한국어에서는 단수/복수 개념이 뚜렷하지 않긴 하지만, 어쨌든 옵션 이름이 의미하는 것처럼 단수로 지정해 주어야 한다.

    참고로 Piklist라는 빠른 개발용 플러그인이 있다. 여기서 별도의 훅을 지정해 커스텀 포스트를 생성할 수 있도록 하는 기능을 제공한다. register_post_type 함수를 래핑하여 보다 편하게 쓸 수 있게 만든 것인데, 여기서는 name 인자를 하나만 넣어 줘도 영어 단수/복수 규칙을 추측해 singular_name, plural name을 지정하는 기능을 갖추고 있다. 왠지 영어권 사용자에게는 나름 유용해 보인다.

    add_new 레이블 옵션

    메뉴에 나오는 ‘Add New’ 텍스트를 변경할 수 있다. 이 텍스트는 명백하게 두 곳에서 사용되고 있다.

    custom_post_label_003

    어드민 메뉴의 메뉴 항목과 페이지 h1 문단 제목 우측의 버튼의 텍스트로 사용된다.

    add_new_item, edit_item 레이블 옵션

    add_new_item은 새 개체 작성 페이지의 h1 문단 제목으로 사용된다. ‘_item’ 접미의 item은 해당 단수 개체 이름이라고 생각하면 된다. 마찬가지로 edit_item은 개체 편집 페이지의 h1 문단 제목으로 사용된다. 아래 그림은 이 옵션을 지정하지 않은 것과 한 것을 비교한 것이다.

    custom_post_label_004

    new_item 레이블 옵션

    UI 전반에서 사용되는 레이블은 아닌 것으로 보인다. 코드의 완전성을 위해 넣은 듯하다.

    view_item 레이블 옵션

    개체 편집 화면의 어드민 바에는 프론트 화면에서 작성된 개체가 어떻게 보이는지 바로 확인하는 메뉴가 삽입된다. 이 옵션으로 그 텍스트를 변경할 수 있다.

    custom_post_label_005

    또 참고로 이 레이블 텍스트는 댓글(Comments) 목록 화면의 ‘댓글이 달린 글(In Response To)’ 칼럼에서 사용된다. ‘글 보기(View Post)’ 링크 텍스트가 바로 그것이다. 물론 이것은 기본인 ‘post’ 타입에만 해당되겠지만.

    custom_post_label_006

    search_items 레이블 옵션

    개체 목록을 검색하기 위한 검색 버튼의 텍스트를 변경할 수 있다.

    custom_post_label_007

    not_found, not_found_in_trash 레이블 옵션

    not_found 레이블 옵션은 개체 검색 결과가 없는 경우에 이를 알리기 위한 텍스트를 위해 주어진다. 마찬가지로  not_found_trash 레이블 옵션은 휴지통에서 검색한 경우의 텍스트이다.

    custom_post_label_008

    parent_item_colon 레이블 옵션

    포스트 타입을 계층적(hierarchical)으로 설정한 경우 개체의 부모 개체를 설정해 줄 수 있다.

    사실 이 레이블은 UI 상에서 찾아 보기가 어렵다. 이 텍스트가 명시적으로 보이는 한 예는 이렇다. 포스트 목록에서 부모 포스트가 삭제되거나 휴지통으로 이동하여 부모-자식 위계에 영향을 주는 사안이 발생한 경우 자식 포스트에서 부모 포스트가 어떤 것인지 표시해 줄 때 사용된다.

    아래 그림처럼 부모 – 자식 관계가 있는 페이지 목록이 있다고 보자.

    custom_post_label_009

    그리고 부모 페이지인 ‘Parent Page’를 휴지통으로 보내고 난 다음 목록을 다시 불러오면 아래처럼 화면이 변경된다.

    custom_post_label_010

    텍스트 구조는 이렇다: | parent_item_colon <부모 개체의 이름>

    한편 포스트 타입 구조가 아닌 카테고리에 해당하는 케이스지만 비슷한 사례라 기록을 남겨 본다. 아래 그림은 ‘post’ 편집 화면에서 카테고리 설정을 위한 메타박스 UI이다.

    워드프레스에는 시각적으로 표시되지는 않는 여러 텍스트들이 HTML 코드 상에 존재한다. 시각장애인을 위해 존재하는 들리는 텍스트들이다. 그림의 붉은 상자로 표시된 쪽의 입력 상자를 위한 들리는 레이블이 존재한다. 이 레이블의 텍스트로 카테고리의 parent_item_colon 속성이 사용된다.

    custom_post_label_011

    HTML 보면 아래 코드와 같이 출력된다.

    <label class="screen-reader-text" for="newcategory">Add New Category</label>
    <input type="text" name="newcategory" id="newcategory" class="form-required form-input-tip" value="New Category Name" aria-required="true"/>
    <label class="screen-reader-text" for="newcategory_parent">
      Parent Category:
    </label>

    label for=”newcategory_parent” 태그를 보면 Parent Catecory: 라는 텍스트를 확인할 수 있다. 포스트 레이블이 아닌 카테고리 레이블의 용례이지만, 둘 다 엇비슷하므로 같이 남겨 본다.

    all_items 레이블 옵션

    ‘모든 개체들’의 의미를 지닌 텍스트를 출력하기 위해 지정할 수 있다.

    커스텀 포스트를 생성하면 나오는 하위 메뉴로 ‘모든 글’, ‘모든 페이지’ 같은 ‘모든 개체’ 항목이 자동으로 생성된다. 이 때 이 레이블이 사용된다.

    한편 커스텀 포스트의 ‘show_in_menu’ 옵션은 불리언 혹은 문자열을 넣을 수 있다. 불리언의 경우는 메뉴에 보일 것인지 숨길 것인지 정하기 위해 사용한다. 그런데 인수로 문자열로도 입력할 수도 있는데, 이렇게 되면 해당 메뉴의 하위 메뉴에 커스텀 포스트 UI가 삽입된다.

    예를 들어 ‘show_in_menu’의 값으로 ‘tools.php’ 문자열을 입력하면 HackPosts UI는 도구(Tools) 메뉴의 하위 메뉴로 이동하게 된다. 이 때에도 레이블의 텍스트로 all_items 레이블 옵션이 사용된다.

    custom_post_label_012

    다른 예로 두 유저간에 개체 편집 충돌이 일어나는 경우에 출력된다. 가령 사용자 A가 편집 중인 개체를 사용자 B도 편집하기 위해 해당 URL로 접근한다고 생각해 보자. 그러면 사용자 B는 다음과 같은 메시지를 받음으로써 사용자 A가 편집 과정에 있음을 통지받는다.

    custom_post_label_013

    이 때 사용자 B가 Take Over 단추를 눌려 편집권을 이양받게 되면, 편집 중이었던 사용자 A는 이런 메시지를 실시간으로 전달받게 된다. 이 때 all_items 레이블이 사용된다. (설명은 장황한데, 정말 너무 사소한 부분이다…)custom_post_label_014

    archives 레이블 옵션

    메뉴 편집 화면에서 해당 타입의 아카이브를 위한 레이블이다. 아래 그림을 보자.

    custom_post_label_015

    ‘All HackPosts Items’라고 나온 것은 archives 레이블 옵션이 정해지지 않았을 때이다. 이 값이 없다면 all_items레이블 옵션에서 가져오기 때문에 all_items 레이블이 출력된다.

    custom_post_label_016

    archives 레이블을 적절히 조절하면 위 그림처럼 별도의 레이블을 가지게 된다.

    insert_into_item 레이블 옵션

    custom_post_label_017

    미디어 삽입 화면에서 개체에 미디어를 삽입시키는 버튼 텍스트로 사용된다.

    uploaded_to_this_item 레이블 옵션

    ‘미디어 추가(Add Media)’ 버튼을 클릭하면 나오는 미디어 관리 창에 속한 미디어 필터의 한 레이블 텍스트. 아래 그림과 같은 위치에 있다.

    custom_post_label_018

    이 아이템으로 업로드된 미디어란 의미는 로 연결되었다는 의미이다. 현재 편집 중인 화면에서 미디어 추가를 눌러 파일을 업로드하면, 해당 업로드 파일은 편집 중인 개체에 특정지어 업로드 되었다고 간주된다. DB에서는 attachment 포스트의 post_parent 필드가 현재 편집 중인 post ID로 대입된다는 의미로 볼 수 있다.

    featured_image, set_featured_image, remove_featured_image 레이블 옵션

    support 옵션에 ‘thumbnail’을 추가하면 커스텀 포스트도 특성 이미지를 지정할 수 있다. 특성 이미지라는 이름을 다른 것으로 변경하기 위해 featured_image를 사용한다. 이름으로 추측 가능하다. 다들 특성 이미지 관련 UI에서 레이블로 사용된다.

    custom_post_label_019

    use_featured_image 레이블 옵션

    이 레이블이 직접적으로 사용된 UI를 찾기가 쉽지 않다. 왜냐하면 이 레이블이 코어에서 직접적으로 쓰이는 곳은 단 한 곳, wp-admin/includes/media.php 파일의 get_media_item() 이라는 함수 뿐이다.

    get_media_item 함수는 이미지 첨부 메타데이터를 수정할 수 있는 HTML 폼을 출력하는데, 워드프레스 관리 UI 상에서 일반적인 방법으로는 이 주소가 명시적으로 나타난 곳이 없다. 최근 버전의 워드프레스는 미디어 파일의 조회와 수정은 post.php에서 모두 처리하도록 되어 있다.

    그렇지만 이 화면을 완전히 볼 수 없는 것은 아니다. 직접 주소를 쳐서 들어가면 된다. 다음과 같은 경로로 접근하면 된다.

    <site_url>/wp-admin/media.php?action=edit&attachment_id=<attachment_id>&post_id=<post_id>

    action, attachment_id 두 파라미터는 정상적으로 화면이 출력되기 위해 필수적으로 있어야 하며, 특성 이미지 속성까지 출력시키려면 post_id 파라미터가 별도로 필요하다. 이 때 attachment_id는 attachement 포스트의 ID, post_id는 특성 이미지를 집어 넣고 싶은 포스트의 ID이다. 올바르게 입력하면 아래 그림과 같은 화면이 출력된다. 목록 마지막에 use_featured_image 레이블 옵션이 나오는 것을 확인할 수 있다.

    custom_post_label_020

    menu_name 레이블 옵션

    기본값이 name 옵션과 같은 이것은 관리자 메뉴의 레이블을 별도로 지정하고 싶을 때 사용된다.

    custom_post_label_021

    filter_items_list, items_list_navigation, items_list 레이블 옵션

    이 레이블들은 숨겨져 있고 스크린 리더를 위한 텍스트로 입력된다. 개체 목록 UI 화면에서 <h2 class=’screen-reader-text’>를 검색하면 해당 레이블 값이 나오는 것을 확인할 수 있다.

    filter_items_list는 필터 버튼을 위한 스크린 리더 텍스트이다.

    <h1>HackPosts
        <a href="http://wphack.vagrant:8080/wp-admin/post-new.php?post_type=wphack" class="page-title-action">
            Add New HackPost
        </a>
    </h1>
    <h2 class='screen-reader-text'>
        Filter HackPosts List
    </h2>
    <ul class='subsubsub'>
    ...

    items_list_navigation은 개체의 수가 많아 몇 개의 페이지로 나뉘어 질 때 페이지네이션을 위한 스크린 리더 텍스트로 사용된다.

    <h2 class='screen-reader-text'>
        HackPosts List Navigation
    </h2>
    <div class='tablenav-pages'>
        <span class="displaying-num">6 items</span>
        <span class='pagination-links'>
        ...

    items_list는 테이블 목록 바로 위에 있다.

    <h2 class='screen-reader-text'>HackPosts List</h2>
    <table class="wp-list-table widefat fixed striped pages">

    name_admin_bar 레이블 옵션

    어드민 바의 ‘+ 새로 추가 (+ New)’ 메뉴에 새 개체를 삽입하는 명령을 추가할 수 있다. 이 때의 레이블 옵션을 별도로 지정할 수 있다. 이 옵션이 없다면 singular_name 레이블 옵션을 가져와 사용한다.

    custom_post_label_022

    마치며

    어떤 레이블이 UI 어디에서 어떻게 쓰이는지에 대해 굉장히 궁금했었다. 물론 코덱스에 있는 대로 적당히 뉘앙스나 문맥만 파악한 후 개체에 맞게 말을 고쳐 주면 그만일 수도 있지만, 그 레이블이 나오는 정확한 문맥을 알 수 없었기에 과연 올바른 의미를 가진 레이블로 제공되는지 확실히 알 수가 없었다. 이번 기회로 각 레이블이 어떤 상황에서 어떻게 쓰이는지 명확히 파악해 보았다. 이젠 보다 자신있게 커스텀 포스트의 레이블을 다룰 수 있으리라 생각한다.

  • 내가 어쩌다?

    내가 어쩌다 이렇게 워드프레스 개발에 깊숙히 들어오게 된 걸까?

  • 우커머스 결제/배송 정보 관련 의문점

    의문점.

    결제 관련 정보(Billing details)에서는 결제자의 이메일과 전화번호를 적는 란이 있다. 그런데 배송 정보(Shipping details)에는 그렇지 않다. 조금 전 생짜 워드프레스에 우커머스만 올려 놓고 테스트를 해 배송 정보에는 이메일과 전화번호 필드가 포함되지 않음을 확인했다.

    간혹 우커머스의 결제와 배송이 서로 다른 경우에는 어떻게 하란 말인가? 한국에서는 배송을 할 때 배송 상자에 받는 사람의 전화번호도 같이 적게 되어 있다. 그리고 택배 기사님들이 보통 수신인이 부재중일 경우 이 전화로 전화를 걸기도 하고. 결국 한국 상황에 맞춰 일괄적으로 기능 확장을 하는 수 밖에 없지 않은가?

  • Korean Conversation #1

    오늘 처음으로 한국어 대화 자원 봉사에 참가해 보았다. 여러 사람을 만날 수 있는 좋은 기회가 된 것 같다.

    첫 만남은 류선우 씨. 오스트리아 사람.

    • “오늘은 커피가 땡긴다.”
    • “그들은”에서 “그 애들은”이나 “걔네들은” 이란 표현을 알려 줌

    둘째 만남은 뮤리엘. 미국 플로리다의 사람. 춤을 좋아하고 댄서가 되고 싶어하는 현 영어 강사.

    • 계피는 좋지만 넛멕(nutmeg)이라는 알 수 없는 향신료는 싫어한다는 사람.
    • 홍대는 시끄러워서 싫다고.
    • 사진찍기, 요리하기, 노래하기, 춤추기를 좋아한다네요.

     

     

  • 난 왜 이 짤이 이렇게 좋지?

    1443538166.68

    난 이 짤이 왜 이리 좋지? 🙂

  • 커스텀 포스트 검색: 커스텀 필드도 포함되도록 조정

    이번 포스팅은 포스트 목록에서 커스텀 필드(메타 테이블 값)도 검색되게 하는 방법에 대해 적고자 합니다. 거두절미하고 커스텀 포스트를 만들어 보겠습니다.

    커스텀 포스트 생성

    포 스트 타입은 ‘my-test-post’입니다. 그냥 별다른 설정 없이 기본값으로만 만들었어요. 그리고 이 타입의 포스트에는 ‘my_test_value_1’, ‘my_test_value_2’라는 커스텀 필드를 가지도록 설정을 했습니다. 플러그인이 활성화 되면 임시로 2개의 포스트를 자동 생성하도록 했습니다. 그리고 비활성화되면 해당 포스트와 커스텀 필드는 삭제하도록 만들었습니다.

    커스텀 포스트 화면
    커스텀 포스트 화면
    커스텀 필드와 같이 커스텀 포스트 화면
    포스트 편집 화면. 아래 커스텀 필드를 입력할 수 있도록 설정되었습니다.

    ※ 스크린샷을 찍을 때는 편의를 위해 별도의 플러그인을 사용해 커스텀 필드를 생성했습니다만, 나중에 플러그인 구현에는 그냥 커스텀 포스트를 생성하는 것으로 변경했습니다. 플러그인 소스를 직접 활성화 했을 때는 위 스크린샷과는 약간 다를 수 있습니다.

    기본적인 포스트 검색

    이 렇게 커스텀 포스트를 생성한 후, 포스트 목록에서 몇몇 키워드를 넣고 검색을 해 봅니다. 여기 검색은 그다지 똑똑한 편이 아니라 키워드를 정확하게 넣어야만 합니다. 그런데 여기서 제가 지적하고픈 문제가 생깁니다. 검색은 포스트 제목과 내용에만 국한된다는 점입니다. 아마 기본 포스트 목록도 마찬가지일 것입니다.

    보통 커스텀 포스트는 단지 포스트 제목(post_title), 포스트 내용(post_content), 혹은 발췌(post_excerpt) 같은 포스트 테이블(보통 wp_post 테이블) 기본 필드 뿐만 아니라 메타 테이블(보통 wp_postmeta)을 활용한 다양한 커스텀 필드를 활용하고 싶기 때문에 생성합니다. 또한 그런 커스텀 필드들은 별도의 조정을 해서 목록 테이블에서도 표시되도록 만들기도 하구요. 바로 아래 그림처럼 말이죠.

    커스텀 포스트의 칼럼. 필요로 하는 커스텀 필드를 별도로 표시하고 있습니다.
    커스텀 포스트의 칼럼. 필요로 하는 커스텀 필드를 별도로 표시하고 있습니다.

    위 그림은 위치 정보를 기록하기 위한 다른 형태의 커스텀 포스트입니다. 위도/경도를 출력하는 ‘Position’ 칼럼, 지도의 상세 주소를 표시하는 ‘Address’ 칼럼, 그리고 그 장소에 대해 임의로 평점을 매기는 ‘Rating’ 칼럼은 워드프레스 자체에서는 정의되지 않았죠. 그러나 커스텀 포스트를 통해 저런 별도의 값도 저장하도록 확장이 가능하고, 또 이렇게 목록에 원하는 형태로 출력되록 조정도 가능합니다.

    이렇게 테이블이 구성되면 당연히 사용자는 “주소”를 기반으로 검색이 될 거라 생각할 수 있습니다. 가령 “서울특별시”라는 검색어로 서울에만 있는 장소만 나타나게요. 그러나 주소는 커스텀 필드이므로 아무리 검색을 해도 나타날 수가 없답니다. 왜냐면 커스텀 필드는 기본 검색 대상이 아니니까요. 다시 말씀드리지만 기본 검색 대상에 포함되는 것은 단지 포스트 제목과 본문 내용 뿐입니다.

    검색 작동 방식 분석

    URL 테스트

    이 렇게 커스텀 필드가 검색에서 제외될 수 밖에 없는 이유는 당연합니다. 코어가 어떤 커스텀 값을 기준으로 검색해야 할지 알 수 없기 때문이죠. 알게 모르게 하나의 포스트에는 여러 메타 키와 값들이 붙습니다. 그 중에는 사용자가 지정한 값들이 있을 수도 있고, 아니면 시스템이 관리 목적으로 붙여둔 것들도 있습니다. 그러므로 사용자가 별도의 플러그인을 작성하여 명시적으로 그 값이 검색되도록 흐름을 수정하는 수 밖에는 없습니다.

    워드프레스 관리자 화면에서 검색어를 넣어 질의를 하면 URL이 변경됩니다. 예를 들어 “테스트”라는 검색어를 넣으면 다음과 같은 URL로 연결이 됩니다. 여기서 검색어와 관련된 쿼리 변수는 ‘s’임을 쉽게 눈치챌 수 있겠죠?

    edit.php?s=테스트&post_status=all&post_type=my-test-post&action=-1&m=0&paged=1&mode=list&action2=-1

    그렇다면 워드프레스는 어떤 작업을 통해 “s=테스트”를 실제 DB 검색까지 되도록 만들까요? 코어가 동작하는 흐름을 따라가보면 알 수 있겠죠.

    워드프레스 코어의 흐름 따라가 보기

    워드프레스 버전 4.2.2 기준, wp-admin/edit.php 163번째 줄에서 테이블이 표시될 아이템을 준비하는 코드가 나옵니다. 이 부분은 프로그램 코드를 따라가므로 설명이 약간 복잡할 수 있습니다.

    $wp_list_table->prepare_items();
    

    prepare_items() 메소드는 wp-admin/includes/class-wp-posts-list-table.php 소스의 ‘WP_Posts_List_Table’ 클래스에서 정의된 것입니다. 해당 부분인 105번째 줄을 살펴 보면, wp_edit_posts_query() 함수가 호출됩니다.

    $avail_post_stati = wp_edit_posts_query();
    

    이 함수는 wp-admin/includes/post.php 975번째 줄에 정의되어 있습니다. 여기서 1048번째 줄에 wp() 함수가 호출되는 것을 찾을 수 있습니다.

    한 편 wp() 함수는 전역으로 선언되어 있는 WP 인스턴스의 main() 메소드를 호출합니다. WP 클래스는 wp-includes/class-wp.php 에 선언되어 있고 main() 메소드는 이 파일 607줄에서 찾을 수 있습니다. 전역으로 선언된 WP 인스턴스는 wp-settings.php 279번째 줄에서 찾을 수 있습니다.

    $GLOBALS['wp'] = new WP();
    

    URL 로 질의한 내용을 바탕으로 포스트를 가져오는 단계는 WP::query_posts() 입니다. 이 메소드 안에서 WP_Query 클래스의 query() 메소드를 호출합니다. WP_Query는 wp-includes/query.php 라인 837에 선언되어 있습니다.

    WP_Query::query()는 WP_Query::get_posts()를 호출합니다(라인 2380). WP_Query::get_posts() 안에서 WP_Query::parse_search()가 호출됩니다(라인 2684).WP_Query::parse_search() 함수(라인 2055)는 SQL 쿼리의 일부분을 만들어냅니다. 아래는 WP_Query::parse_search() 결과의 한 예입니다.

    AND (((wp_posts.post_title LIKE ‘%테스트%’) OR (wp_posts.post_content LIKE ‘%테스트%’)))

    정리하면 이렇습니다.

    • edit.php: WP_Posts_List_Table::prepare_items() 호출
    • WP_Posts_List_Table::prepare_items() 에서 wp_edit_posts_query() 호출
    • wp_edit_posts_query() 에서 WP::main() 호출
    • WP::main(), 여기서 WP::parse_request() 호출, 다시 WP::query_posts() 호출
    • WP::query_posts() 에서 WP_Query::query() 호출, WP_Query::get_posts() 호출, WP_Query::parse_search() 호출

    검색 쿼리 부분 결과를 보면 포스트 제목과, 포스트 내용만 가지고 LIKE 질의를 만들고 있습니다. 이렇기 때문에 포스트들은 제목과 내용 밖에 검색되지 않는 것입니다.

    커스텀 필드가 검색되도록 수정

    그렇다면 커스텀 필드를 검색할 수 있도록 SQL을 수정하도록 해 보지요. 우선 쿼리를 해석하는 과정에서 메타 필드를 검색하도록 조정해야 합니다. 이 때 ‘pre_get_posts’ 액션을 사용하는 것이 적절해 보입니다. 왜냐하면 WP_Query::get_posts() 메소드의 구현을 보면, WP_Query::parse_query() 메소드 호출 이후 처음 불리는 액션이기 때문입니다. 여기서 해석된 쿼리를 일부 변경하는 것이 가능합니다.

    add_action( 'pre_get_posts', 'my_test_get_posts' );
    function my_test_get_posts( $query ) {
       global $pagenow;
       global $post_type;
    
       $term = $query->get( 's' );
    
       if( !is_admin() || $post_type != 'my-test-post' || $pagenow != 'edit.php' || empty( $term ) ) {
          return;
       }
    
       $meta_query = $query->get( 'meta_query' );
    
       $search = array(
          'relation' => 'OR',
          array(
             'key'     => 'my_test_value_1',
             'value'   => $term,
             'compare' => 'LIKE'
          ),
          array(
             'key'     => 'my_test_value_2',
             'value'   => $term,
             'compare' => 'LIKE'
          )
       );
    
       $meta_query[] = $search;
       $query->set( 'meta_query', $meta_query );
    }

    그런데 아직은 문제가 있습니다. 커스텀 필드의 값으로 검색해도 값은 검색되지 않을 것입니다. 완성된 SQL 쿼리를 보면 이유를 알 수 있습니다.

    SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
    INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id )
    WHERE 1=1
    AND (((wp_posts.post_title LIKE ‘%다섯%’) OR (wp_posts.post_content LIKE ‘%다섯%’)))
    AND ( (
    ( wp_postmeta.meta_key = ‘my_test_value_1’ AND CAST(wp_postmeta.meta_value AS CHAR) LIKE ‘%다섯%’ )
    OR ( wp_postmeta.meta_key = ‘my_test_value_2’ AND CAST(wp_postmeta.meta_value AS CHAR) LIKE ‘%다섯%’ )
    ) )
    AND wp_posts.post_type = ‘my-test-post’
    AND (
    wp_posts.post_status = ‘publish’
    OR wp_posts.post_status = ‘demo’
    OR wp_posts.post_status = ‘lock’
    OR wp_posts.post_status = ‘pending’
    OR wp_posts.post_status = ‘future’
    OR wp_posts.post_status = ‘draft’
    OR wp_posts.post_status = ‘demo’
    OR wp_posts.post_status = ‘lock’
    OR wp_posts.post_status = ‘private’
    )
    GROUP BY wp_posts.ID
    ORDER BY wp_posts.post_title LIKE ‘%다섯%’ DESC, wp_posts.post_date DESC
    LIMIT 0, 20

    붉은색으로 된 글씨는, 위에서 메타키와 값을 쿼리 조건으로 주었을 때 생성되는 부분인데. 기본적으로 메타 키/값을 이용해 검색을 할 경우 무조건 ‘AND’로 시작되게 짜여 있습니다. 이렇게 메타 쪽 쿼리를 작성하는 코어 소스는 wp-includs/meta.php 부분입니다. 다행히 바로 저 붉은 색 부분만 조절할 수 있는 필터도 제공되는데, 해당 파일의 1197번째 줄 ‘get_meta_sql’ 필터입니다. 대개 포스트 메타 테이블은 항상 포스트 테이블과 조인되며, AND 조건절로 이어져 진행된다는 가정이 있는 것 같습니다. 하지만 이 경우에는 적합하지 않지요. 이 부분을 변경해야 합니다.

    가장 간단한 해결책은 붉은색 부분의 AND를 바로 OR로 교체하는 것입니다. 코드 마지막 부분에 다음처럼 필터를 걸어 주면 됩니다.

    add_filter( 'posts_search', function ( $sql ) {
    add_filter( 'get_meta_sql', function( $sql ) {
        $sql['where'] = preg_replace( '/\s*AND\s+(.+)/ms', ' OR $1', $sql['where'] );
        return $sql;
    });

    이 때 주의할 점이 있습니다. 연산자가 AND에서 OR로 변경되면서 잠재적인 버그의 요소가 있을 수 있습니다(OR가 연산 순위가 AND보다 낮으므로 AND 연산자가 먼저 고려된 다음 OR가 계산됩니다). 원래 쿼리는 사실상 AND 연산으로만 구성되어 있습니다. 그런데 여기서 불쑥 OR가 발생하므로,

    1=1 AND (포스트 테이블 검색 조건) AND (메타 테이블 검색 조건) AND (포스트 타입 조건) AND (포스트 상태 조건)

    와 같은 구조는

    1=1 AND (포스트 테이블 검색 조건) OR (메타 테이블 검색 조건) AND (포스트 타입 조건) AND (포스트 상태 조건)

    으로 변경됩니다.

    이 때 1=1은 항상 참이고, 또 (포스트 타입 조건) 또한 이미 콜백 함수 처음에 if 문으로 검사를 하므로 여기까지 흐름이 왔다면 항상 참입니다. 그러므로

    (포스트 테이블 검색 조건) OR (메타 테이블 검색 조건) AND (포스트 상태 조건)

    이 경우 (메타 테이블 검색 조건) AND (포스트 상태 조건) 부분이 우선 검사됩니다. 여기서 약간의 문제가 발생할 수 있습니다. 포스트의 제목과 내용이 검색어와 일치하는 경우 포스트 상태에 관계 없이 검색될 수 있다는 점입니다. 원래 검색은 휴지통에 넣지 않은 포스트를 대상으로 해야 하는 것인데, 이렇게 쿼리가 변경될 경우에는 포스트 상태가 지워져도 검색 결과에 포함될 수 있을 것입니다. 이것은 워드프레스의 기본 동작 방식이 아닙니다. 그러므로 쿼리 구조를 대대적으로 변경하여,

    1=1 AND (포스트 테이블 검색 조건 OR (메타 테이블 검색 조건)) AND (포스트 타입 조건) AND (포스트 상태 조건)

    와 같이 WHERE절을 변경해야 할 것입니다. 그러므로 위 필터를 삭제하고 아래 세 개의 필터를 더해 주면 됩니다. 개인적으로 전역 변수의 사용을 꺼리지만, 설명의 편의를 위해 사용했습니다.

    $my_test_search_query = '';
    $my_test_meta_query = '';
    
    // post_title, post_content 검색 쿼리 부분 기록
    add_filter( 'posts_search', function ( $sql ) {
     global $my_test_search_query;
     $my_test_search_query = $sql;
     return $sql;
    } );
    
    // meta 부분 쿼리 기록 (join, where 키로 구성된 array)
    add_filter( 'get_meta_sql', function ( $sql ) {
     global $my_test_meta_query;
     $my_test_meta_query = $sql;
     return $sql;
    } );
    
    // where 절을 최종 편집
    add_filter( 'posts_where', function ( $sql ) {
     global $my_test_search_query, $my_test_meta_query;
    
     $m = NULL;
     // search_query 문자열은 보통 괄호 두 개 안에 post_title, post_content 각 필드 조건을 OR로 묶음.
     if( preg_match('/\(\((.+)\)\)/ms', $my_test_search_query, $m ) ) {
    
     $meta_part = $my_test_meta_query['where'];
    
     // WHERE 절에서 AND ... 로 시작하는 meta 검색을 삭제
     $sql = str_replace( $meta_part, '', $sql );
    
     $post_part = $m[1];
     $meta_altered = preg_replace( '/\s*AND\s+(.+)/ms', ' OR $1', $meta_part );
     $substitute = 'AND ((' . $post_part . $meta_altered . '))';
    
     // post title, post content 검색과 같이 커스텀 필드도 OR 조건으로 검색
     $sql = str_replace( $my_test_search_query, $substitute, $sql );
     }
    
     return $sql;
    } );

    이렇게 쿼리를 대대적으로 수정하면 다음처럼 쿼리가 출력됩니다.

    SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND (((wp_posts.post_title LIKE ‘%01%’) OR (wp_posts.post_content LIKE ‘%01%’) OR (
    (
    ( wp_postmeta.meta_key = ‘my_test_value_1’ AND CAST(wp_postmeta.meta_value AS CHAR) LIKE ‘%01%’ )
    OR
    ( wp_postmeta.meta_key = ‘my_test_value_2’ AND CAST(wp_postmeta.meta_value AS CHAR) LIKE ‘%01%’ )
    )
    ))) AND wp_posts.post_type = ‘my-test-post’ AND (wp_posts.post_status = ‘publish’ OR wp_posts.post_status = ‘demo’ OR wp_posts.post_status = ‘lock’ OR wp_posts.post_status = ‘pending’ OR wp_posts.post_status = ‘future’ OR wp_posts.post_status = ‘draft’ OR wp_posts.post_status = ‘demo’ OR wp_posts.post_status = ‘lock’ OR wp_posts.post_status = ‘private’) GROUP BY wp_posts.ID ORDER BY wp_posts.post_title LIKE ‘%01%’ DESC, wp_posts.post_date DESC LIMIT 0, 20

     

    마치며

    워드프레스의 막강한 플러그인들이 워낙 많고, 또 이러한 단순 키워드 검색은 그다지 효율적이지 않을 수 있습니다. 그러나 때로는 이러한 검색이 필요한 순간이 있을 것입니다. 특히 커스텀 포스트에서 검색 확장이 용이하지 않다는 점이 아쉬워서 이렇게 글을 남겨 보았습니다. 설명한 플러그인 소스 전문은 여기에서 확인하실 수 있습니다.

  • X Japan – Jade

     

    Walk through the light to find the shadow,
    Til’ your gods stall you from the edge,
    Sunk in the thought, the feel, the distance,
    Was it your secret?
    Stop holding your fear, let memory live and die alone.
    No need to be there, let your desire scream,

    Til’ you feel alive.
    ‘Cause you are beautiful,
    Your scars are beautiful,
    Like the jade.
    You’ll still shine, when you sink into the sea,
    When the bleeding scarlet jealousy, carves the way you believe.

    Brink of the light, the wings of the night,
    Look into the eyes of fallen angels,
    Sink like a stone into the dark,
    Where no light can touch.
    Will god break my fall?
    I feel the mystery, tries to take me along,
    To the end of the world, where I still believe,
    The colour is from your eyes.

    ‘Cause you were beautiful,
    Your blood was beautiful, yesterday,
    I still hide at the seam of memories,
    Oh, I still hold my rosary, beneath the pain of life.
    Where the tears, when the bloody face of love, they take away the stain,
    Know that the stars of the sky,
    Glow in the ocean,
    The art of life,
    Makes me wanna die in the colour of heaven.

    Oh, another day has gone,
    Another friend has gone into the flame,
    It’s burning now, jibun de kirisaita mune no kizuato sae birei ni naru made.

    Oh, ’cause you are beautiful,
    Your scars are beautiful, like the jade,
    You’ll still shine, when you sink into the sea,
    When the bleeding scarlet jealously carves the way you believe,
    Now and forever, you’ll be loved,
    Let your destiny lead your heart
    My jade

    Read more: X-Japan – Jade Lyrics | MetroLyrics

     

  • 커스텀 포스트의 카테고리 필터에 대한 기록

    커스텀 포스트의 카테고리 필터에 대한 기록

    워드프레스를 이용하여 워드프레스 개발에 관한 포스트로는 처음이네요. 이번에 개발을 하면서 잠깐 보게 된 커스텀 포스트에서 카테고리 필터 드롭다운 상자 기능에 대해 약간 알아본 것이 있어 기록하고자 포스트를 남깁니다.

    워드프레스 기본 포스트 타입: post

    우선 워드프레스의 가장 기본적인 포스트 타입은 ‘post’입니다. 워드프레스가 설치되면 기본적으로 생성되는 포스트 타입이며, 한국어 워드프레스에서는 ‘글’이라고 번역했으니 해당 메뉴에서 확인할 수 있습니다.

    기본 포스트 타입의 카테고리 분류 필터
    기본 포스트 타입의 카테고리 분류 필터

    타입의 모든 목록이 출력되는 ‘모든 글’ 페이지를 잠깐 살펴 보겠습니다. 이 곳에는 ‘모든 카테고리’라는 ‘필터’가 보입니다. 이 드롭다운 상자를 클릭하면 현재 워드프레스의 포스트 카테고리가 출력됩니다. 이 중 하나를 선택하고 필터 버튼을 누르면, 화면 전환이 일어납니다. 그리고 새로운 페이지에 해당 카테고리에 속하는 포스트만 걸러져 나오죠.

    카테고리는 글을 분류하는 중요한 기준이 되므로, 당연히 글을 찾아 보거나 일람하는데 있어 매우 중요한 기능 중 하나입니다. 그래서 플러그인 개발을 할 때 포스트처럼 커스텀 포스트에도 기본 포스트 타입처럼 이런 카테고리 필터링 기능을 추가하는 경우가 많습니다.

    기본 포스트에서 카테고리 필터 동작

    기본 포스트에서 기본 카테고리 필터를 동작시키면 다음처럼 긴 URL 파라미터가 붙게 됩니다.

    /wp-admin/edit.php?s&post_status=all&post_type=post&action=-1&m=0&cat=2&filter_action=필터&paged=1&mode=list&action2=-1

    여기서 우리가 살펴 볼 중요한 파라미터는 ‘cat=2‘입니다. ‘cat’이라는 변수는 워드프레스에서 기본 카테고리를 위해 사용하는 키로 워드프레스 코어에서도 고정적으로 사용되고 있습니다. 여기서 변수의 값이 2인 것은 해당 카테고리의 ‘term_id’ 값이 2이기 때문입니다.

    태그, 카테고리, 그리고 택소노미(taxonomy)

    워드프레스에서도 포스트들을 분류하기 위한 분류체계를 가지고 있는데 이 또한 ‘택소노미’라고 부릅니다. 기본적으로 제공되는 ‘태그(tag)’와 ‘카테고리(category)’도 택소노미의 하나입니다. 한 택소노미는 위계적(hierarchical)이거나, 수평적(flat)인 두 속성 중 하나를 가질 수 있습니다. 카테고리는 위계적인 속성을 가지고 태그는 수평적인 속성을 가지죠.

    태그와 카테고리 뿐만 아니라 워드프레스는 자신이 원하는 택소노미를 별도로 생성해서 사용할 수 있습니다. 예제 플러그인에서도 별도의 택소노미인 ‘collection-type’을 만들어서 사용하고 있죠.

    포스트 목록 테이블의 소스 코드 분석

    그러면 이렇게 포스트에서 해당 필터 드롭박스를 출력하는 부분이 워드프레스 소스에서 어떻게 구현되었는지 살펴 보도록 할께요. 이 부분은 wp-admin/includes/class-wp-posts-list-table.php 파일 ‘WP_Posts_List_Table::extra_tablenav()’ 메소드에서 구현되어 있습니다. 사실 아주 간단하게 wp_dropdown_categories()라는 함수로 이 HTML 요소를 출력하는군요.

    코덱스에서 핵심 함수 확인

    그렇다면 이 wp_dropdown_categories() 함수가 어떻게 동작하는지 알아 보도록 하죠. wp_dropdown_categories() 함수는 하나의 array를 인자로 받는데, 이 array는 여러 키를 가질 수 있습니다. 우선 WP_Posts_List_Table::extra_tablenav() 메소드에서 선언된 함수의 인자를 살펴보도록 할까요.

    $dropdown_options = array(
        'show_option_all' => __( 'All categories' ),
        'hide_empty' => 0,
        'hierarchical' => 1,
        'show_count' => 0,
        'orderby' => 'name',
        'selected' => $cat // 이 부분을 주목!
    );

    간단하게 몇 개의 키만 사용하고, 나머지는 기본값으로 남겨 두었네요. 여기서 눈여겨 보아야 할 것이 ‘selected’ 키의 값입니다. 카테고리 항목을 하나 선택한 후 필터 버튼을 누르면 화면 전환이 일어납니다. 그리고 웹브라우저의 화면이 새롭게 그려집니다. 헌데 필터의 드롭다운 상자는 내가 방금 전 선택한 항목을 정확하게 표시하고 있죠. 바로 ‘selected’ 키 때문입니다.

    커스텀 포스트에서도 이런 필터를 붙여 보자

    테스트 플러그인을 만들어 보도록 하겠습니다. 이름은 ‘taxonomy-dropdown’이라고 하고, 아주 간단한 택소노미와 커스텀 포스트를 등록해 보도록 하겠습니다.

    플러그인이 활성화 될 때 적당히 기본 포스트의 택소노미인 ‘카테고리’와 거의 비슷한 스타일의 택소노미인 ‘collection-type’, 그리고 샘플 포스트 3개를 자동으로 삽입하도록 합니다. 또한 플러그인이 비활성화 되면 DB에 기록된 택소노미와 샘플 포스트를 삭제하도록 처리했습니다.

    플러그인이 별도로 생성한 커스텀 포스트 'collection'
    플러그인이 별도로 생성한 커스텀 포스트 ‘collection’

    스크린샷에서도 보이듯이, 커스텀 포스트를 만들면 기본 포스트 타입처럼 카테고리를 필터하는 드롭다운 상자는 보이지 않습니다. 코어는 어떤 택소노미를 그렇게 처리해야 할지에 대해 알지 못하기 때문입니다. 별도로 우리가 추가를 해 주어야 합니다. 이를 위한 훅은 바로 ‘restrict_manage_posts’입니다. WP_Posts_List_Table::extra_tablenav() 에 선언되어 있습니다. 기본 포스트가 했던 것과 거의 동일하게 훅의 콜백 함수에서 드롭다운 상자를 만들어 주면 됩니다.

    // 처음은 요렇게
    wp_dropdown_categories(
        array(
            'show_option_all'   => 'All Collections',
            'taxonomy'          => 'collection-type',
            'show_count'        => 0,
            'selected'          => $term->term_id,
            'hierarchical'      => TRUE,
            'name'              => 'cat',
            'value_field'       => 'term_id',
        )
    );

    이렇게 하면 드롭다운 단추가 생깁니다. 간단하죠? 그럼 이제 필터링이 되는지 테스트해 볼까요?

    restrict_manage_posts 훅을 이용해 기본 포스트 타입과 유사한 카테고리 필터 도구 삽입.
    restrict_manage_posts 훅을 이용해 기본 포스트 타입과 유사한 카테고리 필터 도구 삽입.

    드롭다운 버튼이 생겼습니다. 아무 카테고리를 선택해 필터 버튼을 누릅니다. URL이 생성되고 페이지 전환이 일어납니다. 그러나 아직은 글이 필터링 되어 나오지는 없습니다. 왜냐면 폼을 통해 전송된 HTML select 태그 속성 name의 값 ‘cat’은 워드프레스 코어가 알아들을 수 있는 쿼리 변수가 아니기 때문입니다. 어, 이상하죠? 아까 기본 포스트 타입에서는 분명해 ‘cat’을 써서 필터링을 했는데요…

    왜냐하면 지금 화면은 우리가 등록한 커스텀 택소노미를 다루고 있기 때문입니다. ‘카테고리’, 그러니까 ‘category’ 택소노미는 사실 ‘post’ 타입을 위해 만든 택소노미죠. 코어는 ‘cat’이라는 URL 쿼리 문자열을 발견하면 ‘category’라는 택소노미만을 다루도록 처리되어 있습니다. 다른 택소노미는 감히 범접(?) 할 수 없는 영역이에요.

    wp-includes/query.php 소스 내부 1886번째 줄(버전 4.2.2기준)

    // Category stuff
    if ( ! empty( $q['cat'] ) && ! $this->is_singular ) {
       $cat_in = $cat_not_in = array();

    \WP_Query::parse_tax_query() 메소드 내부에서 URL 쿼리 문자열로 보낸 ‘cat’에 대해 처리하는 코드가 있습니다. 이 때 cat이라는 키를 사용하면 무조건 field는 ‘term_id’로, ‘taxonomy’는 ‘category’로 동작하도록 정해져 있습니다. 이것은 우리가 원하는 동작이 아닙니다. 적어도 택소노미는 우리가 원하는 것으로 동작하도록 변경해야 합니다. 그러려면 가장 간단하게 \WP_Query::parse_tax_query() 내부에서 호출하는 ‘parse_tax_query’ 훅을 이용하면 됩니다.

    add_action( 'parse_tax_query', 'taxdd_parse_tax_query' );
    function taxdd_parse_tax_query( $wp_query ) {
       $wp_query->tax_query->queries[0]['taxonomy'] = 'collection-type';
    }

    물론 WP_Query가 포스트를 데이터로 가져올 때 동작을 변경할 수 있는 훅은 여러 가지가 있습니다. 예제 플러그인처럼 ‘parse_tax_query’ 훅도 쓸 수 있지만, 때에 따라서는 ‘pre_get_posts’ 훅도 사용할 수 있습니다. 사실 인터넷에서 검색할 수 있는 많은 예제에서도 주로 사용되는 훅이 pre_get_posts입니다. 그러나 필터링에 쓸 키를 ‘cat’으로 정했다면 parse_tax_query 훅 콜백 함수에서 간단하게 값 1개만 변경하면 되므로 이 쪽을 선택했습니다.

    요약

    1. 필터 조건을 거는 키가 ‘cat’인 경우 기본 포스트처럼 동작하려 한다. 코어는 택소노미가 ‘category’인 것으로 인식한다.
    2. 그러므로 parse_tax_query 훅에서 우리가 정한 커스텀 택소노미로 인식하도록 변경한다.

    첨언

    WP_Query에 대해 언급하자면, 워드프레스가 포스트 정보를 데이터베이스로부터 가져올 때 사용되는 클래스입니다. 워드프레스의 포스트를 쿼리하는 표준적인 방식이며 거의 모든 포스트 정보는 이 클래스를 통해 얻어집니다. 내부에 여러 액션/필터가 설정되어 있어 커스터마이징을 할 수 있는 부분이 곳곳에 있습니다. 사실상 ORM의 일종이라 생각하면 됩니다. 이 객체는 URL 쿼리 문자열으로부터 유효한 변수들을 먼저 파싱하여 자기가 어떤 포스트를 가져와야 하는지를 기본적으로 결정합니다. 예를 들어 URL에 post_type이라는 키가 있으면 WP_Query 객체는 DB로부터 post_type 값에 해당하는 포스트 타입을 쿼리하게 됩니다. WP_Query는 wp-includes/query.php에 선언되어 있습니다.

    URL 쿼리 문자열으로 ‘cat’만이 유일한 방법은 아닙니다. WP_Query 객체 내부에 정의된 훅을 잘 이용하면 어떤 변수 이름이라도 사용 가능합니다. 그렇지만 다음에 설명드릴 대용 때문에, 어떤 식으로 구현을 하더라도 option 태그의 값은 term_id로 고정해야 할 것 같습니다.

    다른 접근: query_var를 자연스럽게 활용

    코드 변경

    워드프레스 코어의 쿼리 처리 방식을 자연스럽게 활용하는 방법이 하나 더 있습니다. ‘restrict_manage_posts’ 훅의 콜백 부분을 다음처럼 변경합니다.

    $term = get_term_by( 'id', $_GET['collection-type'], 'collection-type' );
    wp_dropdown_categories(
       array(
          'show_option_all'   => 'All Collections',
          'taxonomy'          => 'collection-type',
          'show_count'        => 0,
          'selected'          => $term->slug,
          'hierarchical'      => TRUE,
          'name'              => 'collection-type',
          'value_field'       => 'slug',
       )
    );

    이렇게 하고 parse_tax_query 훅의 선언을 주석 처리합니다. 이렇게 하면 그저 필터 드롭다운 상자만 만들어도 글이 필터링됩니다.

    슬러그를 통한 검색

    그 이유는 register_taxonomy() 함수의 인자를 살펴 보면 알 수 있습니다. 인자인 array 타입의 $args 중 유효한 키로 ‘query_var’가 있습니다. URL 쿼리 문자열에 사용될 키 이름을 여기서 별도로 지정하는 것입니다. 이 때 우리는 별도의 값을 지정한 적이 없으므로 기본값인 ‘collection-type’이 됩니다. 바로 이것이 자연스럽게 코어에서 필터링이 된 핵심입니다.

    또한 우리는 드롭다운 상자 내부의 option 태그가 가질 value 속성을 ‘term_id’에서 ‘slug’로 변경했습니다. \WP_Query::parse_tax_query() 라인 1859에 보면, 워드프레스에서 선언된 모든 택소노미를 대상으로 루프를 돌며 URL 쿼리 문자열에 query_var와 매칭되는 것이 있는지를 검사하는 코드가 있습니다. 이 때 검사를 시도할 값은 ‘slug’로 되어 있습니다. 그러므로 별도의 처리를 하지 않아도 WP_Query의 기본 동작만으로 포스트를 필터할 수 있는 것입니다.

    그러나 치명적인 문제가…

    그러나 이 방법은 약간 문제가 있습니다. 드롭다운 상자의 내용이 기억되지 않고 항상 첫번째 항목으로 고정 선택이 됩니다. 이것은 wp_dropdown_categories() 함수 내부에서 html 태그를 출력하는 역할을 맡은 Walker_CategoryDropdown::start_el() 메소드 때문입니다. 이 메소드는 wp-includes/category-template.php 에 정의되어 있습니다.

    이 파일의 라인 1153 근처를 보면

    if ( $category->term_id == $args['selected'] )
       $output .= ' selected="selected"';

    라고 되어 있습니다. 네, wp_dropdown_categories() 함수는 입력 인자 중 ‘selected’가 ‘term_id’ 일때만 동작하도록 설계되어 있기 때문입니다. 코어를 다음과 같이 수정하면 필터링과 드롭다운 내역 기록이 동시에 가능합니다. 위 소스를 다음처럼 수정합니다.

    if ( $category->{$args['value_field']} == $args['selected'] )
       $output .= ' selected="selected"';

    그러나 이 방법은 그다지 추천하고 싶지 않은 것이, 무려 코어를 건드리는 것이기 때문입니다. 비슷한 방법으로 같은 결과를 얻을 수 있음에도 불구하고 코어를 건드리는 것은 부담스럽습니다. 아무리 작은 수정이라도 그 수정이 어떤 결과를 가져올지 검증되지 않았기 때문이죠. 그러나 이러한 방법이 분명히 존재한다는 것과, 코어에 약간의 수정을 가하면 올바르게 동작한다는 것을 보여 주고 싶었습니다. 사실, 코어의 start_el() 함수 내부 “if ( $category->term_id == $args[‘selected’] )” 부분은 분명히 문제가 있다는 것을 지적하고 싶었습니다.

    요약

    1. register_taxonomy의 인자로 ‘query_var’를 확인하라. 이를 이용할 것이다.
    2. wp_dropdown_categories() 인자 중 ‘name’의 인자로 1 항목의 ‘query_var’를 사용하라. 그리고 ‘value_field’를 ‘slug’로 사용한다.
    3. 이렇게 되면 별도의 콜백 절차 없이 필터링을 사용할 수 있다.
    4. 단, 드롭다운 상자의 기능에 약간의 문제가 발생한다. 이는 코어 수정을 통해 해결할 수는 있다.

    결론

    커스텀 포스트에서 카테고리(보다 정확히 말하자면, 택소노미)를 기준으로 필터링하는 드롭다운 상자를 만드는 법에 대해 포스팅했습니다. 카테고리 드롭다운 단추를 만드는 것은 물론 생짜로 HTML 태그를 생성하는 법도 있지만 워드프레스에서 wp_dropdown_categories() 함수를 제공하므로 이를 쓰는 편이 좋습니다.

    그런데 문제는 이 함수에 넣는 인자입니다. 인자를 어떻게 쓰느냐에 따라 필터링이 될 수도 있고 안 될수도 있고, 드롭다운 상자의 동작 또한 제대로 될 수도 있고, 문제가 발생할 수도 있습니다.

    한 가지 방법은 드롭다운 상자의 이름을 ‘cat’으로 지정하고 term_id를 기준으로, 그러니까 기본 포스트의 카테고리 택소노미와 유사하게 동작하도록 만드는 것입니다. 그러면 워드프레스 코어는 이것이 기본 포스트 타입의 ‘카테고리’ 택소노미인 것으로 인식하고 동작하려 합니다. 그러나 우리는 커스텀 택소노미를 사용해야 하므로 parse_tax_query에서 택소노미를 강제 변경해 원하는 결과를 추출해 낼 수 있었습니다.

    이렇게 하는 이유는 이렇습니다. wp_dropdown_categories() 함수 내부에는 \Walker_CategoryDropdown::start_el()이라는 메소드를 사용합니다. 그런데 여기서는 단지 ‘term_id’만을 기준으로 option HTML 태그의 “selected” 속성 출력 여부를 결정하고 있습니다. 그러므로 term_id가 아닌 다른 방식으로 HTML 태그를 출력하고 데이터베이스 쿼리까지 동작하려면 별도의 훅을 이용해 동작을 변경시켜야 합니다.

    term_id를 사용하는 것이 아닌 방식의 한 예로는 slug를 사용하는 것입니다. 이것을 택소노미의 query_var 속성과 잘 연결하면 별다른 훅 콜백 함수 없이도 자연스럽게 글 필터링이 가능하지만, 아쉽게도 어떤 액션/필터를 동원해도 출력되는 HTML 속성을 컨트롤 할 수가 없습니다. 그래서 글 필터링은 잘 되지만 사용자가 방금 선택한 필터 항목 내용이 표시되지 않는다는 단점이 있습니다. 코어에서도 이를 잘 반영하도록 처리할 수 있는데 조금 아쉬운 부분입니다.

    소스코드 다운로드: [link]

  • 좋은 추억도 있고 나쁜 추억도 있지

    좋은 추억도 있고 나쁜 추억도 있지. 후회들 때도 있구. 그치만 후회 같은 거 하느라 삶이 비관적으로만 보이더라도 꼭 과거의 내가 잘못만 했던 건 아니거든?

    또 오늘을 살면서 내가 처한 상황이 마냥 나쁘지만도 않다는 걸 깨닫는다. 어떻게 받아들이냐에 따라 어떤 조건은 꽤 유익한 것이 될 수 있지.

  • 谷村新司 祇園祭 歌詞

    谷村新司 祇園祭 歌詞

     

    祇園祭

    歌:谷村新司
    作詞:谷村新司
    作曲:谷村新司

    化野をぬけて 清滝へ向かう
    祇園祭りの遠ばやしを
    逃がれるように 宿につけば
    心細さもつのる
    渡月の橋を 渡りきるまでは
    振り向いちゃいけない 自分の過去だけは
    真顔になった君は去年
    確かにこの宿にいた
    あー宵山の 人波にまぎれて
    あー握りしめた 君の手を覚えてる今も

    祇王の夢と祇女の涙
    庭に佇み夕暮れまで
    悲しすぎるわ女はいつも
    男の為に生きてくなんて
    結婚するより このままの方が
    一生懸命 生きれるみたいと
    つぶやく君は 僕よりずっと
    大きく見えた旅の宿
    あー宵山の 人波にのまれて
    あー流れてゆけ 若すぎる恋の痣よ

    風の噂に聞くこともない
    祇園祭りに さらわれたまま
    君の写真も少し黄ばんで
    僕もいくつか年老いた
    もっと男で いられたならば
    君も女でいられたならば
    恋に走れたあの年の夏
    帰ることない暑い夏
    あー宵山の 人波のどこかを
    あー愛している あの人も流れているだろう

    あー祇園祭りの 遠ばやしが聞こえる
    あーあの日のまま あーあの日のまま
    あーあの日のまま あーあの日のまま

    via 谷村新司 祇園祭 歌詞.

  • 잡소리 #3

    내 액시스 프레임워크가 보다 강력한 무언가를 가지게 되면 좋겠네.

  • 잡소리 #2

    잘 해서 끝까지 가는 건 아니고 실력이 좋아서 끝까지 가는 거 아니다. 그 가는 길이라는 거, 니가 하는 걸 사랑해야 그렇게 될 수 있는 거다.

  • 잡소리 #1

    병신 짓도 꾸준해야 하고, 잡소리도 일관되어야지. 이랬다저랬다 하면 안 돼.

     

  • 관심 있어하는 키워드들

    교보문고 북로그에 가면 책 조사한 리스트들이 있다. 구매할 때 참고하면 좋을 것 같다.

    오늘 조사한 관심 있어하는 키워드를 정리해 보았다. 무작위이며 순서는 없다.

    • 웹 해킹
    • html5
    • css3
    • Javascript
    • node.js
    • Django
    • MySQL
    • 건강
    • 죽음
    • 당뇨
    • 정의 (justice)
    • 총, 균, 쇠
    • 한국 현대사
    • 심리
    • 철학
    • 중고등교재
    • 고전
    • 자동차정비
    • 수학
    • 통계
    • 미적분
    • 패션
    • 인문교양
    • 일반 소설

     

  • 작고 가볍고 빠르게, 그리고 쉽게

    그동안 집중한답시고 너무 진득하게 오랫동안 무얼 잡고 끌어 보곤 했는데, 이게 지금 보니 별 성과는 없고 사람 기분만 착잡하게 만들게 하는 것 같네. 그냥 작게, 가볍게, 빠르게, 그리고 쉽게 쉽게. 예전의 내가 보면 “산만하다”는 느낌이 들 정도로 휙휙 변하는 것이 차라리 나을 것 같다.

    어떤 일을 잡고 어떤 시간 동안 하려고 맘 먹었지만, 잘 되지 않아서 딴짓만 하다가 시간을 낭비했어. 예전 같음 아아~왜 시간을 낭비했지 하면서 탓을 하면서 침울해지곤 했어. 그렇지만 이제는 방식을 바꿔 보려고. 이제는 이런 일이 있다면 에, 그래? 뭐 어쩔 수 없지. 다음 일로 넘어가! 하면서 그냥 슥삭 다음 일로 넘어가려고 생각을 해. 그래야 마음도 가볍고 또 새롭게 다른 일을 하면서 분위기 전환도 되는 거지. 한 가지 일을 잘 안되는 데도 불구하고 오랫동안 잡고 있는 건 바보 같아 보여.

    그냥 심플하게 방법을 바꾸어 보는 거야. 훨씬 나은 것 같아. 기분 전환을 위해 노력하는 일도 꽤 많아졌고 산책 같은 것도 많이 하게 되었어. 왜, 내가 프로그램 짤 때 KISS(Keep It Simple & Stupid) 원칙 지키라고 하잖아.  재미로 시작하는 것이 더 오래, 강하게 남는다고 했잖아. 그런 깨달음을 내 일반적인 생활 패턴에도 적용해 보기 시작하는 거야.

  • 내가 세상을 바꿀 수 있을까?

    내가 세상을 바꿀 수 있을까?

  • 단상

    훌륭한 소프트웨어를 보면 인간이 때로는 위대하게 보이고 내가 짠 코드가 버그나는 걸 보면 인간이 대개 멍청하다는 걸 느낍니다.

  • 기분이 좋다!

    요즘은 대체로 기분이 좋다! 운동의 덕택이다!!

  • 생활코딩에 남긴 글.

    제가 프로그래밍을 좋아하는 것은 내 생각이 이러이러하게 했더니 결과 따단 하고 나오는 통쾌함이 있어서였어요. 그리고 좀 더 공부하고 난 후에는 프로그래밍의 철학이 그 외의 영역과 많은 부분에서 맞닿아 있다는 생각이 들었습니다. 마치 진리는 하나다 라는 메타포를 프로그래밍에서 발견했다면 지나친 과장일까요?

    세상 많은 것들이 의외로 프로그래밍의 잠언으로 해석해도 교훈적이라는 생각히 듭니다.

    가령, 이건 아주 단순한 예인데, 변수명을 보기 좋게 지으라는 교훈을 들죠. 실생활에서 확장해 느껴 볼 수 있습니다. 개념을 명쾌하게 설정하고 다른 것들과 구분되게 하라는 일반적인 말들과 일맥상통하죠. 또한 어떤 개념을 말할 때 비슷해 보여도 같음과 다름을 잘 가려 구분하라는 말과 비슷해 보입니다.

    전 오랫동안 즐겁게 프로그래밍을 하기로 결심했어요. 이렇게 세상에 멋진 일을 만나게 되어 감사합니다. 앞으로도 이렇게 좋아하는 마음을 가지며 일할 수 있는 것이 꿈입니다. 이 대한민국에서는 프로그래머의 대우가 박하다는 것이 일반적인 이야기이나, 저는 이러한 낭만(?)을 가지고 일하고 싶어요. 그런 세상이 여기서도 이뤄 질 수 있다고 말하고 싶어요.

  • 일산 최군맥주 라페스타 점 방문

    20140723-192814-70094171.jpg

    20140723-192812-70092502.jpg

    20140723-192810-70090691.jpg

    감자튀김과 맥주를 편하게 즐길 수 있는 최군맥주. 혼자서도 간단히 즐길 수 있은 분위기가 맘에 든다.