목차

워드프레스 플러그인 만들기

소개

워드프레스는 단순히 블로깅 용 툴이 아니라 여러 종류의 콘텐츠를 다룰 수 있는 잘 짜여진 CMS입니다. 즉, 다양한 콘텐츠를 어떻게 웹 상에서 효율적으로 제공하기 위한 플랫폼입니다. '블로그'는 그러한 콘텐츠의 한 부분집합인 것이구요.

워드프레스는 점차 많은 곳에서 채택되어 사용되고 있습니다1). 제작 및 유지보수도 용이함도 있지만, 플러그인을 통한 유연한 기능 확장을 통해 손쉽게 원하는 기능을 구현할 수 있기 때문에 아닐까 생각합니다. 더구나 요즘은 다양한 플러그인들이 제작되어 있어, 뭔가 부족하거나 아쉬운 기능은 플러그인을 찾아 보면 구할 수 있는 경우가 아주 많습니다. 단순한 기능 추가부터, 쇼핑몰 구축까지 아주 다양한 분야에서 다양한 종류의 플러그인들이 구비되어 있습니다.

그런데 이러한 플러그인 제작은 어떻게 하는 것일까? 내 입맛에 맞는 워드프레스 플러그인은 제작할 수 없을까? 본 문서에서는 그동안의 짧은 경험을 정리하고자 워드프레스 플러그인 제작 방법을 기술합니다. 짧은 경험이므로 혹여 워드프레스에서 권장할 만한 내용이 아닌 부분도 더러 있을 수 있습니다. 반드시 Codex와 교차 검증하도록 하세요.

플러그인 이름짓기

