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

WordPress Logo
WordPress Logo

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

워드프레스 기본 포스트 타입: 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]