메타 테이블 체계 확장하기

그러니까, 워드프레스에서 메타 테이블 체계는 상당히 매력적이다. 물론 단점도 많긴 하다. 그러나 간단하게 확장 가능한 유연한 저장 장소가 필요할 때 메타 키 – 메타 값 식으로 테이블을 구성해서 쓰고 싶은 생각이 들 때가 한두번이 아니다.

다행히 워드프레스 코어 또한 메타데이터 코드는 한 벌만 만들고 포스트, 텀, 유저 등에서 확장하여 재활용하고 있다. 플러그인에서도 동일하게 확장하여 메타데이터 스타일의 테이블을 사용할 수 있다. 한 번 해 보자.

메타데이터

메타데이터 체계부터 간단히 설명하자. 워드프레스 테이블을 보면 ‘meta’로 끝나는 것들이 있다.

메타 테이블의 예시 그림
메타 테이블의 예시

위 예시는 포스트 테이블과 매칭되는 포스트메타 테이블인데, 메타 테이블은 그림처럼 4개의 칼럼만으로 구성되어 있다.

쉽게 말해 칼럼으로 만들어야 할 데이터를, 포스트에 부가적으로 저장할 데이터를 적당히 키와 텍스트 형태의 값으로 만들어 이 테이블에 레코드 형태로 줄줄이 저장하는 것이다. 이렇게 하면 저장해야 할 필드가 증가해도 테이블의 변경이 발생하지 않는다. 메타 키만 적당히 잘 구분지으면 그만이니까. 그러므로 워드프레스 같은 확장을 가장 염두에 두고 개발해야 하는 스타일에 잘 맞는다.

오브젝트

물론 데이터 검색과 조회를 효율적으로 하려면 메타 테이블만 사용할 수 없다. 그런 것들은 미리 오브젝트(object)의 테이블에 정의한다. 기본적인 오브젝트 타입들은 ‘post’, ‘term’, ‘user’가 있고 각각 {$prefix}_posts, {$prefix}_terms, {$prefix}_users 테이블들이 해당 오브젝트의 테이블이다.

그리고 오브젝트 타입에 별도로 서브오브젝트(subobject) 타입이 존재한다. 이건 해당 오브젝트 내에서 다양한 확장을 시도할 때 활용된다. 간단한 예로 포스트를 확장하여 커스텀 포스트 타입을 등록하는 것을 들 수 있다. 사실 커스텀 포스트 타입은 오브젝트의 서브오브젝트 타입인 것이다.

커스텀 오브젝트 메타데이터

당연히도 오브젝트를 새롭게 등록하는 방법은 특별히 없어 보인다. 어떤 형식의 콘텐츠인지 미리 알 방법은 없을 테니까. 적당히 커스텀 테이블을 만들고 해당 테이블의 데이터를 적절히 오브젝트로 지정하면 된다. 그리고 오브젝트에 대한 메타이터 테이블을 별도로 생성한다.

우선 완성된 코드는 github repo에 올려두었다. 해당 코드를 받아서 보면서 설명을 보는 것이 좋겠다. 코드는 워드프레스 플러그인이다. 활성화하면 관리자 메뉴에 ‘WCM Sample’이라는 메뉴를 생성한다.

주의! 플러그인을 운영 중인 사이트에서 사용하지 마세요!

플러그인이 가정하는 상황

플러그인이 가정하는 환경은 이렇다. 가끔 워드프레스 A 사이트와 B 사이트 같이 두 개의 분리된 워드프레스 사이트가 있는데, 개념적으로 A 사이트는 B 사이트의 하위 사이트로 운영된다. B 사이트의 회원 정보 일부는 A 사이트와 동기화되어 사이트의 운영에 사용된다.

그렇다고 해서 B 사이트의 회원 정보를 그냥 A 사이트의 회원 테이블에 쌓아둘 수도 없다. ID 충돌 우려도 있고 A 사이트에서 B 사이트 정보로 로그인을 허용할 수도 없기 때문이다. 그러므로 별도의 회원 테이블을 생성하여 별도로 회원 테이블을 기록해야 할 것이다.

각 회원은 특정한 메일을 수신자에게 보내야 한다. 메일의 내용은 A 사이트에서 작성하며, 송신도 A 사이트에서 각 수신자에서 담당해야 한다.

여기서 메일 수신자를 별도의 회원 테이블로 만들기에는 좀 아쉽다. 물론 수신자 테이블을 만들어 별도로 관리하는 것도 나쁜 건 아니다, 하지만 각 회원의 여러 정보들이 새로 추가됨에 따라 매번 특정 테이블을 생성하다보면?

앞으로 어떻게 확장될 지 알 수 없는 정보들이 많다. 그리고 그 정보들은 각자 테이블로 관리하기에는 너무 지엽적이다 이럴 때 정말 메타 테이블이 간절하지 않을 수 없다.

플러그인의 포인트

관리자 화면의 스크린샷

주 포인트: 메타데이터 API 생성