이름이 제일 중요합니다! 멋진 이름을 짓도록 하세요. 만드려는 플러그인이 아주 독특해서 그 이름 밖에 생각할 수 없고, 그런 이름은 찾아볼래야 찾아볼 수 없다면 행운입니다만, 대개 사람들이 생각하는 것들이 비슷비슷하기에, 그 이름 또한 비슷비슷할 때가 참 많습니다. m( 이렇게 수많은 플러그인 중에서 내 플러그인이 돋보이려면 이름도 기억하기 쉬우면서 독특해야 합니다. 그러나 기억하셔야 합니다. 가장 중요한 점은 바로 플러그인 이름이 유일해야 한다는 겁니다.

또한 플러그인을 만들면서 여러 변수나 함수 이름을 만들 때 기존의 이름과 충돌하지 않게 하기 위하여 이름공간(namespace) 내지는 접두어를 붙여 변수, 함수, 클래스 이름을 작성합니다. 때때로 너무 많은 이름들이 접두어를 사용하고 있어 심각한 경우 '스머프식 이름 관례(Smurf Naming Convention)2)'가 되버려 난감할 때도 있습니다만, 이런 접두어는 일반적으로 워드프레스 플러그인에서는 흔히 보입니다. 이렇게 일반적으로 플러그인의 이름이나 이름의 약어를 직접적으로 코드에 사용하므로 이름을 보기 좋게 만드는 것이 좋습니다. 아무래도 개발하는 사람이 계속 접하게 되니께요.

플러그인 개요

여기서는 플러그인을 작성하기에 앞서 필요한 기본 컨셉을 간단하게 정리해 보았습니다. 플러그인 프로그래머는 가장 기본적으로 다음 세 가지에 대해서 이해하고 있어야 한다고 생각합니다.

  1. 코어와 플러그인의 관계
  2. 플러그인에서 사용되는 액션, 필터, 훅의 개념
  3. 기본 데이터베이스의 구조 및 개념.

플러그인은 워드프레스의 손님

플러그인은 워드프레스 본체에서 기능을 확장하기 위해 추가적으로 불러 오는 모듈입니다. 여러분도 상식적으로 당연하다 생각하실 겁니다. 여기서 중요한 말은 바로 '불러 오는' 이 부분 입니다. 즉, 플러그인이 자기 필요에 의해 요청을 하는 것이 아니라, 반대로 워드프레스 본체가 필요에 의해 플러그인을 소환하는 형태인 것입니다.

이것은 프로그래밍 측면에서 보면 참조를 하는 주체(主體)와 참조되는 객체(客體)가 각각 누구인지가 결정되어 있다는 겁니다. 보통 우리가 프로그래밍을 하게 되면 우리가 주체, 그러니까 '갑'이 되고, 우리 프로그램에 필요한 기능을 위해 외부 라이브러리를 들여다 사용하게 됩니다. 외부 라이브러리는 객체로서 '을'의 입장이 됩니다. 그러나 플러그인 개발에 있어 주체는 워드프레스 본체이고, 객체는 플러그인입니다. 다시 말해 우리의 플러그인은 워드프레스 프레임워크 상에서 내부적으로 호출되어 사용되는, 일종의 라이브러리가 되는 것입니다. 일반적으로 생각해 보면 이러한 모델이 될 수 밖에 없겠죠.

액션, 필터, 그리고 훅

앞서 플러그인은 워드프레스의 손님이라는 표현을 사용했습니다. 플러그인은 워드프레스의 기능을 확장하기 위해 만들어지지만 워드프레스의 코어를 토대로 만들어지며 코어 없이는 동작하지 않기 때문입니다.

플러그인은 코어에게 원하는 시점에 원하는 동작을 하도록 요청해야 합니다. 예를 들어 플러그인은 워드프레스 대시보드 매뉴가 나올 때 자기 플러그인 메뉴도 같이 나오도록 코어에 요청할 수 있어야 합니다. 혹은 회원 가입을 하거나 회원이 프로필을 바꿀 때 회원에 대해 어떤 특별한 작업을 수행하도록 계획했다면, 그 시점에 그러한 행동을 할 수 있도록 코어에 요청을 해 그러한 작업이 이뤄지도록 무언가를 해야 하겠죠?

이렇게 내가 만든 플러그인과 워드프레스 코어 간에 상호작용이 있어야 동작을 할 수 있습니다. 그 역할을 하는 것이 바로 '액션', 또는 '필터'입니다. 쉽게 말해 액션과 필터 둘 다 PHP로 작성하는 콜백 함수(callback function)입니다. PHP의 call_user_func 함수를 원하는 때 원하는 방법으로 이용하도록 만든 워드프레스의 프로토콜이라 생각하시면 됩니다.

액션과 필터는 일반적인 GUI 프로그래밍에서 자주 쓰이는 '이벤트 핸들링'의 개념과 매우 유사합니다. 가령 유저가 버튼을 눌렀을 때 GUI 프레임워크는 버튼 클릭 이벤트를 발생하고, 이벤트 핸들러가 호출됩어 원하는 동작이 일어나는 것을 기대할 수 있지요.

GUI 프로그래밍에서 어떤 특정 사건을 지칭하는 용어를 '이벤트'라고 말한다면, 워드프레스에서는 이 이벤트라는 용어를 일컬어 ''이라고 부릅니다. 즉 어떤 훅이 발생하면 그 훅에 대해 어떤 이벤트 핸들링을 하게 되는데, 그 핸들러의 종류는 액션과 필터 두 종류로 나뉘는 것입니다.

액션필터가 사실상 콜백 함수라면, 왜 이렇게 둘을 갈라 두었을까요?

액션은 워드프레스가 수행하는 '코드'에 관심이 있습니다. 즉 어떤 훅에 의해 호출이 되면 어떤 코드를 수행할지에 초점을 맞춘 것입니다. 반면 필터는 워드프레스의 '데이터'에 더 관심이 있습니다. 훅에 의해 호출이 되면 전달 받은 데이터를 어떻게 가공할지에 대해 더 초점을 맞춥니다. 물론 어떤 일의 성격상 필터로도 가능한 일이 있고, 액션으로도 똑같이 가능한 작업이 있을 수 있습니다. 예를 들어 포스트의 내용을 적당히 바꾸어야 하는 작업을 해야 한다고 가정해 봅니다. 그러면 이런 작업은 포스트를 출력하는 '액션'에서 출력하기 바로 직전에 내용을 변경할 수도 있지만, 필터에서 데이터를 가져올 때 미리 데이터를 변경한 후 출력하게 만들 수도 있는 것이겠죠.

플러그인이 하는 일을 아주 간단하게 요약하자면 이렇습니다. 코어가 정해 둔 양식에 맞춰 할 행동을 등록하고, 코어에서 제공하는 여러 툴을 이용해 원하는 일을 하는 겁니다. 개념적으로 보면 거의 모든 플러그인들이 이런 방법으로 동작하겠죠.

데이터베이스

워드프레스의 테이블 구조에 대해 어느 정도 이해를 해야 플러그인을 작성할 때 원하는 기능을 효과적으로 구현할 수 있겠죠. 워드프레스는 상당히 다양한 형태의 정보를 저장할 수 있도록 테이블을 디자인해 두었지만, 기본적인 테이블의 수는 열 개 내외정도로, 그리 많지 않습니다. 반드시 먼저 워드프레스의 데이터베이스 구조에 대해 설명한 코덱스 페이지에서 자세한 정보를 먼저 확인하세요.

아래 항목은 가장 기본적인 사항 몇 가지만 나열하여 보았습니다.

메타 테이블

약간의 부연설명을 드리자면, 워드프레스의 테이블 중 '~meta'라는 테이블을 몇 개 발견하실 수 있습니다. 워드프레스 데이터베이스 버전이 변함에 따라 변동이 있을 수는 있겠지만 아마 wp_commentmeta, wp_postmeta, wp_usermeta 같은 테이블 이름을 보실 수 있을 겁니다.

이렇게 ~meta 접미사가 붙은 테이블은 그 접미사가 붙지 않은 테이블의 보조 역할을 맡은 테이블이라고 생각할 수 있습니다. 이러한 메타 테이블은 공통적으로 4개의 필드로 구성되어 있음을 알 수 있습니다.

이렇게 키/값을 사용자가 임의로 설정할 수 있도록 구성했기 때문에 데이터베이스는 많은 필드를 정의하지 않아도 되고, 임의의 필드에 대해서도 잘 대응할 수 있습니다. 보통 회원 정보를 담는 wp_users 테이블에는 회원의 이메일 주소는 기본적으로 저장할 수 있도록 해 둡니다. 그러나 그 이외의 여러 프로필, 가령 페이스북의 아이디나 카카오톡의 아이디, 약간 민감하긴 하지만, 회원의 전화번호 등등은 어떤 필드에 저장하두라고 특별히 정해 두지는 않았습니다. 허나 이런 필드가 필요할 경우 어떻게 해야 할까요? wp_users에 필드를 추가할까요? 그렇지 않습니다. 이렇게 부가적으로 불리는 데이터를 저장하라고 만든 테이블이 wp_usermeta 테이블입니다. 같은 방법으로 wp_commentmeta는 wp_comments에 대응되고 wp_postmeta는 wp_posts에 대응됩니다.

예를 들어, 서버 내 설치된 워드프레스를 개발하는 팀끼리 미리 어떠한 이름을 쓰기로 약속을 합니다. 이 이름을 메타 키로 정의합니다. 예를 들어 페이스북, 카카오톡, 전화번호에 대해 다음과 같이 메타 키로 쓰기로 합니다.

그러면 각 회원에 대해 테이블 작업을 하지 않고서도 얼마든지 확장된 정보를 저장할 수 있게 됩니다. 다음과 같은 회원이 있다고 하죠. 편의를 위해 2개의 필드만 나열하겠습니다.

ID user_login
1 admin
2 chulsoo
3 john
4 mary

이 때 이 회원들에게 각각 페이스북 아이디, 카카오톡 아이디, 전화번호를 입력한다면 다음처럼 될 수 있겠죠. 모든 회원이 3가지 값을 다 가지지는 않고 아예 값이 없을 수도 있습니다.

회원 페이스북 아이디 카카오톡 아이디 전화번호
chulsoo chulsoo_face chulsoo_kaka
john 555-1234
mary mary_kaka

위 두 표를 가지고 wp_usermeta 값을 그려 봅니다.

umeta_id user_id umeta_key umeta_value
56 2 facebook_id chulsoo_face
57 2 kakaotalk_id chulsoo_kaka
65 3 phone_number 555-1234
92 4 kakaotalk_id mary_kaka

이런 식으로 저장됩니다.

이렇게 키와 값으로 임의의 데이터에 대해 저장할 수 있는 방법이 있으므로 기본 테이블의 구조를 변경하지 않고도 어지간한 데이터는 큰 문제 없이 처리 가능합니다. 주의할 점은 메타 키 항목은 모든 데이터에 대해 공통적으로 사용해야 하며, 어떤 한 정보를 기록하기 위해 여러 가지 키를 섞어 쓰면 안 됩니다. 다시 말해 전화 번호를 저장하기 위해 'phone_number'라는 키를 쓰기로 약속했으면 끝까지 'phone_number'를 사용해야지, 'phonenum', 'phone', 'num', 'Phone_Number' 등등 원래 약속한 것과 다른 키 이름을 사용해서는 절대 안 됩니다. 이것은 데이터베이스에 'phone_number' 필드를 중복하는 것과 같은 행위입니다.

메타 테이블은 어떠한 형태의 데이터라도 효과적으로 확장하여 데이터베이스에 기록할 수 있도록 해 주는 편리함도 있지만, 만능은 아닙니다. 개발하는 팀 내부에서 명확히 규약을 맞춰 두지 않으면 이름 파편화가 생길 수도 있고, 개별적으로 필드를 설정하는 것만큼 성능이 뒤따라주지 않을 수도 있습니다. 또한 모든 형태의 자료 구조에 대해 100% 효율적으로 딱 들어맞는다고 말하기도 어렵습니다. 이럴 때는 워드프레스가 제공하는 테이블이 아닌 별도의 테이블 형태를 만들어 사용하시면 됩니다. 워드프레스에서 기본적으로 주는 테이블 이외의 테이블을 사용한다고 워드프레스에서 사용할 수 없는 건 절대 아니니까요.

포스트 타입과 커스텀 포스트

워드프레스는 기본적으로는 블로깅 저작 툴로 생각할 수 있습니다. 기본적인 동작은 거의 블로그로써 포스트를 작성하기 위해 있습니다. 그러나 워드프레스는 단순히 블로깅 뿐만 아니라 일반적인 형태의 웹사이트도 잘 지원하도록 확장이 가능합니다. 블로그의 게시물인 '포스트'뿐만 아니라 정적인 웹 페이지를 위해서도, 그리고 어느 정도는 일반적인 형태의 게시물을 저장하기 적절합니다.

워드프레스는 포스트를 작게는 한 블로그의 포스트로서, 크게는 어떤 작은 데이터 단위로서 취급합니다. 그리고 그 데이터 단위를 타입에 따라 구분하여 wp_posts 라는 테이블에 모두 저장하도록 합니다. wp_posts 테이블의 한 레코드는 블로그의 포스트가 될 수 있고, 정적인 페이지가 될 수도 있고, 굳이 웹페이지로 출력될 필요는 없지만 웹사이트 운영에 필요한 어떤 데이터의 한 단위가 될 수도 있습니다.

이 때 wp_post 내부에서 각 레코드가 어떤 타입인지를 정해주기 위해 'post_type'이라는 필드가 있습니다. 이 필드 값은 20자 내의 임의의 문자열을 저장할 수 있는데, 이를 통해 각 레코드의 정보가 어떤 목적을 위해 있는지를 구분해 줍니다. 워드프레스에서 기본적으로 정의한 포스트 타입으로 post, page 두 개를 들 수 있습니다. post는 말 그대로 블로그 포스트를 위한 타입이며, page는 정적인 한 페이지를 위한 타입입니다. wp_post 필드를 적절히 활용하여 저장하고자 하는 데이터를 계획하는 것이 가장 워드프레스와 잘 어울립니다.

예를 들어 어떤 학교의 반에서 학급 신문을 만든다고 합니다. 이 때 워드프레스를 이용해 웹사이트 형태로 구축한다고 합니다. 학급 신문 기사들은 기사 나름대로의 데이터 구조를 가지고 있을 것입니다. 기사 발행 시간이나 수정 시간, 기사를 입력한 사람, 본문, 요약, 헤드라인, 또는 기타 여러 부가적인 정보들을 필요로 하겠죠. 이런 것들을 위해 테이블을 다시 만들 필요는 없어 보입니다. 이렇게 하면 어떨까요?

필요한 정보 활용할 필드
기사 발행 시간 wp_posts.post_date, wp_posts.post_date_gmt 활용
기사 수정 시간 wp_posts.post_modified, wp_posts.post_modified_gmt 활용
기사 작성자 wp_posts.post_author 활용
본문 wp_posts.post_content
요약/헤드라인 wp_posts.post_excerpt
기타 wp_posts 의 필드 혹은 wp_postmeta 활용
카테고리/태그 워드프레스가 기본적으로 제공하는 카테고리, 태그 기능 활용
기사 타입 wp_posts.post_type에서 기사 성격별로 정의.
예) 새 소식: article_news, 사설: article_editorial, …

별로 힘들이지 않고 새로운 데이터 형태를 구축할 수 있습니다. 그리고 부족한 필드는 메타 필드를 이용하면 됩니다. 별도의 테이블을 쓰지 않고 포스트 타입을 확장하는 것이 멋진 이유는 이렇습니다. 워드프레스의 기본 타입인 post를 확장하는 것이기 때문에 워드프레스 내부에서 우리가 만든 타입은 post와 동등한 대우를 받습니다. 별도의 복잡한 로직을 재생산하지 쓰지 않고 워드프레스가 쓰는 방법 그대로 적용해서 데이터 작업을 할 수 있습니다. 또한 기존의 UI 요소를 완전히 재활용 할 수 있습니다.

이렇게 커스텀 포스트를 만들기 위해서 워드프레스는 일련의 API를 제공하고 있습니다. 더욱 자세한 내용은 Custom Post Types 코덱스, 그리고 register_post_type 함수 등을 참고하세요.

한편 register_post_type의 인자를 보시면 아시겠지만 매우 인자가 복잡합니다. 그래서 쓰기가 좀 복잡하고 불편할 수도 있는데, 이를 위해 Types라는 플러그인이 있으니, 이를 활용하여 보다 편하게 커스텀 포스트 타입을 정의할 수 있습니다.