플러그인을 활성화하면 적절히 news_users 테이블과 news_usermeta 테이블을 생성하고 유저 1, 2, 3을 적당히 생성해둔다(비활성화하면 사라진다). 이것은 외래 사이트에서 흘러온 사용자 목록이라고 가정한다. 유저의 ID, 로그인, 이메일 같은 기본적인 사항은 news_users 테이블에 저장한다.

각 회원의 수신자 목록은 여기서도 편집할 수 있다고 생각했다. 수신자 목록에서 이메일을 한 줄에 하나씩 적고 저장하면, 부적절한 이메일 주소는 걸러내고 오름차순으로 정렬해 저장한다. 이 수신자 목록은 메타 테이블이 저장한다.

그리고 init 액션에 맞춰 다음처럼 콜백을 작성한다.

function wcm1_define_extra_objects() {
  global $wpdb;

  static $init = false;

  if ( ! $init ) {
    $init = true;

    // Dynamically add our extra tables.
    // $wpdb->{$object_type}
    $wpdb->news_user     = $wpdb->prefix . 'news_users';
    $wpdb->news_usermeta = $wpdb->prefix . 'news_usermeta';

    // In our case, $object_type is 'news_user'.
    add_filter( 'get_object_subtype_news_user', fn() => 'news_user', 10, 2 );
  }
}Code language: PHP (php)

오브젝트를 별도로 등록하는 API가 존재하는지는 확실치는 않지만, 여기서는 $wpdb에 직접적으로 주입시킨다. 동적으로 속성을 다는 것이 PHP 8.2이상에서 허용될지는 모르겠지만, 일단 이렇게 하자.

이 때, $wpdb->news_user 로 동적으로 생성하는 속성은 오브젝트 타입과 동일하게 ‘news_user’와 맞춰야 한다. 몇가지 잘 맞춰야 하는 포인트가 있다.

  1. 오브젝트 타입을 ‘news_user’로 지었다면, $wpdb 속성 이름은 그대로 지어야 한다. 속성의 값은 테이블의 news_user의 테이블 이름이다.
  2. 메타데이터를 위한 $wpdb 속성 이름은 반드시 ‘news_usermeta’로 한다. ‘user’와 ‘meta’ 사이에 어떤 문자도 붙이지 않는다. 속성의 값은 메타 테이블의 이름이다.
  3. ‘get_object_subtype_news_user’ 필터 콜백을 붙여 ‘news_user’를 리턴시킨다.
  4. ‘news_usermeta’ 테이블의 외래 키 필드는 ‘news_user_id’로 지어야 한다. 반드시 ‘news_user’로 시작해야 한다.

이렇게 하고 add, delete, get, update 에 대한 함수를 래핑해서 작성한다.

function wcm1_add_news_user_meta( int $user_id, string $meta_key, $meta_value, bool $unique = false ) {
  return add_metadata( 'news_user', $user_id, $meta_key, $meta_value, $unique );
}

function wcm1_delete_news_user_meta( int $user_id, string $meta_key, $meta_value = '' ): bool {
  return delete_metadata( 'news_user', $user_id, $meta_key, $meta_value );
}

function wcm1_get_news_user_meta( int $user_id, string $meta_key = '', bool $single = false ) {
  return get_metadata( 'news_user', $user_id, $meta_key, $single );
}

function wcm1_update_news_user_meta( int $user_id, string $meta_key, $meta_value, $prev_value = '' ) {
  return update_metadata( 'news_user', $user_id, $meta_key, $meta_value, $prev_value );
}Code language: PHP (php)

이렇게만 하면 포스트메타, 유저메타와 동일하게 news_user에 대해 메타데이터를 관리할 수 있게 된다. 이렇게 하면 코어가 내부적으로 news_user 에 대한 캐싱도 뒷받침해주니 이득이다.

기타 포인트 #1: 필드 등록

여기에 register_meta()를 사용하여 코어에 미리 필드를 등록시키면 sanitize_callback을 사용할 수 있어 입력값 세정에 편리하다. 이 부분은 wcm1_init_meta() 함수 구현을 참고하기 바란다.

기타 포인트 2: 권한

약식이긴 하지만 user_can( 'edit_news_user_meta', ... )에 대한 권한 체크도 구현되었다. current_user_can() 같은 API로 권한 체크를 할 때, register_meta() 함수의 인자로 넣는 ‘auth_callback’과 연동 방법, 그리고 해당 연동이 실제로 동작시키기 map_meta_cap 필터의 구현을 참고한다

마치며

두 개의 분리된 WP 사이트가 정보를 동기화하려는 계획을 세우는 경우를 꽤 많이 봤으며, 이번에 작업하는 사이트도 그러하다. 또한 사소한 정보를 저장해야 할 필요가 있을 때 메타 테이블을 설계하는 케이스도 자주 접한다. 이 때 워드프레스 코어의 오브젝트 캐싱 기능도 활용하면서, 코드 구현의 부담도 줄이고 싶다면? 앞으로 위 포스트의 내용을 참고하여 메타데이터를 확장하여 편리하게 써 보자.

댓글 남기기