텀, 택소노미, 그리고 포스트와의 관계

'텀(term)'은 '용어'라는 뜻이고 워드프레스 내부에서는 태그나 카테고리 하나하나의 엔트리로 생각할 수 있습니다. 포스트 타입만으로는 모든 저작물의 분류를 나누기에는 역부족이죠. 한 타입의 저작물 내부에서 세부적으로 정보를 분류해두기 위한 수단으로 태그나 카테고리가 기본적으로 제공됩니다. 태그나 카테고리나 둘 다 어떤 포스트에 대한 부가적인 정보이지만 태그는 수평적인 구조, 카테고리는 위계적인 구조를 가지고 있습니다.

'택소노미(taxonomy)'는 사물이나 개념을 분류하기 위한 방법이라고 생각할 수 있습니다.

예를 들어 워드프레스에서 '쇠고기'이라는 텀을 만들었습니다. 이 텀은 태그로서도 사용될 수 있지만, 블로그의 카테고리를 위해 사용된 텀일 수도 있습니다. 또한 쇠고기는 '음식 재료'를 분류하기 위한 카테고리를 위해 사용될 수 있지만 '내가 만든 요리를 주 재료별로 분류'하기 위해서도 사용할 수 있습니다. '음식 재료별'로 어떤 글을 쓰기 위해 음식 재료별 카테고리를 쓰는 것과, 내가 만든 요리를 포스팅하기 위해 '주 재료별'로 카테고리를 쓰는 것은 전혀 다른 맥락입니다.

그래서 워드프레스는 각각의 단어, 텀에 대한 정보는 wp_terms에 저장합니다. 그리고 이 텀이 어떤 맥락에서 사용되는지에 대해서는 wp_term_taxonomy에 저장합니다. 여기서 해당 텀이 태그로 사용되는지, 카테고리로 사용되는지도 정의합니다. 마지막으로 어떤 포스트가 어떤 텀/택소노미를 사용하는지는 wp_term_relationships 테이블에 저장합니다. 다시 정리하면,

그렇다면 '쇠고기'에 대한 텀이 워드프레스에서 태그나 카테고리로 쓰이는 예를 들어 보지요. 우선 쇠고기라는 텀을 만듭니다.

wp_terms

term_id name slug term_group
64 쇠고기 beef 0

텀의 이름은 '쇠고기'이지만 워드프레스 코드 내부에서는 보다 프로그래밍 하기 좋게 'beef'라는 문자로 생각합니다. 그래서 우리가 보기에는 '쇠고기'라는 용어는 워드프레스 코드 내부에서는 'beef'라고 해석됩니다 (이렇게 실제 용어를 분리해서 프로그램 내부에서 사용하기 좋게 식별자를 지정한 것을 '슬러그(slug)'라고 말합니다.)

이제 '쇠고기'라는 용어를 태그로 사용합니다.

wp_term_taxonomy

term_taxonomy_id term_id taxonomy description parent count
78 64 post_tag 쇠고기 태그 0 0

워드프레스에서 기본적으로 정의된 포스트 타입인 'post'를 위해 태그를 사용한다면 그 택소노미는 'post_tag'입니다. 마찬가지로 만약 우리가 특정 포스트 타입을 정의한다면 그 포스트 타입을 위해서는 taxonomy 필드에 고유한 문자열을 사용해야 합니다.

그럼 이 용어를 그대로 카테고리의 하나로도 써 보죠. 그럼 우선 상위개념을 텀으로 추가합니다. 내 요리를 재료별로 분류하기 위한 최상위 카테고리는 '내 요리'입니다.

wp_terms

term_id name slug term_group
64 쇠고기 beef 0
65 내 요리 my-cooking-by-ingredients 0

그리고 내 요리의 하위 카테고리로 쇠고기를 다시 추가합니다. 이 작업을 모두 워드프레스 UI에서 할 경우에는 '쇠고기'라는 용어가 중복되어 기록될 수 있습니다. 그렇지만 여기서는 그냥 반복하지 않고 하나의 텀을 재활용하겠습니다.3)

wp_term_taxonomy

term_taxonomy_id term_id taxonomy description parent count
78 64 post_tag 쇠고기 태그 0 0
79 65 category 쿠킹 최상위 0 0
80 64 category 쇠고기 카테고리 79 0

기본 포스트 타입인 'post'에서는 카테고리를 위한 택소노미는 'category'입니다. 커스텀 포스트에서 별도의 카테고리를 지정하려면 이 'taxonomy' 필드에 적절한 식별자를 따로 지정해야 합니다. 쿠킹 최상위는 parent가 0이고 카테고리로 사용되는 항목인 term_taxonomy_id 80번의 부모는 79번 '쿠킹 최상위'가 지정되어 위계 관계가 이뤄진 것이 보입니다.

이제 이것이 포스트에 적용된 것을 예로 듭니다. 포스트 ID 1023번에는 태그, 포스트 ID 1030번에는 카테고리를 적용해 보죠.

wp_term_relationships

object_id term_taxonomy_id term_order
1023 78 0
1030 80 0

사실 이렇게 카테고리를 지정하는 작업은 UI를 통해서 하는 것이 더 적절합니다. 왜냐하면 wp_term_taxonomy에는 count라는 필드가 있는데, 이 값은 그 분류에 속하는 포스트의 개수를 기록해 두기 때문입니다. 직접 DB를 조작하는 경우는 이 값을 염두에 두고 작업하는 것이 좋습니다.

참고로 term_group은 이렇게 쓰라고 딱 잘라 정해진 규칙은 없습니다. 플러그인 등에서 적절히 앨리어스로 활용할 수 있어 보입니다. 또한 term_order는 0으로 두고 잘 사용하지는 않지만 카테고리 출력 순서가 중요한 경우 지정해 둘 수 있습니다.

플러그인 예제: Hello, World!

플러그인을 이제 실제로 만들어 봅니다. 아주 간단한 플러그인을 만들어 보도록 할께요, 무의미한 플러그인이지만 나름의 동작을 하도록 만듭니다.

플러그인이 할 만한 기본적인 동작이라 생각해서 구성해 보았습니다. 분량상 DB를 읽어 오거나 업데이트하는 부분은 넣지 않았습니다. 콜백 함수가 두드러지다 보니 소스를 볼 때는 텍스트 검색을 꼭 사용하도록 하세요 ^_^

소스 설명

소스는 ​github에 별도로 저장해 두었습니다.

당연한 말이지만 나열된 기능들은 워드프레스 기능의 아주 일부에 불과합니다. 더 자세한 내용은 코덱스를 참고하셔야겠죠. 하지만 거의 일상적으로 사용될 법한 기본적인 플러그인 기능만을 요점 정리 식으로 모아 보았습니다. 아마 플러그인을 제작할 때 참고하는 편리하리라 생각합니다. 별도의 기능을 위해 특정 도움 함수를 만들지 않고 거의 뼈대를 있는 그대로 노출하는 식으로 전개해 보았습니다.

플러그인 소스를 참고하면 알겠지만 대부분이 어떤 액션을 걸고, 그에 대한 콜백 함수를 정의하는 것이 플러그인이 하는 일입니다. 그러다 보니 액션 및 필터의 선언 및 콜백 함수 정의의 범벅으로 코드가 전개되기 마련입니다. 보통 함수를 정의하고 액션(또는 필터)를 선언하든가, 아니면 그 반대로 하는데 저는 액션을 먼저 선언하고, 그 아래에 바로 그 액션에 대한 콜백을 선언하는 식으로 구성했습니다.

사실 플러그인을 제작하다 보면 콜백 함수에서 다시 콜백 함수를 정의해 주어야 경우가 흔합니다. 이렇게 콜백의 깊이가 깊어지면 추적하기도 어렵고 때로는 코드가 지저분하게 될 수도 있습니다. 이 점을 유념하고 플러그인 제작을 하시는 것이 좋겠습니다.

또한 플러그인에서는 MVC 패턴 등을 기본적으로는 지원하지 않습니다. 굳이 어떻게든 PHP 상에서 MVC를 도입하려면 별도의 PHP 프레임워크와 연동을 시키든지, 아니면 정말 하나하나 만들든지… 해야 합니다. 그런데 고작 플러그인 하나 만들자고 프레임워크까지 도입하기는 너무 무겁습니다. 이에 제가 별도로 워드프레스 플러그인과 테마를 위한 MVC 패턴을 도입하기 위한 실험적인 작은 프로젝트를 진행하고 있습니다. GitHub에 있으니 별도로 참고하시면 감사하겠습니다. 이것은 다른 문서에서 차후 다루도록 하겠습니다.

활성화, 비활성화, 제거

플러그인이 활성화되거나 비활성화 될 때, 그리고 아예 플러그인이 삭제될 때 어떤 콜백 함수를 실행시켜 주도록 할 수 있습니다. 예제에서는 활성화 될 때 현재 사용자의 이름으로 특정 옵션 값을 설정하고, 비활성화될 때에는 그 값을 삭제하도록 만듭니다.

워드프레스 플러그인은 삭제 전 반드시 먼저 비활성화를 시켜 두어야 합니다. 또한 활성화, 비활성화, 삭제 시 불리는 콜백 함수는 어떠한 문자열도 출력해서는 안 됩니다. 워드프레스 코어는 이런 문자열이 발생되면 모두 에러로 간주합니다.

플러그인이 특정 테이블을 관리해야 할 때 활성화, 비활성화, 삭제 액션을 아주 유용히 활용하겠죠. 보통 활성화 될 때 별도의 테이블을 생성하고, 비활성화 하거나 삭제될 때 해당 테이블을 삭제하는 식으로 플러그인을 만듭니다.

메뉴

플러그인에 메뉴를 넣지 않는 경우는 거의 없을 겁니다. 플러그인의 관리자 메뉴 삽입과 관련된 훅은 'admin_menu'입니다. 이 훅으로 액션을 삽입하고 콜백에 적절히 구성된 메뉴를 삽입하면 됩니다.

워드프레스 플러그인 메뉴

그림은 플러그인 메뉴가 삽입되고 마우스 커서가 해당 메뉴 페이지 영역에 접근했을 때의 상황을 보여 주고 있습니다. 하나의 메뉴 페이지에 2개의 하위 메뉴 페이지가 삽입된 상태입니다. 그래서 커서가 근접하면 나오는 오른쪽 부분의 서브 메뉴 부분에 총 3개의 항목이 보이게 됩니다. 이렇게 메뉴 페이지를 삽입하면 항상 기본적으로 같은 이름의 하위 메뉴 페이지가 가장 먼저 삽입됩니다. 이 첫번째 메뉴가 의도한 동작이 아니라면 remove_submenu_page를 호출하 삭제하면 됩니다.

POST 액션

어떤 폼을 제작해서 사용자로부터 데이터를 받아 서버로 전달하게 해야 한다고 생각해 봅니다. 이 때 메소드를 지정해 주어야 하는데, 주로 GET이나 POST 방법을 사용합니다. 전달할 곳의 URL은 어떤 형태이든 접근 가능하기만 하면 됩니다. 그러나 우리는 워드프레스 플러그인에서 워드프레스의 기본적인 토대 위해 이러한 작업을 처리하고 싶습니다. 그래야 워드프레스에서 제공하는 기능을 활용할 수 있죠. DB 접속 등등의 코드를 우리가 다시 반복해야 할 이유가 없습니다.

워드프레스는 GET/POST 전송에 대해 하나의 URL을 정해 두고 모든 요청에 대해 처리하도록 디자인되어 있습니다. 대신 각 요청에는 자신이 어떤 요청인지를 스스로 밝히는 식별자를 두고 이 식별자에 의해 어떤 종류의 요청인지 제각각 구분하도록 되어 있습니다. 보통 이 주소는 wp-admin/admin-post.php입니다. 이 주소를 얻기 위해 워드프레스는 admin_url이라는 함수를 제공합니다. 보통 이렇게 사용해서 URL에 관계없이 일괄적으로 주소를 얻어 활용합니다.

<?php echo admin_url( 'admin-post.php' ); ?>

모든 종류의 폼이 이 주소로 들어오므로 반드시 이 요청이 어떤 것인지를 구분해 주어야 합니다. 그러므로 모든 폼은 요청에 대한 식별자를 가지고 있습니다. 이 식별자는 'action'이라는 이름이며 이 이름으로 전달된 값으로 모든 요청을 구분합니다. 값은 그냥 문자열이므로 사용자가 알아서 고유하게 구분하기만 하면 됩니다. 이 값은 사용자에게는 보여질 필요가 없으므로 대부분 input 태그에 hidden 타입으로 미리 폼에 삽입되어 있습니다.

한편 사용자가 이렇게 데이터를 처리해주기를 원한다는 사실을 코어도 알고 있어야 합니다. 액션을 등록해야죠. 이를 위해서 코어가 제공하는 훅이 바로 'admin_post_$action'입니다. 여기서 $action 부분은 자신이 원하는 'action' 변수의 값으로 만들어야 하는 규칙이 있습니다. 이렇게 해 두면 코어는 알아서 폼을 전달 받았을 때 우리가 원하는 함수를 호출해 줍니다. 콜백 함수에서는 적절히 wpdb 같은 것을 이용해서 원하는 동작을 처리하고 원하는 곳으로 사용자를 리다이렉트 시키면 됩니다.

당연하지만 폼으로 전달받은 값이 항상 올바른 값이라고는 가정해서는 안 됩니다. 항상 폼으로 전달된 변수가 혹시나 서버를 공격하는 위험한 코드가 아닌지 검증해야죠. 이에 대해서는 data validation 코덱스를 참고하세요. 그리고 폼에 nonce를 넣어 보호하는 것도 잊지 마세요.

AJAX 액션

POST 방식은 간결하지만 페이지 전환이 일어난다는 단점이 있습니다. 한 페이지 안에서 보이지 않게 서버 요청을 하려면 AJAX 방식을 사용하는 것이 낫습니다. 워드프레스는 jQuery를 기본으로 탑재하고 있으므로 자바스크립트에서는 큰 어려움 없이 AJAX 요청을 사용할 수 있습니다. 다만 '$'를 기본적으로는 허용하지는 않으므로4) 항상 'jQuery'를 쓰는 것이 좋습니다.

AJAX 요청 또한 미리 서버에 등록해 두지 않으면 올바르게 동작하지 않습니다. 세팅이 올바르지 않으면 서버는 항상 '0'만을 리턴합니다. 등록은 언제나처럼 액션을 등록해 주는 것으로 시작합니다.

AJAX 요청을 등록하려면 'wp_ajax_$action' 훅 또는 'wp_ajax_nopriv' 훅을 사용하시면 됩니다. wp_ajax와 wp_ajax_noprive의 차이는 wp_ajax는 권한 있는 유저에게만 사용 가능하며 wp_ajax_nopriv는 권한이 없는(로그인하지 않은) 유저도 사용할 수 있다는 점입니다.

AJAX 액션 또한 어떤 특정한 URL에서만 요청을 받습니다. 일반적으로 이 URL은 wp-admin/admin-ajax.php입니다. 보통 admin_url( 'admin-ajax.php' ) 처럼 일관된 주소를 얻을 수 있게 씁니다. 그런데 POST 방식 때와는 달리 이 주소는 자바스크립트 코드 내에서 다루어져야 합니다. 이 주소는 보통 echo를 사용해 출력하기 보다는 워드프레스에서 스크립트의 로컬라이즈를 준비하는 wp_localize_script 함수를 이용해 자바스크립트 객체에 삽입되어 전달됩니다. wp_ajax_$action은 이 객체가 기본값으로 등록되지만 wp_ajax_nopriv는 기본으로 등록되지 않으므로 항상 wp_localize_script 함수에 해당 URL을 삽입해야 합니다.

이렇게 액션을 등록한 후, POST와 마찬가지로 AJAX 요청을 할 때 반드시 파라미터에 action 항목이 딸려 오고, 그 action은 훅에서 지정한 $action 문자열과 일치해야 합니다. 예제에서는 AJAX 호출을 할 때 action 이름을 'request_code'라고 이름지었습니다. 이 action을 서버에 등록하기 위해 다음처럼 코드를 넣었습니다.

add_action( 'wp_ajax_request_code', ... );

폼에는 미리 숨겨진 input 요소를 넣었습니다.

<input type='hidden' name='action' value='request_code' />

POST와 동일하게 action을 보고 우리가 등록한 콜백 함수를 호출해 줍니다. 함수 호출의 마지막에는 반드시 die를 실행하여 AJAX 응답만 출력하게 한 뒤 다른 동작을 강제로 멈추도록 합니다.

AJAX는 자바스크립트 기반으로 동작하므로 여기서 자바스크립트 삽입 방법 및 CSS 삽입 방법에 대해 언급하도록 하겠습니다. 물론 HTML 소스 안에 하드 코딩 방식으로 CSS와 스크립트를 우겨 넣어도 관계는 없습니다. 그러나 그다지 안정적인 방법은 아닙니다. 이를 위해 워드프레스에서는 wp_register_script, wp_enqueue_script, wp_enqueue_style 함수를 제공합니다. 이것으로 스크립트와 스타일 시트를 등록하세요. 예제에서는 스크립트에 대해서만 코드를 작성했는데, 스타일 시트는 이와 비슷하거나 더욱 쉽게 쓸 수 있으므로 생략했습니다.

보통은 간단하게 wp_enqueue_script 콜 한 번으로 스크립트 삽입은 끝납니다. 그러나 다국어를 대비해야만 한다면 wp_localize_script가 필요합니다. 이 때 추가적으로 wp_register_script를 호출할 수도 있구요. 그래서 저는 아예 처음부터 스크립트를 출력할 때는 wp_register_script → wp_localize_script → wp_enqueue_script 순으로 호출합니다.

Rewrite

일단 이 기능은 서버에 rewrite가 가능해야만 동작합니다. 예를 들어 아파치 서버를 쓴다면 rewrite 모듈이 있어야 동작합니다. 어떤 URL의 규칙을 다른 규칙으로 변경해주는 역할을 합니다. 보통 .htaccess 안에 많이 삽입하기도 하는데, 워드프레스에서 함수로도 이 기능을 제공합니다. add_rewrite_rule을 참고하세요.

이 기능을 이용해서 지저분한 URL을 좀 더 깨끗하게 만들 수 있습니다. 예제에서는 /tutorial/(어떤 문자열) 방식의 주소를 허용하도로고 했습니다. 이런 형식의 주소는 자동으로 index.php?tutorial=(어떤 문자열)과 같이 변형됩니다.

쿼리 파싱

rewrite를 이용해 /tutorial/(어떤 문자열)index.php?tutorial=(어떤 문자열)로 치환되었으나, 이것만으로는 부족합니다. 왜냐하면 워드프레스 코에에 파라미터인 tutorial=(어떤 문자열)을 어떻게 처리할 것인지를 전혀 알려 주지 않았기 때문입니다. 코어는 정해진 쿼리 변수 이외에는 반응하지 않도록 되어 있습니다. 우리가 임의의 파라미터를 처리하려면 미리 코어에 그 파라미터의 존재를 반드시 알려 주어야 합니다. 이를 담당하는 훅은 'query_vars' 훅입니다. 그리고 이 훅을 통해 제어하는 것은 사이트의 행동 방식이 아닌 데이터 자체이므로 add_action이 아닌 add_filter를 사용해야 합니다.

add_filter에 등록한 콜백은 하나의 인자를 받는데, 여기에 쿼리로 받을 수 있는 파라미터의 목록(array)이 전달됩니다. 이 목록에 원하는 내용을 추가한 후 반드시 이 목록을 리턴하세요. 예제에서는 'tutorial'이라는 파라미터를 허용하도록 콜백을 구성했습니다.

이제 쿼리 파라미터를 허용했으므로 파라미터의 값이 코어로 전달됩니다. 하지만 여기까지는 값이 전달된 것입니다. 이 값에 어떤 동작을 하라고는 정해 주지 않았습니다. 그러므로 액션을 추가하여 요청 파라미터를 분석 후 원하는 동작이 이루어지도록 해야 합니다. 이것을 위해 'parse_request'라는 훅이 있습니다. add_action을 통해 훅을 등록합니다. 이 훅의 콜백에 우리가 원하는 파라미터를 발견하면, 그 파라미터의 값에 따라 원하는 동작을 하도록 추가할 수 있습니다.

예제에서는 parse_request 훅에 대한 동작을 다음과 같은 방식으로 추가했습니다. 쿼리 중 tutorial이라는 파라미터를 발견하면 특정 페이지로 이동합니다. 이 특정 페이지는 플러그인이 임의로 만들어내는 페이지입니다.

템플릿 리다이렉트

플러그인에서 특정 페이지로 이동을 시키기 위해서는 'template_redirect' 액션을 사용합니다. 이것으로 플러그인이 임의로 템플릿이나 워드프레스 기본 '페이지' 메뉴를 이용하지 않고도 어떤 페이지를 생성할 수 있습니다.

커스텀 포스트

플러그인은 tutorial 타입의 커스텀 포스트를 생성합니다. 관리자 메뉴에서 이 포스트 메뉴가 나오고, 이 타입으로 새로운 글을 작성도 가능합니다. 커스텀 포스트를 생성하기 위해서는 init 훅에 register_post_type 함수를 호출하면 됩니다. 이 함수는 옵션의 개수가 엄청나게 많지만, 많은 값들은 적절히 기본값으로 써도 무방합니다.

그리고 '메타 박스'라고 하여 커스텀 포스트에 항상 고정적으로 들어갈 수 있는 필드들을 미리 UI 상으로 만들어 주는 기능도 가지고 있습니다 그런데 이 UI 위젯을 모조리 사용자가 직접 디자인해 주어야 하므로 앞서 말씀드렸든 폼을 구성하기 위해 콜백 함수가 많이 등록됩니다. 이러한 위젯은 재사용성이 큰데, 일일이 만들기도 힘들고 어렵습니다. 그래서 왠만해서는 그냥 types 플러그인이 훨씬 편합니다.

로컬라이즈

텍스트 도메인

간단히 말해 플러그인이 출력하는 텍스트가 어떤 소속인지를 지정하는 것입니다. 이렇게 지정해 두지 않으면 모든 번역문이 뒤죽박죽이 되어 있겠지요. 이 텍스트 도메인은 미리 메인 파일의 플러그인 헤더에 지정해 두고 사용하실 수 있습니다.

코드 처리

워드프레스 로컬라이즈는 gettext를 기반으로 합니다. 간단히 말해 출력되는 모든 문자열애 대해 다른 언어로 번역을 하고, 기본 언어가 아닌 다른 언어를 사용할 때 해당 번역을 출력하는 방식입니다.

이렇게 모든 출력되는 텍스트에 대해 미리 __(), _e(), _n() 등의 함수 처리를 해 두면, gettext를 활용한 텍스트 번역을 할 수 있습니다.

텍스트 번역

번역은 Poedit 같은 툴을 활용하는 것이 좋습니다. 예제에서는 메인 페이지에 나오는 'Hello, World' 한 줄과 register_post_type을 선언하면서 만든 텍스트가 전부이지만 플러그인 번역에 대한 예제로는 충분할 것으로 생각합니다.

위 그림은 Poedit 실행 화면입니다. Poedit은 인터페이스가 썩 좋은 편이 아니라 한 단계씩 자세히 설명하겠습니다. 예제 플러그인은 새롭게 시작하는 번역 작업이므로 새 카탈로그를 만들어야 합니다. File > New Catalog을 선택합니다.

Project 이름으로 적당히 지어 줍니다. 우리는 기본 영어에서 한국어로 번역을 할 것이므로 Language에는 ko_KR을 입력합니다. Plural Forms란에는 nplurals=1; plural=0;을 입력합니다. 다른 언어의 Pluram Form에 대해서는 이 곳을 참고하세요.

이제 플러그인 소스 경로를 지정해 주어야 합니다. Sources paths 탭을 선택해 줍니다. 그리고 다른 파일 탐색기를 통해 플러그인 소스 경로를 복사한 후 경로를 지정해 줍니다. 직접 창에서 경로를 선택하는 방법이 없습니다.

그다음 번역할 텍스트를 스캔할 키워드(함수) 이름들을 선언해 줍니다. 기본으로 '_', 'gettext', 'gettext_noop'가 있지만 워드프레스에서는 다른 키워드를 사용하므로 위 그림처럼 '__', '_e', '_n'을 넣어 줍니다. 확인 버튼을 누르고 po 파일을 다른 이름으로 저장하면 됩니다. 그리고 이렇게 만든 파일은 바로 .pot 파일로 만들어 템플릿으로 저장해도 됩니다. 이렇게 파일이 만들어진 후에는 소스에서 텍스트를 추출해 보여주게 됩니다. 혹시 보이지 않는다면 경로를 확인하고 Update 버튼을 눌러주세요.

po 파일을 저장할 때, '(워드프레스의 텍스트도메인 이름)-(언어코드)' 식으로 짓는 것이 편리합니다. 왜냐하면 워드프레스에서 로컬라이즈가 완성된 형태인 .mo 파일을 읽을 때 반드시 저런 이름을 지킬 것을 요구하기 때문입니다5). 예를 들어, 예제 플러그인의 텍스트 도메인은 'wp_plugin_tutorial'입니다. 이 때 한글 번역 파일 이름은 반드시 'wp_plugin_tutorial-ko_KR.mo'가 되어야 합니다. 비슷하게 플러그인에 대해 일본어로 번역을 했다면 'wp_plugin_tutorial-ja.mo'6)가 됩니다.

각 소스의 텍스트를 한국어로 변역해 주면 됩니다. Source Text를 보면서 하나하나 번역을 해 주면 됩니다. 아래처럼 Preferences 메뉴에서 'Automatically compile .mo file on save'를 체크하셨다면 저장 할 때마다 .mo 파일이 생성되므로 편리합니다.

모든 텍스트를 변역했으면 저장하고 poedit을 닫습니다. .mo 파일이 생성되면 플러그인에서 번역된 텍스트를 이용할 수 있습니다.

번역문 출력

플러그인 로딩 때 .mo 파일을 로드하도록 하면 됩니다. 해당 훅은 'plugins_loaded'이고 이 훅의 콜백에서 load_plugin_textdomain 함수를 호출하면 됩니다.

워드프레스가 영어와 한글로 번역되는지를 확인하세요. Settings-General에 보면 Site Language라는 항목이 있습니다. 여기서 'English'와 '한국어'를 찾을 수 있어야 하죠.

워드프레스를 영문으로만 설치했으면 한글 번역이 별도로 설치되지 않았을 수도 있습니다. 서버 설정에 따라 언어를 자동으로 다운로드 받을 수 있고, 언어 파일을 별도로 설치해야 할 수 있습니다. 언어 팩은 여기여기를 통해 개별적으로 다운로드 가능합니다. 플러그인의 언어는 워드프레스 사이트 언어에 종속됩니다. 각각 영어와 한글로 언어를 변경해서 플러그인에서도 언어가 변경되는지 확인해 보세요.

끝내며

그동안 워드프레스 플러그인에 대해 한 번 정리를 해 보고 싶었습니다. 플러그인 자체는 그다지 어려울 것이 없습니다. 코어에 원하는 기능을 등록하고, 그 등록된 기능에 대해 호출이 왔을 때 어떤 식으로 동작할지 정의해주는 것이 전부입니다. 여기서 어떤 때 어떤 훅을 써야 하는지, 어떤 API 함수를 써야 하는지 잘 알아 두는 것이 좋겠죠.

플러그인은 워드프레스를 확장하는 데 있어 핵심적인 역할을 합니다. 이미 수많은 플러그인들이 개발하여 코어 자체로는 하지 못하는 일들을 보조해 주고 있습니다. 잘만 활용하면 워드프레스만으로도 엄청나게 많은 일들을 할 수 있습니다. 보고 있으면 하나의 플랫폼 같다는 생각이 들기도 합니다.

조언해둘 것이 있습니다. 플러그인과 템플릿은 둘 다 워드프레스를 확장하는 기능입입니다. 그러나 둘의 역할은 분명히 다릅니다. 템플릿은 워드프레스를 어떻게 보여줄지에 더욱 집중이 된 요소이고, 플러그인은 기능적인 확장에 더 집중된 요소입니다. 다시 말해 잘못하면 둘의 역할을 구분 없이 섞어서 쓰는 것도 사실은 가능하다는 말입니다. 그렇지만 그렇게 기능을 섞으면 안 됩니다. 템플릿은 템플릿이 하는 것이 좋고, 플러그인은 플러그인이 하는 일로 제한해야 합니다.

예를 들어 템플릿에서도 액션을 걸어서 유저가 프로필을 업데이트 할 때마다 특정 동작을 할 수 있게 만들 수 있습니다. 예를 들어 프로필의 업데이트된 내용을 다른 서버와 동기화 한다든지 같은 시나리오가 될 수 있겠네요. 그런데 이런 기능을 템플릿에서 꼭 해야만 할 이유가 없습니다. 만일 템플릿을 바꾼다면 그 기능이 동작하지 않게 될 수 있겠죠. 이와 반대의 경우도 마찬가지입니다. 템플릿에서 해야 할 일으르 플러그인에서 해 버리면 그것도 좋지 않습니다.

이것은 플러그인 사용상의 조언입니다. 직접 플러그인을 만들어 보시거나, 만들어진 플러그인의 소스를 보면 아실 겁니다. 수많은 액션, 필터의 범벅으로 구성되어 있습니다. 이렇게 수많은 훅들이 걸리고, 또 그에 대해 서버가 이에 대응을 하는 방식으로 구성되어 있습니다. 그러므로 사용하지도 않는 플러그인을 무의미하게 활성화시켜 두면 그만큼 서버는 불필요하게 동작할 수 있겠죠? 그러므로 안 쓰는 플러그인은 비활성화 시켜 두는 것이 좋습니다.

워드프레스 플러그인에 대해 정리를 했으니 다른 것도 차차 정리해 두고 싶습니다. 다음과 같은 내용들이 다음 타겟이 될 수 있겠네요.

3)
워드프레스 4.2 부터는 하나의 term을 복수의 taxonomy가 중복하여 사용하지 않고 분리하도록 변경되었습니다. Working with “split terms” in WP 4.2+을 참고하세요.