본 포스트는 UI 디자인 등에 대한 전문적인 의견이 아니라, 워드프레스 관리자 화면이 어색하다는 사람들을 설득하기위해 써 본 글임을 알려 드립니다. 올바르지 못한 점이 있다면 피드백 부탁드립니다.
처음 쓰는 툴이니 어색한 것은 당연하겠지만, 워드프레스를 초심자들은 많은 부분들을 어색해합니다. 그 중은 상당히 오랜 기간 학습을 필요로 하지요. 그 중 대표적인 하나로 “관리자 화면”을 들 수 있습니다.
대부분의 초심자들은 관리자 화면에 익숙해지기까지 상당한 인내심을 지불합니다. 약간 과정하면, 마치 웅녀가 곰에서 사람이 되기 위해 동굴에서 쑥과 마늘 먹으며 괴로움을 참아야 했던 것처럼요. 그 괴로움을 끝까지 참아내기 어려운 경우도 많고, “도대체 왜 도구 때문에 내가 바뀌어야 하는가?”라고 생각할 수도 있습니다. 참지 못한 몇몇 ‘호랑이’들은 참지 못하고 다른 CMS로 갈아탈 것이고, 이 분들 중에는 결국 워드프레스에 강한 거부감을 보일 수도 있을 것입니다.
반면 잘 적응한 사람들은 대부분 워드프레스 관리자 화면은 합리적이고 쓰기 편하다고 합니다. 아무리 적응하기 나름이라고는 하나, 단순히 적응도 때문에 불편함과 편함이 생기는 것은 아닐 겁니다. 그러면 과연 무엇 때문에 어떤 이들은 불편하다고 느끼고, 어떤 이들은 편하다고 생각하는 걸까요?
워드프레스 관리자 화면이 익숙하지 않은 이유.
우선 워드프레스의 관리자 화면이 어색하고 불편하다면, 친숙하고 편한 스타일의 관리자 스타일이 존재할 것입니다. 그것을 무엇이라 칭하기 어렵지만 “기존 스타일”이라고 뭉뚱그려 표현해 보고자 합니다. 네, 우리 한국 사람에게 익숙한 문법과 문맥을 가진 바로 그 스타일입니다. 이 문법과 문맥을 거칠게나마 특성을 몇가지 나열해 보지요.
메뉴는 위계를 이루는데, 최상단에서부터 최하단까지 상당히 깊은 깊이, 적어도 3단 이상을 가지고 있습니다.
메뉴의 레이블은 “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 디자이너의 의견이 아니라 틀린 점도 많고, 제가 제대로 이해하지 못한 점이 많을 것입니다. 틀린 점이 있다면 댓글로 피드백을 부탁드립니다.
코드는 행복해야 한다. 그 클래스나 모듈 내에서 코드가 행복해 하면 좋다고 생각한다. 물론 어떤 것이 행복하냐? 물으면 그건 프로그래머 각자의 생각에 따라 다루겠지. 서로의 시각이 다르니, 같은 코드라도 다른프로그래머의 각각 다른 시선으로 “필터링” 되면 쓰레기가 될수도, 진주같이 빛나는 코드가 될 수 있다. 애매모호하다. 코드는 논리적이다. 그래서 대부분의 경우 확실하게 잘잘못을 나눌 수 있는 명제의 문제가 많다. 그러니 코드의 “행복론”이라니 이건 너무 추상적이지 않은가? 헛소리이지 않은가? 맞다. 헛소리 같다.
그렇지만 그래도 이런 행복론은 왠지 멋져 보인다. 허황되지만 은근 믿어 보고 싶다. 아, 그리고 아주 완곡하게 이야기해 보자. 코드는 프로그래머의 산물이다. 내가 프로그래머라면, 내가 짠 코드는 내 자식과 같은 것이다. 내 새끼들은 그래도 잘나고 멋있기를 바라는 것이 부모의 심리다. 나도 가능하면 내 코드가 잘나고 멋있었으면 좋겠다.
또 코드에 정답과 오답이 언제나 칼로 자르는 것처럼 나누어지냐고 한다면 또 그건 아니다. 아까와는 정반대의 이야기를 하니 모순 같지만 약간 차원이 다른 이야기다. 그러니까 이거다. 코드가 풀려고 하는 문제의 풀이 과정(알고리즘)은 정답이냐 오답이냐를 딱 잘라 이야기할 수는 있지만, 그 정답의 도출 과정은 제각각이다. 답이 여럿일 수 있다. 알고리즘이 아닌 방법론 차원으로 올라가면 프로그램은 세상만사처럼 딱 잘라서 선악 참거짓 우열을 가리기 어려워질 수도 있다. 예를 들어 프로그래밍 패턴 같은 것이다. 어떤 문제에 대한 어떤 패턴은 영 아니지만 어떤 패턴은 잘 맞는다. 한편 어떤 문제에 대해서는 다양한 해결 패턴이 존재할 수도 있다. 애매모호해지는 것이다. 괜히 프로그래밍이 우스갯소리로 “도 닦는 직업”이라고 하는 게 아니다.
그런 와중에 문득 행복론이란 생각이 들었다. 그래서 이 메모를 장황히 적고 있다. 사람은 어떻게 살아야 할까 고민을 한다. 많은 현명한 이들이 “행복”을 중요한 가치라고 조언해 주었다. 그래 행복하면 좋지.
엉뚱한 생각이 떠올랐다. 코드가 만약 사람이라면? 내가 짠 내 새끼들이 살아 숨쉬는 생물이라면? (머신 속에서 개념적으로 살아 숨쉬기는 하지. 우리는 이걸 개채화-instantiation라고 부른다.) 그러면 그 애들은 과연 행복할까? 살고 죽는 (생명 주기) 짧디짧은 순간을 행복하게 보내고 있을까?
내 멋대로 그런 가치를 사람의 행복론에서 따 와서 상상해 보았다. 뭐 잡동사니같은 생각인데, 괜히 멋지구리해 보였다. 코드가 행복해 한다? 어떻게 행복해할지는 지금은 잘 모르겠지만 직감적으로는 뭔가 작은 실마리 같은 게 잡힌다고 느낀다. 착각일 수도 있지만서도.
개발 의도상 테마를 별도의 디렉토리에 두고 싶은 생각이 들었다. 플러그인을 놓고 쓰는 것처럼 말이다. 그런데 wp-config.php 코덱스를 참고해도 플러그인의 경로를 바꾸는 일은 허용되나, 테마에 대해서는 이런 설정이 공식적으로는 존재하지 않는다. 테마를 별도로 쓰려면, wp-contents 디렉토리를 벗어나지 않는 한에서 변경하기를 권장하기도 한다.
사실 실사용 서버에 이런 일을 할 필요는 없다. 단지 개발 서버상에서만 편하자고 하는 일이다.
나는 코어와 플러그인의 디렉토리를 분리해서 사용한다. 이렇게 해서 여러 사이트에 대해 개발을 할 때 단 한 벌의 코어로 대응할 수 있다. 이렇게 사용하는 법은 다른 포스트를 통해 소개한 바 있다.
테마 디렉토리는 플러그인과 같은 레벨에 둔다고 가정한다. 그리고 MU 플러그인을 하나 만들고 거기에 그냥 다음과 같이 적으면 된다.
이렇게 하면 이동한 테마 디렉토리에 있는 테마 목록이 워드프레스 ‘외모’메뉴에서 보이게 된다.
그런데 이렇게만 하면 문제가 발생한다. 왜냐하면 이 시점에서 정적 자원들을 웹서버로 접근할 때의 URL이 정해지지 않았기 때문이다. 테마가 나오지만, 이미지나 자바스크립트는 404 에러를 내면서 죽을 것이다. 당연히 웹서버에 현재 /themes 디렉토리에 대해 URL로 접근할 수 있도록 처리해야 한다. 플러그인의 WP_PLUGIN_DIR과 WP_PLUGIN_URL 설정이 두 개로 나뉘어져 있는 것과 동일한 이치다.
User Role Editor 보다 더욱 세밀한 권한 체크를 진행시킬 수 있는 레시피. ‘user_has_cap’ 필터를 잘 활용하면 된다.
좀 더 구체적인 예로 설명을 하자. 만일 내가 포스트 아이디 1141번을 임시 글로 등록해 두었다고 가정하자. 그리고 단지 이 포스트에 대해서만은 editor들은 편집을 허용하지 않게 만들고 싶다. 그렇다면 다음처럼 코드르 만들 수 있다. 커스텀 포스트 타입은 ‘music_collection’이고 적절히 이에 따라 권한 세트를 생성했다.
콜백 함수는 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? 편집은 못하지만 지울 수는 있다. 편집자 역할은 휴지통에 있는 글도 영구 삭제 가능하다. 물론 같은 스태프끼리 그럴 일은 없겠지만… 아, 권한 체크는 세심해야 함을 강조한다. 해당 권한 목록을 꼼꼼하게 살펴서 이런 구멍이 없도록 잘 대비하기를 권한다.
2017년 02월 18일 토요일자로 6년간 사용한 아이폰을 교체했다. 갤럭시 A5 2017 모델이 이제 내 새로운 폰이 되었다.
그동안 많은 일을 같이 겪어준 폰인데, 물건에 감정을 너무 쓴 걸까? 마음이 짠하다. 대학원 때, 아르헨티나 때, 귀국 후 인턴, 게으른 프리랜서(?), 일산 고시원 시절하며 최근에 이르기까지… 늘 내 손, 호주머니, 가방에 있었던 녀석이었다.
그렇지만 이제는 너무나 느려지고, 한 번 갈았던 배터리도 시원찮아졌다. 추억은 많지만 더 이상은 같이 하기 어려울 것 같다. 일산 원룸으로 옮기고 나서 인터넷 사정이 그닥 좋지 않은데, 후진 3G 망으로는 도저히 인터넷 사용량을 감당할 수 없었다.
언젠가는 애플 휴대폰을 다시 쓸 날이 올 거라 생각한다. 일산에 와서 많은 것이 새롭게 변해 간다. 새로워지는 일은 나쁜 일은 아니지만, 익숙한 것을 버린다는 것은 상당히 자주 짠한 감정을 가져 와서, 그걸 안고 가기 버거울 때가 많다. 쓸데 없이 감상적이다. 밤이라 그렇다.
팁이라고 하기는 너무 거창하고… 현재의 내 상태를 기록해 두는 뜻으로 포스팅을 해 봅니다. 워드프레스를 개발할 때 즐겨 사용하는 세팅을 기록합니다. 고도로 숙련된 세팅이라고 할 수는 없으니, 저 아닌 다른 분들은 “아, 얘는 이런 식으로 쓰는 구나” 하고 참고만 해 두셨으면 합니다.
기본 환경
OS
OS는 리눅스 민트를 사용합니다. 리눅스 중에서는 가장 대중적이고, 무탈하고 쓰기 편합니다. 여러 리눅스 OS를 거쳐가며 삽질을 해 봤지만, 초기 부팅 및 설치 후 사용성까지 생각하면 민트를 따라잡을 OS는 없다고 개인적으로 생각합니다.
윈도우 10이 리눅스 서브시스템을 지원하고, 윈도우에서 apt나 bash를 실행시킬 수 있도록 많이 변화하기는 했지만, 아직 안정적이라고 볼 수 있는 단계는 아닙니다. 개인적으로 윈도우는 게임을 할 때 사용하는 OS로 굳어져 있습니다.
물론 맥OS도 좋은 개발 환경을 제공합니다만, 리눅스를 택하는 이유는 강력한 패키지 매니저 때문이죠. apt로 엄청나게 빠르고 편하게 패키지 설치를 할 수 있습니다. 개발 환경은 이렇게 무난하게 꾸미는 것이 최선이라고 생각합니다.
옵션: Vagrant
한때는 vagrant를 활용해서 개발 환경 세팅은 모두 vagrant를 활용한 가상 OS에 밀어 넣은 적이 있었습니다. 이것은 이것대로 엄청난 장점이 있죠. Provision 스크립트를 미리 만들어 둔 덕에, 초기 개발 환경 세팅이 단 2~3분안에 뚝딱 끝나는데다, 항상 균일한 환경을 유지할 수 있다는 점은 엄청나게 매력적이었습니다. 이 때는 가상 OS로 우분투 서버를 사용했습니다. 소스 코드, 웹브라우저, IDE는 호스트에 놓이고, 개발 환경만 게스트에 놓이도록 만들었습니다.
하지만 장점에는 단점도 있는 법. 개인적인 개발을 할 때는 조금 불편한 면이 종종 있었습니다. 호스트와 게스트는 별도의 OS라, 제대로 IDE에서 디버깅을 하기 위해서는 두 OS간 프로젝트의 경로 매핑이 필요합니다. 플러그인을 새로 제작할 때마다 경로 매핑을 하는 거나, 호스트와 게스트의 경로를 각각 숙지해야 하는 점은 좀 짜증나더군요.
개인적으로 워드프레스 플러그인 개발을 위해서 이렇게까지 vagrant 까지 도입해야할 필요성은 좀 없지 않나 하여 이제는 쓰지 않습니다.
개발 스택
Apache 2.4, PHP, MySQL을 사용합니다. 세 콤포넌트 모두 패키지 관리자의 기본 환경을 사용하고 있습니다. 개발 환경에서 그렇게 세세하게 세팅을 할 이유가 없다고 생각하기 때문에 nginx나 mariadb는 그렇게 고려하지 않고 씁니다. 가상호스트 설정 등등에서 apache가 세팅하기 수월하며, debian 설치 관리자 apt에서는 mysql-server 패키지를 설치할 때가 mariadb-server 패키지를 설치할 때보다 훨씬 간결하게 설치가 끝납니다. phpmyadmin 또한 설치 명령어 한 방에 끝납니다.
참고로 우분투 14.04에서는 PHP 5.6이 기본이지만 16.04에 와서 PHP 7.0이 기본입니다. 원한다면 리포지터리를 추가해서 가장 최신의 PHP 버전을 설치할 수도 있습니다. 소스 컴파일 설치는 별로 선호하지 않습니다.
IDE
에디터로 Atom이나 Sublime Text도 선호되고 있지만, 저는 IDE는 PhpStorm을 사용하고 있습니다. 몇 년째 사용 중인 훌륭한 IDE입니다. 제게는 다른 대안이 없습니다.
워드프레스를 위한 레이아웃
플러그인을 몇 번 제작해 보니 경로를 조직적으로 구성하는 편이 효율적으로 보였습니다. 그래서 머리를 굴려 나름 레이아웃을 구상해 보았습니다.
기본적인 경로는 이렇게 되어 있습니다. .php, .dist 확장자를 제외한 항목은 모두 디렉토리입니다.
각 워드프레스 환경은 아파치의 가상호스트로 구분합니다. 각 호스트는 apache2 디렉토리 안에 .conf 파일을 두어 설정합니다. 이 설청 파일을, /etc/apache2/sites-available에 심볼릭 링크를 걸고, a2ensite로 활성화 시켜 줍니다. 아래 코드는 예입니다.
이후 다시 설명하겠지만, 위 코드는 워드프레스를 버전별로 수동 관리하는 케이스입니다. 각 버전별로 “http://wp.<버전 번호>/” URL로 접근하도록 처리했습니다.
bin 디렉토리
wp-cli와 phpunit 같은 바이너리 파일을 여기에 두었습니다. wp-cli는 사용할 때마다 헷갈립니다만, 가끔씩 워드프레스 동작을 스크립트화 해 두기에는 좋을 겁니다.
niu-plugins 디렉토리
디렉토리 중 mu-plugin (must-use plugin)이라고 해서, 항상 사용하는 플러그인을 넣어 두는 곳이 있습니다. 그걸 따서 niu-plugin (not-in-use plugin) 디렉토리를 별도로 만들었습니다. 큰 의미는 없고 잠시 사용하지 않을 플러그인을 이 쪽에 놓아 둘 목적으로 만든 것입니다. 활성/비활성 차원이 아니라 아예 존재를 이 쪽으로 옮겨 두는 거죠.
plugins 디렉토리
플러그인만 이 곳에 놓아둡니다. 모든 워드프레스 개발 사이트는 이 플러그인을 공유합니다. 활성/비활성으로 적절히 조절하면 되니까 이렇게 몽땅 놔두는 것도 큰 문제가 없습니다.
워드프레스의 코어가 여기 있습니다. 코어를 관리하는 방법은 크케 나눠 두 가지가 있습니다.
1벌의 코어를 직접 디렉토리 아래에 두고, 워드프레스에서 코어 업데이트가 발표되면 같이 따라간다.
버전별로 코어를 관리. 업데이트는 막되, 버전별로 하위 디렉토리에 위치.
코어 버전이 업데이트될 때 종종 데이터베이스도 업데이트됩니다. 큰 변화는 별로 없지만, 그래도 테이블이 한 번 업데이트되면 되돌리기 어렵습니다. 개인적으로는 예전에는 단순한 까닭에 1번 방식을 썼는데, 버전별 차이에도 좀 대응하고 싶어 이제는 손이 좀 더 가더라도 2번을 쓰려고 합니다.
wp-settings 디렉토리
도메인별 워드프레스 설정을 놓아두는 디렉토리입니다.
워드프레스 설정
wp-config.php 파일은 워드프레스 코어에 있어도 되지만, 보안상 코어 하나 위 디렉토리에 두어도 무방하도록 되어 있습니다. 여러 사이트가 특별히 각자의 세팅을 가질 필요 또한 크지 않으므로 wp-core의 상위 디렉토리에 wp-config.php 파일을 위치시켰습니다.
만약 버전별로 코어를 관리하는 경우, wp-core 하위에 각 버전별 코어가 위치하므로 wp-congif.php가 1계층이 아닌 2계층 차이가 납니다. 이 때에는 wp-core 디렉토리에 wp-config.php의 심볼릭 링크를 설정해 주면 됩니다.
개발 환경이므로, 디버깅은 항상 켜 둡니다. 단, 디버깅 용 로그를 웹브라우저로 띄우면 화면이 매우 지저분해지므로 로그로만 볼 수 있도록 만듭니다. wp-content 디렉토리는 기본적으로는 각 코어별로 사용하도록 두었으므로 로그 파일은 각 <코어>/wp-content/debug.log가 됩니다. 스크립트 디버깅도 켜 둡니다.
이렇게 해도 워드프레스 코어를 업데이트하라는 알림이 관리자 화면에 출력될 수 있습니다. 워드프레스 업데이트를 완전히 꺼 두는 플러그인이 있습니다. 이런 플러그인을 활용해 혹시 실수라도 업데이트를 진행하지 않도록 처리하면 좋겠죠.
도메인별 세팅
도메인별로 다른 데이터베이스 테이블을 관리할 수도 있고, 또 모든 도메인이 같은 테이블을 공유할 수도 있습니다. 그러나 버전별로 워드프레스를 관리하려면 반드시 버전별로 다른 테이블을 사용해야 합니다. 앞서 말씀드렸듯 워드프레스 버전마다 데이터베이스 버전도 달라자기 때문입니다.
버전별로 워드프레스를 관리하기 위해 다음과 같은 도메인 규칙을 만들었습니다.
wp.<워드프레스 버전>
그러므로 워드프레스 4.6.2 버전은 “http://wp.4.6.2/”, 4.7 버전은 “http://wp.4.7/”로 접속합니다.
물론 이렇게 하려면 /etc/hosts 파일에 해당 도메인으로 접속할 수 있도록 추가해야 합니다.
127.0.0.1 wp.latest wp4.7 wp.4.6.2 wp4.6.1
그리고 wp-settings/wp.4.6.2.php, wp-settings/wp.4.7.php 같은 도메인 이름과 같은 php 파일을 두고, 그 안에 도메인별 설정을 넣으면 됩니다.
코어 하나만을 사용하는 경우에도 비슷합니다. /etc/hosts 파일 안에 각 도메인에 IP를 127.0.0.1로 대응시키고, wp-settings/<domain>.php 파일을 만들면 됩니다.
설정 파일에는 최소 하나의 변수가 있어야 합니다. 아래는 그 예입니다.
<?php
$table_prefix = 'wp471_';
마치며
위 설명한 구조는 제 github repository에 있습니다. 손은 좀 가지만 그리 어려운 것은 아니므로 다운로드 받아서 한 두번 살펴보면 쉽게 파악할 수 있을 것입니다.
그리고 이 곳에 우분투 16.04 서버를 위해 만든 provision 스크립트가 있습니다. 개인적으로는 APM 설치 및 세팅에 알찬 도움이 되고 있습니다.
테마는 제가 잘 다루지 않으므로 이 레이아웃이 테마까지 효율적으로 관리할 수 있을지는 모르겠습니다만, plugins 디렉토리를 응용해 테마 디렉토리를 별도로 관리한다면 플러그인과 큰 차이는 없을 것이라 생각해 봅니다.
요즘 워드프레스에 뜸했다. 며칠 전 워드프레스 4.7.1을 보다가 훅의 구현이 엄청나게 변한 것을 알게 되었다. 구체적으로 어떤 점이 변경되었는지 알아 보자.
우선 이 포스트에서 WP_Hook 이란 클래스가 새롭게 도입되었다는 사실을 발견할 수 있었다. 저 포스트에서 발견한 trac 페이지를 참고하면 대략 다음과 같은 이유로 도입이 되었다는 사실을 접할 수 있다.
필터와 액션은 아주 오래전부터 워드프레스의 플러그인 기능의 기반으로 있어 왔습니다. (중략) 하지만 몇 년동안 엣지 케이스(edge case)가 불거져 나오게 되었습니다. 특히 훅을 재귀적으로 실행하거나, 훅을 스스로 지우려고 하는 경우 기존의 구현은 매우 복잡한 노력을 기울여야만 했습니다 (중략)
사실 4.7에서 WP_Hook 클래스 때문에 발생할 수 있는 변경점은 크게 없다. 다만 $wp_filter 등 훅과 관련된 전역 변수에 직접적으로 접근한 경우, 인용한 글과 같이 훅을 재귀적으로 쓰는 코드는 더러 문제가 발생할 수도 있다. 허나 $wp_filter 등을 직접 접근은 완전 변태 코드. 피할 수 없는 문제가 아닌 이상 잘못된 접근이고, 또 훅을 재귀적으로 쓰는 일 또한 그리 흔치 않다. 그러니 기존의 코드는 거의 변함 없이 유지될 수 있을 것이다.
하지만 나는 궁금하다. 도대체 어떤 문제 때문에 훅 매커니즘은 이제 세대교체를 해야만 했을까? 그래서 알아 보기로 했다.
Action & Filter
다들 알 거라 생각하겠다. 액션(action)과 필터(filter)는 사실 같은 존재라는 걸. 워드프레스는 미리 시나리오를 다 짜놓고 필요한 구간에 ‘훅’ 이란 것을 걸 수 있도록 장치했다. 이 훅에 대한 콜백 함수를 편리하게 쓸 수 있도로 만든 장치가 액션과 필터이다. 다시 말하지만 액션과 필터는 같은 콜백 함수이며, 단 하나의 차이점은 액션은 리턴이 없고, 필터는 값을 리턴한다는 점 뿐이다.
웹 프레임워크들은 모든 기능들을 블록처럼 만들어 놓고 개발자가 의도에 맞게 그 모든 블록을 완전히 구현시켜 쌓아가는 방식이다. 반면 워드프레스는 이미 워드프레스 코어라는 완성품을 만들어 놓은 상태이다. 단, 그 코어에는 아주 많은 ‘구멍’을 뚫어 놓았다. 플러그인이나 테마는 그 구멍에 맞춰 적절히 콜백 함수를 끼워 넣는다. 이미 완성된 덩어리에 개발자가 자기 의도대로 부가 확장하는 방식이다.
기존 구현의 문제점
기존 혹 관련 API는 wp-includes/plugin.php 파일에 있으며, 4.7 버전에도 변함없이 존재한다. 단 WP_Hook이 있으므로 직접 $wp_filter를 조작하는 코드 일부를 WP_Hook에 넘겨 주었다. 이를 테면, 4.6 버전에서는 add_filter 함수의 구현은 이렇게 되어 있다.
함수 내부에서 $wp_filter에 훅 이름($tag)과 우선순위($priority)에 맞춰 콜백이 등록된다. array가 잔뜩 들어간 지저분한 구현이고, 개인적으로 PHP의 array는 정말 이상한 물건이라 생각하지만, 아무튼 그렇다. $wp_filter는 무려 4중첩의 array이다.
WP_Hook() 객체를 사용하고 WP_Hook::add_filter()를 사용하는 것이 보인다.
WP_Hook 클래스는 wp-includes/class-wp-hook.php에 구현되어 있다. 재미난 것은 이 클래스에 $nesting_level이라는 private 변수가 하나 있다. 만일 필터(또는 액션, 앞으로는 액션과 필터를 필터로만 언급)가 재귀적으로 실행되는 경우에 이전 구현에서는 이를 처리할 방법이 없었지만, 이제는 이것을 감지해 낼 수 있게 되었다.
필터가 재귀적으로 실행되는 경우 기존의 구현은 확실히 문제가 있다. 다음 4.6 버전의 appy_filters() 코드 조각을 살펴 보자.
...
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
if ( !is_null($the_['function']) ){
$args[1] = $value;
$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
}
} while ( next($wp_filter[$tag]) !== false );
...
약속된 모든 모든 콜백함수를 경유하면서 PHP의 call_user_func_array()를 이용해 콜백 호출을 하는 것이 보인다. 마지막 줄에서 do ~ while 루프 안에서 next()로 array pointer를 이용해 각 priority를 단순하게 순회하는 것이 보이는가? 여기서 재귀적인 apply_filter()가 호출되면 문제가 발생한다.
이 함수가 2번 재귀적으로 호출된다고 가정해 보자. 그러면 do ~ while 루프 안쪽에 같은 구조의 do ~ while 루프가 만들어지는 것과 비슷하게 된다. 그런데 next( $wp_filter )라는 부분은 두 함수가 공유하는 부분이다. 그러므로 안쪽의 루프가 먼저 array를 소진해버린다. 결국 번째 함수의 실행이 끝나고 첫번째 함수로 돌아온 시점에서 next( $wp_filter )는 첫번째 함수가 의도한 유효한 현재 이후의 순위를 가진 콜백 함수 목록을 가져오지 못한다.
필터 적용은 전역적이며, 일괄적으로 동작한다
필터의 콜백 선언은 전역적이며 일괄적이어야 한다. 만약 10개의 콜백 함수가 등록되었다면, apply_filters(), do_action() 호출로 필터를 적용하면 언제나 등록된 그대로 10개의 콜백이 실행되어야 한다.
그런데 기존의 구현은 어떤 조건에서 이 가정을 올바르게 지키지 못하는 경우가 있다. 그 대표적인 케이스로 ‘필터 적용이 재귀적으로 일어날 때’를 들 수 있다. 다음과 같은 조건이라면 이 현상이 일어난다.
콜백 함수가 종료되기 전에 해당 훅에 대한 필터를가 재차 적용된다.
콜백 함수에허 해당 훅에 대한 편집이 이루어진다.
흔히 잘 알려진 재귀적인 필터 사용의 예로 들 수 있는 것 중 하나는 ‘save_post‘ 액션이다. 이 액션은 포스트가 저장될 때 사용되며, 해당 콜백에서 포스트 저장 시 동작을 일부 변경, 확장 가능하도록 해 준다. 흔히 이 함수 내부에서 플러그인이나 테마의 특정 데이터를 데이터베이스에 저장하도록 프로그래밍을 하는데, 흔히 이 콜백에서 어떤 포스트를 업데이트하는 코드를 작성하곤 한다. 포스트를 “저장”하려는 시점에 실행 중인 콜백이 포스트를 재차 “저장”하려고 시도하는 것이다. 만약 부주의하게 프로그래밍하는 경우 콜백은 무한으로 증식해 에러가 난다. 그래서 save_post 액션에 대한 코덱스에서는 ‘Avoiding infinite loops‘라는 항목을 따로 두어 주의를 주고 있다.
코덱스에서 서술하는 것과 같이 무한 루프만을 막기 위한 일시적인 필터 등록 해제, 재등록 정도는 아무런 부작용이 없다. 그러나 해당 콜백 내부에서 자신의 필터를 변경하려고 하는 경우에는 문제가 발생하게 된다. 자세한 것은 실험을 통해 알아보자.
실험: 4.6과 4.7의 콜백 동작 차이
WP_Hook 클래스의 도입 후 변경된 훅의 동작을 실험해 보기 위해 실험 플러그인을 작성하여 gist에 등록해 두었다.
코드 설명
플러그인에서 ‘wph47_test01’이라는 훅을 실행한다. 이 훅의 콜백에서는 자기 자신을 변경한다. 또한 콜백 함수 안에서 다시 필터를 적용하는 기행(?)을 저지른다.
wph_47_test01_body()는 wph47_test01 필터를 적용한다. 콜백 함수인 wph_test01() 안에서 자신의 훅을 편집한 후 재차 wph47_test01 필터를 적용하고 있다. ‘wbh47_cb_x ‘ 함수들은 간단하게 ‘x’를 로그로 출력한다.
코드 결과
4.6 버전과 4.7 버전의 코드 결과는 약간 다르다. 먼저 4.6의 실행 결과는 이렇다
test begin
9
11
12
13
test end
그리고 4.7의 실행 결과는 이렇다. 4.6과 다른 부분은 별표를 뒤에 붙였다.
test begin
9
11
12
13
11 *
12 *
13 *
test end
9, 11, 12, 13은 각각 우선순위 9, 11, 12, 13에 의해 등록된 함수에 의해 출력된 것이다. 그런데 왜 버전 4.7에서는 11, 12, 13이 반복된 것일까?
위 코드에서 우선순위 10으로 실행된 콜백 함수 wph47_test01()안에서는 같은 이름의 wph47_test01 훅을 변경한다. 우선순위 9, 11, 12, 13의 액션을 등록하고, 다시 해당 액션을 재차 부르는 것이다. 그러면 재귀적으로 불려진 액션의 콜백에 의해 9, 11, 12, 13이 출력된다.
여기서 주의해야 한다. 이 시점에서 훅의 상태는 변경되었다. 콜백은 우선순위 9, 10, 11, 12, 13로 구성되어 있다. 우선 순위 10번은 무한 루프 방지를 위한 코드 때문에 한 번만 실행된다고 간주하더라도, 10번 이후의 11, 12, 13이 등록되어 있는 상태이다. 그렇다면, 재귀 호출이 끝나고 한 단계 위의 함수로 올라가게 되면 (콜스택이 하나 줄어들면) 저기서 등록된 훅들은 실행되어야 하는가? 아니면 무시되어야 하는가?
앞서 말했듯이 필터는 전역적이다. 필터를 명시적으로 해제하는 명령이 없는 이상, 프로그램이 마음대로 콜백을 호출하고 말고를 결정할 수는 없다. 그러나 잘 살펴보면 버전 4.6에서는 이를 무시하는 결과가 나오고 있다. 물론 훅의 변화가 재귀적으로 호출된 함수 내부에서 일어나는 것이 특이하긴 하다. 그러나 그깟 재귀함수가 뭐라고?
4.7은 콜백 내부에서 등록된 필터라도 그 콜백이 끝난 후에 잘 반영된다. 11부터 반복된 것은 wph47_test01() 함수가 우선순위 10을 가지기 때문이다. 훅의 증가 뿐만 아니라 훅의 감소에도 당연히 올바르게 동작한다.
변한 것은 없다. 더 정교해졌을 뿐.
워드프레스가 제공한 API 함수만을 이용해 플러그인을 작성한 경우 특별히 코드를 변경할 이슈는 거의 없으며, 이렇게까지 트리키한 코드를 사용하지 않는 이상 딱히 이 변경점을 고려할 이유는 없다.
WP_Hook을 부분적으로 이해한 현시점에서 어떠한 개선이 이뤄진 것인지 조목조목 파악하지는 못했지만 보다 정교해진 코드가 나로서는 반갑다. 사실 예전에 디버깅을 위해 콜백 함수를 덤프해 보면 array가 양파처럼 구성된 것을 보며 약간의 혐오감까지 느낀 적이 있던 터라, 오히려 “오 여기가 드디어 변경되었나?” 하는 반가움이 든다.
워드프레스의 권한(authority) 시스템은 매우 잘 구축되어 있다. 권한은 세부적으로 역할과 권한(capability)으로 쪼개어져 문맥(context)에 따라 어떤 작업을 허가할 수도, 거부할 수도 있도록 조직되어 있다.
역할 (Roles)
사실 ‘역할’은 ‘권한’보다는 커스텀 포스트와는 덜 밀접한 관계이긴 하지만 권한과 역할이 워낙 가까운 관계라 언급은 해야 할 것 같다. 쉽게 말해 사용자의 그룹이라 볼 수 있다. 한 사용자가 사이트 내부에서 어떤 담당인지를 나타내 준다.
각 역할 별로 일정한 권한의 목록이 기본으로 주어진다. 물론 새로운 역할도 추가할 수 있고, 특정 권한을 더 추가할 수도, 덜어낼 수도 있다. 역할에 대해서는 자세한 설명을 생략하도록 하겠다. 코덱스를 참고하기 바란다.
권한 (Capabilities)
워드프레스에서 동작에 대한 문맥을 나타낸다. 예를 들어 포스트를 편집, 삭제, 발행하는 각각에 대한 동작들을 의미한다. 역할과 마찬가지로 사용자별로 권한을 더해줄 수도, 덜어낼 수도 있으며 완전히 새로운 권한을 만드는 것도 가능하다. 단, 여기서는 커스텀 포스트 옵션과 관련된 사항에서만 설명을 하도록 하겠다.
커스텀 포스트의 권한 대응
커스텀 포스트는 기본 포스트 타입을 확장하여 사용하는 것이므로, 기본 포스트가 가진 권한 시스템을 그대로 물려받게 된다.
포스트, 혹은 페이지에 대해 다수의 권한이 있고, 커스텀 포스트를 생성하면서 이 권한들을 1:1로 모두 새롭게 재정의해 주어야 코어 내부에서 올바르게 권한을 체크할 수 있다.
예를 들어 publish_posts 권한을 살펴 보자. 이 권한이 주어지지 않은 사용자는 글을 쓰고 편집할 수는 있어도 편집 화면에서 ‘공개하기(publish)’ 버튼이 나오지 않는다.
Contributor의 글 작성 화면. Publish 대신 Submit for Review 버튼으로 나온다.
위 그림을 보자. 어떤 기여자(Contributor) 역할의 사용자가 새 포스트를 작성 중이다. Publish 메타 박스를 보면 보통 볼 수 있는 ‘Publish’ 버튼 대신 ‘Submit for Review’라는 버튼으로 변경되어 있다. 왜냐하면 기여자는 새 글을 쓸 수는 있고 그 글을 편집할 수는 있지만 글을 최종 단계인 발행 상태로는 만들 수 없기 떄문이다.
자, 이제 커스텀 포스트를 생성해야 한다고 보자. 그럼 publish_posts 권한에 대해 두 가지 결정을 할 수 있다. 기존의 포스트(혹은 페이지)와 동일한 권한을 가질 것인가, 아니면 별도의 권한으로 나누어야 할 것인가.
동일한 권한이라면, 글(혹은 페이지)를 편집, 수정 발행할 수 있는 권한을 가진 사용자는 커스텀 포스트에 대해 동일한 작업이 될 것이다. 반면 별도의 권한을 가지도록 처리한다면, 조금 성가시기는 하겠지만, 보다 세밀하게 권한을 나누어 원하는 대로 분업을 진행할 수 있을 것이다.
그럼 이제 PHP 코드 관점에서 권한의 구조를 생각해 보자. 권한은 연관 배열(혹은 stdClass)을 사용한다. 권한의 공통적인, 즉 ‘문맥상’의 호칭은 연관 배열의 키(혹은 속성)로, 해당 실제 권한명은 값으로 기록한다. 아래는 그 예이다.
두 포스트 타입, ‘sample_audio’와 ‘sample_video’를 등록할 때 publish_posts라는 문맥상의 권한에서 audio는 포스트의 것을 그대로 가져오는 반면 video는 ‘publish_videos’라는 새로운 권한 이름을 지어 냈다.
$caps[] = $post_type->cap->publish_posts;
반면 코어 코드에서는 이렇게 쓰이기도 한다. 내부적으로 array를 stdClass로 타입 변환하는 과정이 생겨 키가 아닌 속성으로 불리긴 하나, 이 코드에서 audio 포스트타입이라면 $caps[]에 저장되는 문자열은 ‘publish_posts’, video 포스트 타입이라면 ‘publish_videos’가 될 것이다.
map_meta_cap: 옵션, 함수, 그리고 필터의 이름
여기저기서 map_meta_cap이라는 문자열이 언급되는데, 워드프레스 코어에서 이 것이 세 가지 의미로 쓰이고 있다. 애매모호함을 없애기 위해 우선 이것부터 명확히 정의하고 넘어가자.
첫번째로 map_meta_cap은 register_custom_post() 함수의 옵션 인자로 쓰인다. 자명하다.
두번째로 map_meta_cap은 함수 이름으로 쓰인다. map_meta_cap() 처럼 뒤에 괄호를 붙여 구분하기도 한다. 이후 메타 권한 설명에서 설명하겠지만, map_meta_cap() 함수는 권한 판별에 매우 중요한 역할을 하는 함수이다.
셋째로 map_meta_cap은 필터 이름으로 쓰인다. map_meta_cap() 함수 내부에서 가장 마지막에 함수의 연산 결과를 재정의할 때 사용된다.
이 셋이 치명적인 오해를 불러일으키는 것은 아니지만, 코어 코드를 보면, map_meta_cap이 콜백 함수로 불리기 때문에 마치 이것이 필터처럼 보이기도 해서 처음 이 쪽 코드를 볼 때 오해하고 살짝 헤맨 경험이 있어 혹시나 해서 언급한다.
// class-wp-user.php, has_cap() function
$caps = call_user_func_array( 'map_meta_cap', $args );
코덱스에서 언급되는 이해하기 쉽지 않은 개념 중에 기본 권한(primitive capabilities)와 메타 권한(meta capabilities)가 있다. 말장난 같기도 하고 좀 어렵지만, 이해해 보면 워드프레스가 권한에 매우 세심하게 디자인을 했다는 것을 알 수 있다.
메타 권한이란 고정되어 있지 않은 권한들을 의미한다. 메타 권한은 조건에 때라 각각 다른 기본 권한으로 대응될 수 있다. 쉬운 예를 들어 설멍해 보자. 에디터 A, B 둘이 있다. 모든 포스트에 대해 A, B 동일한 권한이 있다. 그러나 어느 특정 카테고리에서만 A는 편집을 허용하고, B는 허용하지 않게 해야 한다고 하자. 이렇게 권한을 설정하게 하려면 어떻게 해야 할까? 이렇게 특정한 조건에 따라 유동적으로 설정 가능한 권한이 메타 권한이다.
반면 기본 권한은 고정된 권한이다. 메타 권한은 조건에 따라 기본 권한 하나로 대응된다. 일반적으로는 어떤 개체 하나에 대한 권한은 메타 권한, 복수의 개체에 대한 권한은 기본 권한으로 설정되어 있다.
메타 권한 동작의 예
메타 권한 중의 하나인 ‘edit_post’ 권한을 예로 들어, 이 권한의 코어 내부에서 어떻게 동작하는지 간략히 설명해 본다.
사용자가 포스트의 편집 화면을 접근한다고 하자. URL은 wp-admin/post.php?post=<post_id>&action=edit&…. 와 비슷하게 구성된다. wp-admin/post.php 스크립트는 편집 화면을 구성하는데, 먼저 사용자에게 포스트의 편집 권한이 있는지를 확인한다.
// wp-admin/post.php, Line 116
if ( ! current_user_can( 'edit_post', $post_id ) )
wp_die( __( 'You are not allowed to edit this item.' ) );
현재 사용자가 $post_id에 대해 ‘edit_post’ 권한을 가지고 있는지 물어보고 아니면 wp_die()에 의해 중단된다. current_user_can() 함수는 현재 사용자를 파악하고, 현재 사용자에 대해 class-wp-user.php의 WP_User::has_cap() 함수를 호출한다.
이어 WP_User::has_cap() 함수는 내부에서 map_meta_cap() 함수를 호출한다.
map_meta_cap() 내부에는 긴 switch ~ case 문이 있다. 여기서 case ‘edit_post’ 부분을 타고 들어가 보자. 해당 포스트의 작성자와 포스트의 상태에 따라 메타 권한의 결과가 변한다. if 분기를 세심하게 체크해 보면 어떤 경우에는 ‘edit_published_posts’, 또 어떤 경우에는 ‘edit_posts’나 ‘edit_others_posts’, 또 어떤 경우에는 ‘edit_private_posts’ 같은 기본 권한들로 대응되는 것을 확인할 수 있다.
// If the post author is set and the user is the author...
if ( $post->post_author && $user_id == $post->post_author ) {
// If the post is published or scheduled...
if ( in_array( $post->post_status, array( 'publish', 'future' ), true ) ) {
$caps[] = $post_type->cap->edit_published_posts;
} ...
} else {
// The user is trying to edit someone else's post.
$caps[] = $post_type->cap->edit_others_posts;
...
}
map_meta_cap() 함수는 $caps라는 배열을 리턴한다. 이 배열은 해당 문맥에서 사용자에게 요구하는 필수 요구 권한의 목록이다. 이 예에서는 $caps 배열의 길이가 아마도 1이겠지만, 조건에 따라 길이가 더 길어질 수도 있다.
$capabilities = apply_filters( 'user_has_cap', $this->allcaps, $caps, $args, $this );
// Everyone is allowed to exist.
$capabilities['exist'] = true;
// Must have ALL requested caps.
foreach ( (array) $caps as $cap ) {
if ( empty( $capabilities[ $cap ] ) )
return false;
}
위 코드는 WP_User::has_cap()의 마지막 부분이다. 사용자의 모든 권한을 가져와서 필수 요청 권한 목록인 $caps와 대조하는 코드이다. 전체 권한 중에서 요구된 권한 하나라도 발견되지 않는다면, 해당 사용자는 권한이 없는 것으로 간주된다.
각 옵션 설명
권한은 그 수가 많고 만큼, 쉽게 사용할 수 있어야 한다. 쉽게 쓰려고 의도했으면 쉽게 사용할 수 있도록 되어야 한다. 권한에 대해 고민하지 않고 그냥 포스트나 페이지처럼 만드려면 권한 관련 옵션을 아예 언급조차 하지 않으면 된다. 그러면 포스트나 페이지에 의거해 동일한 권한으로 자동 생성된다.
capapbility 옵션
권한 설정에 관련된 함수는 wp-includes/posts.php get_post_type_capabilities()이다. 소스 코드를 첨부한다.
function get_post_type_capabilities( $args ) {
if ( ! is_array( $args->capability_type ) )
$args->capability_type = array( $args->capability_type, $args->capability_type . 's' );
// Singular base for meta capabilities, plural base for primitive capabilities.
list( $singular_base, $plural_base ) = $args->capability_type;
$default_capabilities = array(
// Meta capabilities
'edit_post' => 'edit_' . $singular_base,
'read_post' => 'read_' . $singular_base,
'delete_post' => 'delete_' . $singular_base,
// Primitive capabilities used outside of map_meta_cap():
'edit_posts' => 'edit_' . $plural_base,
'edit_others_posts' => 'edit_others_' . $plural_base,
'publish_posts' => 'publish_' . $plural_base,
'read_private_posts' => 'read_private_' . $plural_base,
);
// Primitive capabilities used within map_meta_cap():
if ( $args->map_meta_cap ) {
$default_capabilities_for_mapping = array(
'read' => 'read',
'delete_posts' => 'delete_' . $plural_base,
'delete_private_posts' => 'delete_private_' . $plural_base,
'delete_published_posts' => 'delete_published_' . $plural_base,
'delete_others_posts' => 'delete_others_' . $plural_base,
'edit_private_posts' => 'edit_private_' . $plural_base,
'edit_published_posts' => 'edit_published_' . $plural_base,
);
$default_capabilities = array_merge( $default_capabilities, $default_capabilities_for_mapping );
}
$capabilities = array_merge( $default_capabilities, $args->capabilities );
// Post creation capability simply maps to edit_posts by default:
if ( ! isset( $capabilities['create_posts'] ) )
$capabilities['create_posts'] = $capabilities['edit_posts'];
// Remember meta capabilities for future reference.
if ( $args->map_meta_cap )
_post_type_meta_capabilities( $capabilities );
return (object) $capabilities;
}
capability 옵션은 문자열, 혹은 배열을 값으로 쓸 수 있다. 문자열은 단수형 단어(내부에서 문자 뒤에 s를 붙여 크기 2짜리 배열로 조정한다), 그리고 배열은 크기 2짜리로 단수, 복수로 권한에 대한 단어를 입력해 주면 된다. (소스코드 2~6라인 참고)
코어는 capability 옵션에 대해 다음 권한을 기본적으로 생성한다.
edit_post
read_post
delete_post
위 세 권한은 메타 권한이다. (소스코드 9~12라인)
edit_posts
edit_others_posts
publish_posts
read_privte_posts
이 네 권한은 기본 권한으로, map_meta_caps() 함수 내부에서는 언급되는 일이 없다. 그러나 코어 구석구석에서 권한 검사에 사용되고 있을 것이다. (소스코드 13~17라인)
만일 map_meta_caps 옵션을 적지 않거나(null), true로 설정했다면 7개의 기본 권한이 별도로 생성된다. 다음은 그 목록이다. (소스코드 20~32라인)
$args->capabilities가 capabilities 옵션이다. 여기서는 권한의 종류를 더욱 자세하게 정의할 수 있다. 보다시피 capability 옵션으로 인해 생긴 기본 설정에 더불어 별도의 권한을 추가할 수도 있고, 기본 설정을 변경할 수도 있게 짜여 있다.
map_meta_cap
map_meta_cap 옵션 값은 true/false, 그리고 기본으로는 null을 가질 수 있다. null로 값을 주면 내부에서 true로 변환시킨다. 즉 기본값이 true.
간단히 말해 이 옵션은 해당하는 커스텀 타입이 선언한 메타 권한이 기본 권한으로 대응시키는 작업을 허용할지 말지를 결정한다. 이 작업은 map_meta_cap() 함수에 정의되어 있다. 만일 false라면 일부 기본 권한은 수동으로 생성해야 하며, 메타 권한의 기본 권한 대응 작업은 별도로 제작해야 한다.
보다 세밀한 설명은 이렇다. 이 옵션은 다음과 같은 역할을 한다.
첫째로 get_post_type_capabilities() 함수 내부에서 true(혹은 null)일 때 일곱가지 기본 권한(read, delete_posts, 등…)을 설정해주는 역할을 맡는다. 만일 이 값이 false라면 이 일곱가지 기본 권한을 map_meta_cap 필터를 이용해 별도로 보충해 줘야 한다.
둘째로 이 값이 true(혹은 null)이어야 map_meta_cap() 함수 내부에서 post, page와 관련된 메타 권한 체크 로직에서 포스트와 페이지의 여러 상태에 따라 적절한 기본 권한으로 해석되도록 하는 코어의 기본 로직이 동작한다.
map_meta_cap() 함수의 메타 권한 체크 부분 중 포스트나 페이지와 관련된 곳을 보면 다음과 비슷한 코드가 있는 것을 볼 수 있다.
if ( ! $post_type->map_meta_cap ) {
$caps[] = $post_type->cap->$cap;
// Prior to 3.1 we would re-call map_meta_cap here.
if ( 'edit_post' == $cap )
$cap = $post_type->cap->$cap;
break;
}
map_meta_cap 옵션 값이 false가 되면 map_meta_caps() 함수 내에서 메타 권한은 더이상 메타 권한으로써 동작하지 않는다. 즉, 해당 capability를 액면 그대로 요구한다. 그러면 이렇게 반환된 capability에 대해서는 별도의 필터 콜백으로 검사해 주어야 한다.
셋째로 메타 권한인 read_post, delet_post, edit_post, 이 셋의 권한 이름을 변경했을 때 추적할 수 있도록 한다. _post_type_meta_capabilities() 함수와 map_meta_cap() 함수 switch-case 구문의 default 레이블에 구현되어 있다.
예제 코드 #1 특정 사용자의 특정 카테고리 편집 제한
메타 권한에 대한 설명에서 에디터 B에 대해 특정 카테고리의 글은 편집 제한을 거는 실제 코드를 구현해 보자.
에디터 B의 ID는 ‘5’이고, 카테고리의 택소노미 이름은 ‘wphack_taxonomy”이고, 카테고리 이름은 ‘a-only’이다. 포스트를 적당히 만들고 a-only 카테고리로 설정하고, 다음 코드를 작성한다. 접근 제한에 대한 세련된 예는 아니지만 권한에 대한 하나의 예제로써 이해하기를 바란다.
물론 다른 웹프레임워크를 이용해 UI를 다 만들 수도 있겠지만, 워드프레스를 사용하면 나름 괜찮은 UI를 손쉽게 가져다 쓸 수 있다. 이 UI를 통해 개체 수정 및 임의 작성도 가능하니 금상첨화다.
다만, 어떤 다른 후단부(backend)가 존재하여 포스트의 데이터를 모두 일괄적으로 생성하는 시나리오가 있다고 가정하자. 이 커스텀 포스트는 워드프레스 UI 상에서 읽기, 조회만 가능하고 포스트의 생성과 수정은 별도의 스크립트에서 담당해야 한다. 단, 삭제의 경우는 UI 상에서 데이터를 체크하고 수동으로 지우는 일이 가능하다고 하자.
이러한 시나리오에서 커스텀 포스트는 포스트의 목록과 상세 화면 UI가 필요하다. 그러나 커스텀 포스트의 상세 화면에서는 포스트의 어떤 필드도 수정할 수 없도록 처리해야 한다.
먼저 커스텀 포스트 타입을 생성해 보자. ‘init’ 훅에서 register_post_type의 인자를 설정해 준다. 여기서 레이블을 바꿔치기했다. ‘edit’은 이제 편집이 아니라 세부항목을 보는 것으로 의미가 변경된다. 주석 부분을 해제하면 삭제도 불가능하게 만들 수 있다.
화면 상단의 스크린 옵션을 제거해 보았다. 메타 박스 등을 고려한다면 나타나게 할 수도 있을 것이다. 또한 예제에서는 어떤 메타박스도 쓰지 않기 때문에 화면을 1열로만 쓰는 것이 더 깔끔하기 때문에 화면 레이아웃도 1단으로 고정시켰다. 포스트 타입에 따라 적절히 조절할 수 있을 것이다.
코드 전문은 여기서 확인할 수 있다. 이 플러그인을 활성화시키면 다음과 같은 화면을 볼 수 있다.
포스트 목록에서 ‘Edit’ 텍스트가 ‘Detail’ 텍스트로 변경되었다. 커스텀 포스트의 목록은 플러그인을 활성화하면 자동으로 생성되고, 비활성화하면 자동으로 삭제된다.Edit 화면의 레이블이 변경되었다. 어떤 입력 위젯도 출력되지 않으며, 스크린 옵션도 삭제되었다. 커스텀 포스트에 예제로 만든 2개의 커스텀 필드만 표로 출력되게 조작되었다.편집 권한이 없으므로 대량 편집 기능도 삭제하는 것이 맞다.
커스텀 포스트의 옵션 값과는 약간 거리가 있지만, 우선 워드프레스가 어떻게 다시 쓰기를 하는지에 대해 이야기를 먼저 하고자 한다. 옵션이 워낙 적어 분량 채우기기도 하지만… 워드프레스의 다시 쓰기 원리를 이해하고 이 옵션을 보는 것이 더 낫지 않을까?
웹사이트를 돌아다니다 보면 URL이 엄청나게 긴 것들을 볼 수 있다. 그에 비하면 엄청 양반이지만, 워드프레스에서도 이렇게 긴 URL로 쿼리를 할 수 있다.
/index.php?post_type=post&paged=1
위 URL예는 블로그 포스트 1페이지를 불러 오는 예이다. 파라미터가 단 두개 뿐이라 부담은 없지만, 이런 URL은 그다지 기억하기 쉽지도 않고, 예쁘지도 않다. 또 SEO (Search Engine Optimization) 관점에서도 이렇게 장황한 URL은 권장할 만한 사안은 아니다.
깔끔한 URL을 사용하게 되면 URL 자체로도 보다 의미 있어진다. 하지만 어떻게 하면 깔끔하고 좋은 URL을 만드는지 그 요령에 대해서는 논외로 하고, 어떻게 이렇게 URL을 쓸 수 있는지에 대해서만 쓰겠다.
웹서버가 서비스를 하는 가장 기본적인 방법은 파일 기반이다. 그림 파일, 자바스크립트 등등, 또 정적인 HTML 파일들이 그렇다. 그냥 있는 그대로 클라이언트들에게 전달하면 되는 구조다.
그러나 서버 사이트 스크립트 언어인 PHP로 짜여신 파일의 경우는 조금 다르다. PHP 파일 스크립트를 (소스) 그대로 클라이언트에게 전달하는 일은 없다. PHP 스크립트 또한 파일 기반이라는 점은 변함이 없지만, 그 파일에 접근할 때 어떠한 파라미터를 주는지에 따라, 또는 GET인지 POST인지 전달 방식에 따라 결과는 달라질 수 있다.
워드프레스에서도 마찬가지다. 단지 워드프레스 코어가 index.php 이외의 주소로 접근하는 것에 대해 잘 대비했을 뿐이다. 워드프레스의 index.php 외 여러 php 파일을 대상으로 웹브라우저에서 접근해 보라. 그저 빈 스크린만 나올 뿐이다. 물론 로그인이나 관리자 같은 몇몇 특별한 접근 포인트들은 예외지만.
그러면 워드프레스에서는 어떻게 깔끔한 URL을 처리하도록 만드는 것일까? 우선 웹서버에서 rewrite를 지원해야 한다. Apache나 nginx 둘 다 rewrite를 지원할 수 있으므로 이 기능에 의지해 URL을 찾아내는 것이다. 단, rewrite 기능에 모든 것을 의지하지는 않는다. 개략적인 룰만 지정하고, 나머지 복잡한 규칙은 내부의 DB에 저장한 다음 동적으로 처리한다.
기본적인 컨셉은 이렇다.
.htaccess 파일에 rewrite 룰을 다음과 같이 설정해 둔다: URL의 경로를 모두 무시하고 무조건 /index.php 파일에서 요청을 처리하도록 서버 흐름을 조작한다. 어차피 클라이언트가 어떤 경로를 요청했는지는 PHP의 $_SERVER 변수에 다 기록되어 있다.
$_SERVER[‘REQUEST_URI’]에서 읽어온 URL과 데이터베이스에 저장된 rewrite 규칙을 대조한다. 여기서 규칙에 맞는 것을 찾는다면, 안 예쁜 URL로 변경한다. 즉, ‘index.php?param1=val1¶m2=val2…’ 식으로 URL이 변경된다.
파라미터에 맞게 데이터베이스 쿼리를 한다.
템플릿을 불러와 적절히 응답을 한다.
워드프레스는 아파치를 서버로 사용한 경우에 내부에 .htaccess 파일을 생성한다. 생성하지 못하면 .htaccess 파일을 따로 업로드하라고 안내를 한다. 코덱스에서 기술하는 기본적인 .htaccess 파일 구조는 이렇다.
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
‘<IfModule> … </IfMoudle>’은 아파치 서버의 rewrite 모듈이 동작하는 경우에만 그 안의 내용을 읽으라는 뜻이다.
‘RewriteEngine On’은 다시 쓰기를 작동한다는 뜻이다.
‘RewriteBase /’은 다시 쓰기를 ‘/’를 대상으로 이뤄 진다는 것이다.
첫번째 RewriteRule은 URL이 그냥 index.php로 시작하고, 또 그대로 끝난다면, 이 주소는 더 건드리지 않고 놔 두라는 뜻이다.
두 번째 RewriteRule 위에 두 개의 RewriteCond가 있다. 두번째 RewriteRule의 선결조건을 의미한다.
%{REQUEST_FILENAME}는 서버 내에서 가져와야 할 파일의 이름을 의미하는 변수이다.
‘!-f’, ‘!-d’는 각각 ‘파일이 아닌 경우’와 ‘디렉토리가 아닌 경우’를 뜻한다.
그래서 %{REQUEST_FILENAME}이 파일도 아니고, 디렉토리도 아니라면 URL은 무시하고 index.php로 들어와 처리하라는 뜻으로 해석된다.
예를 들어 ‘<home_url>/category/foods’라는 깔끔한 URL을 웹브라우저에서 요청했다. 사실 이런 경로는 서버에 존재하지 않는다. 그러므로 아파치 웹서버는 그냥 index.php 파일로 접속하도록 처리를 한다.
그리고 wp() 함수를 호출하고, WP::main()에서 WP::parse_request() 함수를 처리하는 동안 데이터베이스에 있는 다시 쓰기 규칙을 읽어와 ‘category/foods’라는 경로는 원래 어떤 index.php?쿼리 문자열이었는지를 조사한다.
아마 워드레스에서는 기본적으로 ‘category/food’를 ‘index.php?category_name=food’로 치환할 것이다. 그러면 예로 제시한 URL은 사실 ‘index.php?category_name=food’와 같으며 웹브라우저에서 이 URL로 요청을 해도 같은 응답이 오게 될 것이다.
이 다시 쓰기 규칙은 워드프레스 options 테이블의 ‘rewrite_rules’라는 이름으로 저장되어 있다. 규칙은 배열을 serialize 된 상태로 존재한다.
rewrite_rule들이 데이터베이스의 레코드로 저장되어 있다. 그 위 고유주소(permalinks) 메뉴의 옵션값도 저장되어 있는 것이 보인다.
또다른 예로, 가끔 워드프레스를 이사하거나 정비하는 과정에서 전면 페이지(frontend) 홈은 보이지만 네비게이션 메뉴에 있는 다른 페이지로 옮기면 404 에러가 발생하는 경우가 있다. 이 문제는 .htaccess에서 존재하지 않는 서버상의 경로를 무조건 index.php로 처리하라는 명령이 누락되어 있기 때문이다.
Nginx도 크게 다른 것이 없다. 아파치처럼 .htaccess를 활용하지는 않지만, 어떤 URL로 요청이 들어오든지간에 처리를 index.php에 맡긴다는 점은 동일하다.
permalink_epmask
사실 이 옵션을 별로 건드려야 할 이유는 거의 없다. register_post_type 함수 내부를 들여다 봐도 이 옵션은 단지 EP_PERMALINK라는 기본값을 채워 넣는 것 이외에 언급되는 일도 없고.
사실 epmask를 코어에 유효하도록 설정하려면 add_rewrite_endpoint() 함수를 써야만 한다. 또 이 옵션에 값을 집어넣는다고 해서 register_post_type이 별도로 이 함수를 쓰는 것도 아니다.
말단지점에 대한 부가 설명
말단지점(endpoint)란 http://example.com/service/path 처럼 웹서버의 어떤 경로라고 생각할 수 있는데 이것이 왜 생겼고, 어떻게 쓰여지는지는 여기에 간략히 설명되어 있다. 간단히 말해 서버의 어떤 단일 개체를 위한 엔드포인트에 별도의 패스를 덧붙일 수 있도록 한 것이다.
예를 들어 wphack 커스텀 포스트에 개체에 접근하기 위해서 ‘/wphack/<post-slug>’ 이라는 말단지점을 사용할 수 있다. 그런데 어떤 목적 때문에 포스트의 제목만, 정말 제목만 출력하는 URL이 필요하다고 가정해보자. 물론 URL을 별도로 꾸밀 수도 있지만, 이러한 URL이 포스트 타입이나 페이지 타입 전반에 걸쳐 필요로 하다면? 이러한 경우를 위해 도입된 것이 바로 endpoint api이다.
add_action( 'template_redirect', 'wphack_ep_title_redirect' );
function wphack_ep_title_redirect() {
global $wp_query;
if( !isset( $wp_query->query_vars['title'] ) || !is_singular() ) {
return;
}
global $post;
header( 'Content-TYpe: text/plain' );
echo $post->post_title;
exit;
}
이렇게 하면 wphack, 또는 어떤 포스트, 페이지 타입에 대해 /title/ 이라는 경로를를 추가로 입력하면 개체의 제목만 출력한다. add_rewrite_endpoint 함수에 EP_PERMALINK | EP_PAGES 마스크를 씌웠으므로 각각 포스트, 페이지에 적용되었다. 나머지 상수도 마찬가지이다. 예륻 들어 EP_DAY라는 마스크는 엔드포인트에 날짜가 사용된다면 그 엔드포인트에 추가 경로를 붙일 수 있도록 한다.
이렇게 코드를 짜면, 데이터베이스를 갱신해 주어 서버에서 인식되도록 처리해야 한다. 간단한 방법으로는 설정 (settings) > 고유주소 (permalinks)로 가서 저장 버튼을 한 번 눌러 주는 것이다. 내용을 변화시킬 필요는 없다. 이렇게 하면 options 테이블의 rewrite_rules 내용이 갱신된다.
그리고 이렇게 갱신된 내용은 ‘Debug Bar’와 ‘Debug Bar Rewrite rules’ 플러그인을 사용해 진단해 볼 수 있다.
두 플러그인을 받고 활성화 한다.로그인하면 어드민 바에 Debug 항목이 추가된 것이 보인다. 이 버튼을 누르면 그림과 같은 항목이 나온다. 좌측 메뉴에서 ‘Rewrite Rules’항목을 선택한다.title 부분만 골라 보았다. 이렇게 정규식 매칭 표현과 매칭된 규칙이 어떻게 변환될지 정의되어 있다.
rewrite
rewrite 옵션은 하나의 배열을 값으로 받으며, 이 배열은 아래의 키를 가진다. 이 값을 변경하면 반드시 새로 고침 레코드를 갱신해 주어야 한다.
slug
query_var 옵션이 질의 문자열에 쓰이는 반면, slug 옵션은 rewrite에 사용된다. 만약 slug의 값으로 ‘ask’라는 문자열을 사용했다면 커스텀 포스트 타입 wphack은 전단부에 다음과 같은 URL로 접근 가능해진다.
<site_url>/ask/ # 목록 (아카이브)
<site_url>/ask/page/PAGE_NUMBER # 페이지 번호로
<site_url>/ask/slug/ # 특정 개체만
with_front
true/false를 가질 수 있는다. Front라는 것은 고유주소 설정에서 태그 항목 이전에 고정으로 생성한 문자열을 말한다. 아래의 그림을 보면 이해가 빠를 것이다.
/with-front 부분이 front이다.
고유주소에 /with-front 문자열을 앞에 덧붙였는데, 이렇게 저장을 하면 워드프레스의 대부분의 기본적인 고유주소는 /with-front로 시작한다. 그러나 이 값을 false로 설정하면 현재 동록하는 커스텀 포스트만큼은 이 문자열을 허용하지 않는다.
slug가 ‘ask’로 설정되었고 front가 ‘/with-front’라면
with_front가 false 일 때: <site_url>/ask로 개채의 목록 접근
with_front가 true 일 때: <site_url>/with-front/ask로 개체의 목록 접근
feeds
포스트 타입을 만들면 이에 대한 피드도 간단하게 만들어진다. 피드는 atom, rss, rss2 , rdf 다 지원을 한다. 이 값을 true로 하면 주소 끝에 /feed, /rss, /rss2 만 붙이면 바로 해당 형식으로 콘텐츠를 지원한다.
/feed, /rss, /atom, /feed/rss, 같은 주소만 붙여주면 아주 쉽게 피드를 작성할 수 있다.
pages
목록에서 페이징을 가능하게 한다. 이 항목이 true로 되어 있어야 <site_url>/ask/page/PAGE_NUMBER 처럼 된 URL로 접근 시 제대로 페이징된 목록을 보여 주게 된다.
페이지 기능 자체를 이 옵션으로 켜고 끄는 것이 아니다. 단지 URL 규칙을 제대로 설정하는 것이다.
ep_mask
워드프레스 3.4 이후부터 permalink_epmask 옵션을 대체하는 옵션이다.
마치며
3.4에 오면서 permalink_epmask 옵션이 rewrite 옵션과 통합되면서, 사실상 이 포스트에서 다룬 옵션은 고작 하나였다. 그렇지만 rewrite는 워드프레스 코어 기능과 매우 밀접한 중요한 기능이라 생각해서 별도의 포스트로 따로 작성한 것이다.
쿼리 문자열은 코어에 있어 엄청나게 중요한 부분이다. 어떤 콘텐츠를 어떻게 보여줄지는 모두 쿼리 파라미터에 달려 있기 때문에 사실 이 쿼리 파라미터가 워드프레스를 움직이는 중추라고 할 수 있다. 쿼리 문자열을 직관적인 URL로 변환시켜 주는 옵션이 rewrite이며 이것은 서버의 다시쓰기 기능을 필요로 하지만, 복잡한 다시쓰기 규칙은 워드프레스 코어 자체에서 별도로 수행하고 있다.
내보내기 기능에 해당 커스텀 타입도 포함시키는 옵션이다. 이것이 true 이면 도구(tools) > 내보내기 (export) 메뉴에서 커스텀 타입도 포함된다.
내보내기를 하면 XML 형태로 사이트의 콘텐츠를 다운로드 받을 수 있다. 단, 주의해야 할 점이 있다. 이것은 콘텐츠를 내보내기 위한 용도이며 범위는 콘텐츠에 한해 진행된다.
백업을 하려면 데이터베이스를 sql 형태로 백업받고, FTP 등을 이용해 사이트 전체 파일을 받아 두는 것이 낫다. 사이트 전반에 걸쳐 저장된 옵션, 예를 들어 테마 설정 및 업로드된 첨부 파일의 원본까지 보관해 두어야 하기 때문이다.
description
이 커스텀 포스트 타입이 어떤 것인지 기술하기 위한 용도로 사용된다. 이 옵션 자체가 어딘가 직접적으로 사용되는 일은 없다.
hierarchical
True 이면 ‘페이지’ 타입처럼 처럼 상위/하위 개념이 존재하는 개체로 만들 수 있다. 반대로 false이면 ‘포스트’ 타입처럼 모든 포스트는 동등한 위치를 가진다.
True/false 값의 차이는 supports 옵션에서 ‘page-attributes’를 넣었을 때 확연이 난다. true일 때는 (parent) 옵션이 나타나고, false일 때에는 이것이 보이지 않는다.
또 true면 목록을 출력할 때 하위 개체는 상위 개체에 그룹이 지어져 나온다.
HackPost #6 (최상위)
HackPost #5 (부모: #6)
HackPost #4 (부모: #5)
HackPost #3 (최상위)
HackPost #1 (부모: #3)
HackPost #2 (부모: #3)
taxonomies
배열로 값을 채워 넣을 수 있다. 이 커스텀 포스트에 대한 분류 체계를 설정해 줄 수 있다. 그렇지만 이렇게 분류 체계를 연관지어 주려면 먼저 분류 체계를 미리 등록해야 한다. 만약 새 분류 체계를 사용하나면 별도로 register_taxonomy() 함수로 분류 체계를 등록해야 한다.
이 포스트는 커스텀 포스트의 관리 UI(admin UI)와 관련된 옵션을 다룬다. 목록은 아래와 같다.
public
show_ui
show_in_nav_menus
show_in_menu
show_in_admin_bar
menu_position
menu_icon
supports
register_meta_box_cb
public 옵션은 관리 UI 관련 옵션에도 관여하지만 가시성 관련 옵션에도 관계가 있다. 해당 포스트에서 확인할 수 있다. 전체 옵션은 이 포스트에서 확인할 수 있다.
public이 true이면 show_in_hav_menus, show_ui 옵션이 true. 반대로 false이면 모두 false로 설정된다.
show_ui
포스트 타입을 관리하는 기본 UI를 제공할지를 결정한다. 기본적인 포스트의 목록 확인, 생성, 편집 UI를 재활용할 수 있다. 이 UI는 다음과 같은 URL 패턴을 따라 제공된다.
<site_url>/wp-admin/edit.php?post_type=<post_type> // 목록
<site_url>/wp-admin/edit.php?post=<post_id>&action=edit // 편집
<site_url>/wp-admin/post-new.php?post_type=<post_type> // 추가
커스텀 포스트 목록 UI커스텀 포스트 편집 UI
이 패턴 대로 URL을 입력하는 경우 위 그림처럼 익숙한 UI를 만날 수 있다. 만일 show_ui가 false이면 아래 그림처럼 권한이 없다는 메시지를 만날 수 있다.
관리 UI에 접근 권한 없음
show_in_nav_menus
이 옵션은 기본값으로 public 옵션 값을 따라 간다. 이 값이 true이면 외모(appearance) > 메뉴(menus) 항목에서 커스텀 포스트 항목을 메뉴로 선택할 수 있게 된다.
show_in_menu
관리자 화면에 포스트 타입에 관한 메뉴를 출력할지 결정한다. show_ui는 반드시 true여야 한다.
이 옵션은 true/false, 혹은 문자열을 가질 수 있다.
true이면 관리자 화면에서 메뉴를 출력한다. 반대로 false면 관리자 화면에 메뉴는 출력하지 않지만 목록, 삽입, 편집의 URL을 알면 UI에 접근은 가능하다.
문자열은 이미 워드프레스에 등록된 메뉴 슬러그를 대상으로 한다. 예를 들어 ‘글’ 메뉴는 post.php이다. 워드프레스의 기본 메뉴 슬러그의 예를 들면:
글 (posts): edit.php
미디어 (media): upload.php
페이지 (pages): edit.php?post_type=page
댓글 (comments): edit-comments.php
외모 (appearance): themes.php
플러그인 (plugins): plugins.php
사용자 (users): users.php
도구 (tools): tools.php
설정 (settings): options-general.php
이렇게 메뉴 슬러그를 입력하면 그 메뉴의 하위 메뉴에 해당 개체의 관리 UI 항목이 삽입된다. 아래 그림은 플러그인 메뉴에 HackPosts 관리 메뉴를 삽입한 예이다.
show_in_admin_bar
어드민 바는 워드프레스에 로그인하면 생기는 화면 상단의 관리용 UI이다. 전단부(frontend) 및 관리자 화면 양측에 둘다 나오므로 보다 편리하게 관리 명령을 사용할 수 있다.
여기에 “+ 새로 추가 (+ New)” 버튼을 누르면 글, 페이지, 사용자를 쉽게 추가할 수 있다. 이 옵션이 true일 경우 해당 개체를 새로 추가할 수 있는 메뉴를 여기에 등록해 준다.
기본값은 show_ui 옵션값을 따라 간다.
menu_position
메뉴의 위치를 설정한다. 정수로 입력하는데, 기본값인 null로 입력하면 댓글 아래에 메뉴가 삽입된다. PHP는 타입 저글링이 일어나 이 값을 문자열 형태로 입력해도 될 것 같지만, 실제로 해 보면 진짜 “정수형”만 가능하다는 점에 유의한다. 정수형이 아니면 기본값과 같게 동작하게 된다.
사용자 정의 필드(커스텀 필드 또는 메타 필드)를 직접 수정할 수 있다. 키와 값이 그대로 출력되는 형태라 편집하기 직관적인 형태는 아니다. 주의할 점은, 직렬화된(serialized) 값은 이 목록에 포함되지 않는다. 예를 들어 아래 코드처럼 array를 serialize 시킨 것이라면 목록으로 출력되지 않는다. 개발하면서 가끔 착각할 수도 있으니 참고하도록 하자.
키로 hack_key_01, hack_key_02를 입력하고 값에는 일반 문자열과 직렬화된 PHP 배열을 주었다.hack_key_01은 문자열이라 UI에 표시된다. 그러나 직렬화된 값을 가진 hack_key_02는 키의 존재만 감지될 뿐 UI로 출력되지 않는다.
comments
토론(discussion)메타박스 새 개체 추가 및 편집 화면에서 나온다.
토론(discussion)관련 메타 박스를 출력하거나, 편집 화면에서 댓글(comments) 메타 박스를 출력해 댓글 관리를 할 수 있게 한다.
댓글(comments) 메타 박스. 편집 화면에서 나온다.
revisions
개정판(revisions) 목록을 조회할 수 있는 메타박스를 보여 준다. 개정판 목록은 함부로 수정할 수 없다. 혹시 너무 많은 개정판이 문제라면 별도의 워드프레스 최적화 플러그인을 설치해서 이를 제거해 주거나 wp-config.php 파일을 수정해 주어야 한다.
page-attributes
페이지의 속성을 지정해 주는 메타 박스를 출력한다.
상위(Parent) 항목은 커스텀 포스트의 다른 옵션인 ‘hierarchical’이 true인 경우에 출력된다. Order의 경우 테이블의 menu_order 값과 연결되는데, 이 값은 개체 목록 출력시 별도의 정렬 순서를 지정해 줄 수 있다. 예를 들어 다음과 같이 menu_order 값을 지정해 주었다.
포스트 목록의 기본 순서가 그대로 정렬되어 나온다. 전단부에서 아카이브 페이지로 접근해도 이 순서는 동일하다.
post-formats
포스트 포맷은 워드프레스 3.1에서부터 지원하는 테마 기능이다. 코덱스에서는 대략적인 가이드라인을 줄 뿐, 각 포맷을 처리하는 방식은 전면적으로 테마에게 달려 있다. 아래에 항목에 대한 간략한 설명을 추가했다.
추가정보 (aside): 올바른 번역인지는 잘 모르겠다. 페이스북과 같은 제목이 없이 콘텐츠만 나열하는 스타일로 사용된다.
이미지 (image): 이미지를 위한 포스트 스타일. 첫번째 img 태그의 이미지가 해당 이미지로 간주된다. 포스트 본문에 URL 하나만 있을 수도 있는데, 그러면 그 이미지를 사용하며 이미지의 title 태그는 포스트 제목으로부터 온다.
비디오 (video):비디오 혹은 비디오 재생 목록 스타일. 첫번째 video 태그나 object/embed 태그를 해당 비디오로 생락한다. 비디오와 마찬가지로 URL 하나만 있을 수도 있다.
인용 (quote): 인용에 관한 콘텐츠를 설정하는 경우이다. 이 때 제목은 인용의 출처나 인용구의 저자 정보를 담을 수 있다.
링크 (link): 다른 사이트로의 링크를 기록하는 스타일이다. 처음 <a href=”” 태그가 외부로의 링크를 의미한다. 이미지와 마찬가지로 포스트 본문에 URL 하나만 놓을 수 있는데 이 때 포스트 제목을 이 링크의 제목으로 설정한다.
갤러리 (gallery): 이미지의 모음 스타일. 포스트의 내용으로 갤러리 쇼트코드를 가지고 있고 이미지 첨부 파일을 가지고 있을 수 있다.
상태 (status): 트위터 스타일로 짧은 글을 작성하는 스타일이다.
오디오 (audio): 팟캐스트 같은 오디오 파일 및 재생 목록 스타일이다.
채팅 (chat): 채팅 목록 스타일.
예를 들어, twentysixteen 테마에서 포스트 포맷을 걸어 보면 이렇게 나온다.
표준 포맷
standard 포스트 포맷. 커서가 지나는 중 캡쳐하여 글자색이 다른 건 무시하자.
추가정보(aside) 포맷
aside 포스트 포맷. 제목이 작게 나오고 ‘Aside’ 포스트 포맷이라는 표시가 나온다. 표준 포맷에 대해 제목이 좀 더 작게 나온다.
상태 (status) 포맷
status 포스트 포맷. 제목 자체를 필요로 하지 않는다. 또 제목링크가 완전히 사라져 단일 포스트만 화면의 접근이 차단된다.
register_meta_box_cb
개체에 대한 세부적인 인터페이스를 위해 메타 박스를 추가할 수 있다. 포스트 타입을 설정하면서 메타 박스를 디자인하기 위한 콜백 함수를 값으로 입력하면 된다.
관리 UI 옵션은 커스텀 포스트의 여러 요소 중 관리자 화면에서 나타낼 유저 인터페이스와 관련 깊은 옵션들만 따로 모아 내가 임의로 그룹화한 것이다. 관리 화면 UI 관련 옵션인 만큼 워드프레스 관리자와 가장 밀접한 부분일 것이라 생각한다. 사용하는 목적에 따라 적절한 파라미터 값을 찾아 사용하기를 바란다.
언젠가는 한 번 잡아서 해 볼 포스팅이었는데, 시간이 없다는 핑계로 엄청나게 미루고 있었다. 그치만 다시 여유가 넘치는 생활로 돌아오니, 이제는 다루고 넘어가야겠다는 생각이 든다.
커스텀 포스트는 워드프레스를 자신의 취향에 맞게 활용하기 위한 핵심적인 기능 중 하나라고 할 수 있다. 이 지루한 포스팅을 더 지루하게 만들고 싶지는 않다. 커스텀 포스트가 왜 필요한지, 그것이 무엇인지에 대해서는 설명을 아끼도록 하겠다. 아무래도 개발적인 이야기만 늘어 놓을 작정이니, 이것에 대한 필요성은 이 포스팅일 읽는 거의 모든 이가 잘 이해하고 있으리라 생각한다.
늘 그렇듯 내가 커버하고자 하는 모든 내용은 register_post_type() 함수에 대한 코덱스에서 서술되어 있다. 또 커스텀 포스트는 pods 같은 플러그인에서도 손쉽게 확장할 수 있도록 되어 있다. 그렇지만 제아무리 뛰어난 플러그인이라 할지라도 아마 register_post_type에서 사용하는 수많은 옵션 값들을 생략한 채로 내놓는 플러그인은 없을 것이다.
그런데 이 내용이 워낙 많고 복잡하다보니 나도 잊을 때가 많기도 한데다 기능이 새로 추가되는 일도 있으니 그러한 기능을 공부하고자 하는 목적이 우선이다. 포스팅을 하면서 스크린샷도 많이 달 생각이고, 워드프레스 소스도 많이 달아 두려고 한다. 소스는 현재 사용 중인 4.5.2 기준이다.
포스트 타입 이름
포스트 타입 이름은 20자 내여야한다. 영대문자와 공백은 포함하면 안 된다. 또한 몇몇 이름은 이미 점유되어 있기 때문에 그 이름은 사용해서는 안 되며, 이미 사용중인 커스텀 포스트 타입 이름이 있을 수도 있으니 prefixing을 권장한다. register_post_type 함수의 첫번째 인자로 사용된다.
사실 코어는 모든 포스트 타입을 posts 테이블에 저장한다. 커스텀 포스트로 새로운 타입을 만들든, 아니면 내부의 기본 타입의 포스트를 사용하게 되든 포스트라면 모두 예외없이 posts 테이블에 저장된다.
한편 posts 테이블에는 post_type 이란 최대 길이 20자의 varchar 필드가 존재한다. 바로 이 필드가 모든 포스트 타입을 구분하는 데 사용된다. 포스트를 작성하면 그 레코드의 post_type 필드 값은 ‘post’일 것이고 커스텀 포스트를 저장했다면 그 커스텀 포스트의 타입 이름이 저장될 것이다.
코덱스에도 언급되어 있지만, 아래 포스트 이름은 포스트 타입 이름으로 쓸 수 없다.
post: 기본 포스트 타입으로 선점되어 있다.
page: 기본 페이지 타입으로 선점되어 있다.
attachment: 첨부 파일을 위한 포스트 타입으로 이미 사용된다.
revision: 글을 수정하고 저장하면 ‘개정판’의 개념으로 이전 작성글이 보관된다. 이전 개정판본의 저장 내역을 위한 포스트 타입으로 사용된다.
nav_menu_item: 네비게이션 메뉴 항목도 일종의 포스트로 저장된다.
다음은 내부에서 파라미터 등으로 곳곳에서 사용되기 때문에 사용해서는 안 되는 단어들이다.
action
author
order
theme
아마 내부 로직에서 해당 문자열이 곳곳에 사용되므로 잘못하면 오작동을 일으킬 우려가 있지 않을까 생각해 본다. order는 순서라는 뜻 말고 ‘주문’이라는 뜻도 있다. 우커머스 같은 상거래 플러그인과 직결되는데, 우커머스의 주문 포스트 타입도 그리하여 shop_order로 사용한다.
인자 목록
인자 목록은 PHP의 연관배열(associative array)로 구성되어 있다. 키의 구조는 워드프레스의 버전에 따라 추가 혹은 삭제될 수 있다. 이 인자의 수는 포스트 타입에 대한 섬세한 제어가 가능한 만큼, 그 수가 많고 또 몇몇 파라미터는 까다로워서 코덱스에서조차 제대로 설명되지 않은 것도 있다.
이렇게 많은 내용을 한 포스트에 담아 내는 것은 적절하다고 판단하지 않아, 몇 개의 카테고리로 나누어 별도의 포스트로 분할하여 작성해 보았다. 아래는 각 파라미터 마다 적절히 그룹을 짓고 각 하위 포스트를 링크한 것이다.
이 옵션션들은 개체를 워드프레스 전단부(frontend, 일반 사용자가 만나게 되는 사이트 외부 화면)에 노출시키는 방법과 관련이 있다.
참고로 아래 글들은 모두 고유 주소(permalink)를 기본(plain)으로 세팅하였을 때로 상정하고 작성하였다.
public
프리셋 같은 옵션이다. 이것 하나만 true/false로 정해줘도 관련된 다른 옵션은 일일이 지정하지 않아도 그 의미에 따라 자동으로 true/false로 지정된다.
true일 때 다음처럼 동작한다.
exclude_from_search: false
publicly_queryable: true
false일 때 다음처엄 동작한다.
exclude_from_search: true
publicly_queryable: false
이 옵션은 관리 UI에도 관여한다.
exclude_from_search
외부 화면에서 글을 검색할 때 키워드에 해당 포스트는 제외하도록 지정한다. 옵션 이름이 exclude이므로 포함의 반대.
검색 파라미터는 URL상에 /?s=<term> 식으로 입력된다. 다시 말해 파라미터 ‘s’가 커스텀 포스트도 포함하여 데이터베이스를 검색할지 말지를 결정하는 옵션이다.
publicly_queryable
이 포스트를 쿼리할 수 있는지 결정한다. false라면 전단부에서는 글을 조회할 수가 없다. 워드프레스는 아래 방식으로 전단부의 글을 쿼리한다.
?post_type={타입}: 개체 목록 쿼리
?{포스트타입_쿼리변수}={포스트_슬러그}: 단일 개체 쿼리
만일 wphack이라는 커스텀 포스트를 등록했으면 아래 예제와 같다.
?post_type=wphack
?wphack=wphack-6
query_var
쿼리 변수를 별도로 지정할 수 있다. true로 지정하면 $post_type으로 지정하고, false면 쿼리 변수를 쓰지 않는다. 또 문자열로 입력 가능한데, 이러면 별도의 쿼리 변수를 사용한다.
예를 들어 query_var 옵션의 값을 hp로 지정한 경우, 이제 단일 개체 목록을 부르기 위해 사용한 URL ?wphack=wphack-6은 이제 사용이 불가하고 이제는 URL을 이렇게 고쳐야 한다: ?hp=wphack-6
has_archive
이 옵션이 true/false이든 관계없이 개체 목록을 불러오는 URL은 post_type=wphack처럼 사용한다.
위 그림 좌측이 has_archives가 false, 우측이 has_archives 옵션이 true인 경우이다. “그래서 ‘Archives: ‘ 라는 텍스트가 추가된 것 말고는 도대체 뭐가 변한 건가?”라고 의문을 가질 수 있다.
사실 가장 중요한 변경점이 나타난 것이다. 바로 has_archives가 true로 설정되면서 테마의 템플릿이 변경된 것이다.
예로 쓰고 있는 워드프레스의 템플릿은 기본으로 설치되는 twentysixteen인데, 포스트의 기본 템플릿은 twentysixteen/index.php 파일을 이용한다. 아카이브의 기본 템플릿은 twentysixteen/archive.php을 이용하게 된다. 워드프레스 템플릿의 기본 룰이다.
만일 아카이브를 가지지 않는다(false)고 하면, 일반 블로그의 글 목록을 쓸 때 쓰는 가장 기본인 index.php를 템플릿으로 사용한다. 반면 아카이브를 가진다(true)고 하는 경우는 이제 archive.php를 템플릿으로 사용하는 것이다.
다만 index.php와 archive.php 파일을 직접 보면 구조가 거의 같다. 템플릿이 코드가 거의 동일해서 결과가 동일하게 눈에 보이는 것일 뿐, 사실 수면 아래에서는 전혀 다른 프로세스가 일어났던 것이다.
템플릿 룰 중에 또 이런 것이 있다. 특정 포스트 타입을 위한 아카이브 템플릿 파일을 별도로 지정하는 방법이 있다. ‘archive-{포스트 타입}.php’와 같이 이름을 짓는 방법이다. 예제로 드는 wphack 포스트 타입이라면 ‘archive-wphack.php’라고 이름짓고 별도의 템플릿을 디자인하면 이제는 이 포스트 타입만을 위한 아카이브 페이지가 출력될 것이다. 만일 특정 포스트 타입이 별도의 목록 페이지 스타일을 가지게 만드려면 이 옵션을 반드시 true로 설정해야 할 것이다.
한편 아카이브를 가지게 되면 커스텀 포스트의 목록을 메뉴에 삽입할 수 있다. 관리자 메뉴 외모(appearance) > 메뉴(menus)로 접근해 보자. 커스텀 포스트를 위한 만들 때 단일 개체 뿐 아니라 개체 목록 전부를 출력하는 링크를 선택해 메뉴에 삽입할 수 있다.
아래 그림처럼 메뉴로 나온다. 만약 이렇게 메뉴를 삽입하고 난 다음에 has_archives 옵션을 true에서 false로 변경해버리면 링크가 동작하지 않는다.
정리: 옵션 조합
그러니까 어떤 옵션을 쓰면 어떤 상황에서는 개체가 나올 수도, 반대로 나오지 않을 수도 있다. 매우 세부적으로 나눠져 있어 오히려 혼란스럽기도 하다.
사실 전면 화면에서의 가시성은 public 옵션을 쓰는 것이 이상적이고 편하기도 하다. 그렇지만 만일 이런저런 세부 옵션을 조합하면 어떤 결과가 나올까? public을 true/false로 정했을 때 지정되는 방법 이외의 케이스를 한 번 따져 보자.
exclude_from_search: true, publicly_queryable: true
검색에서 제외되지만 쿼리는 가능한 경우이다. 글 목록은 전면에서 조회하지만, 내용을 검색할 수 없어 어색해진다.
exclude_from_search: false, publicly_queryable: false
검색은 되지만 전면에서 글을 볼 수가 없다. 검색은 되지만 본문을 보려 해도 접근이 안 된다. 이 역시 어색하다.
또 publicly_queryable이 false면 query_var는 아무 의미가 없다. 한편 query_var는 true, false, 그리고 어떤 문자열도 가능하다. publicly_queryable이 true로 고정되었다고 하면, query_var의 조합에 따라 다음과 같은 결과가 나올 수 있다.
query_var를 true로 설정하는 것은 가장 자연스러운 옵션이다. ?{query_var}={post_slug} 과 같은 URL로 접근된다. 또 query_var를 어떤 문자열로 쓰는 것도 자명한 결과이다.
그런데 query_var를 false로 쓰는 것은 어떤 결과를 불러 올까? 우선 URL post_type=wphack으로 개체 목록을 접근할 수 있지만, wphack=post_slug으로는 접근 불가능하다.
그렇지만 이것이 각 개체에 대한 접근을 제어하는 것은 아니다. 파라미터의 조합에 따라 query_var 없이도 얼마든지 개체에 접근할 방법이 있기 때문이다. 단지 ?wphack=hackpost-6 처럼 각개 개체에 대한 단일 화면에 슬러그(포스트 이름, post_name)를 이용해 접근하는 것이 불가능해 질 뿐임을 의미하는 것이다. 워드프레스는 query_var가 false가 되면 각기 단일 개체 접근하는 URL을 ‘p’ 파라미터를 이용해 구성한다. p는 포스트의 고유 ID를 받을 수 있다.
예를 들어 ‘HackPost #5’라는 제목을 가진 wphack 개체가 있다고 보자. 이 개체의 슬러그는 ‘hackpost-5’이고 ID는 10이라고 한다. 그러면 query_var에 관계 없이 다음 URL로 접근 가능하다.
site_url/?post_type=wphack&p=10 (단일 개체)
site_url/?post_type=wphack (개체 목록)
site_url/?p=10 (단일 개체)
이것은 고유주소(permalinks) 옵션을 기본(plain)에서 다른 옵션으로 변경한다 하더라도 지장이 없다. 단지 웹브라우저에서 예쁜 URL로 표시되는 것일 뿐, 서버에서는 이 예쁜 URL을 규칙에 따라 분석해서 index.php?post_type=…&p=…&s=… 와 같이 내부적으로 변경하기 때문에 URL을 어떤 스타일로 넣든 결과는 사실상 동일하다.
커스텀 포스트 레이블은 꽤 많은 경우 사소한 문제로 넘어가게 된다. 보통 커스텀 포스트를 워드프레스 기본 포스트 타입인 ‘포스트’와 유사하게 별도의 작성자가 꾸준히 어떤 콘텐츠를 작성(주로 수동으로)해 발행하기 위한 용도로는 잘 사용해 본 적이 없기 때문이다.
그러다 보니 레이블에 대한 인식은 많이 낮았다. 또 레이블에 별로 신경 쓰지 않아도 코어가 ‘post’나 ‘page’ 타입에 의거해 기본적인 레이블을 만들어 주니 신경 쓸 것이 거의 없었기도 했고.
register_post_type 레퍼런스를 보면 꽤나 많은 입력 인자가 있는데, 이 중에서도 상당수를 차지하는 것이 레이블이다. 사소하기는 하나 이 레이블들이 어디에서 어떻게 쓰이고 있는지 꼭 한번은 체크해 보고 싶었다. 물론 이것은 로직과는 별개로 그저 ‘텍스트’의 취향일 뿐이라고 할 수도 있다. 그리고 대략의 문서를 보면 어떤 텍스트를 넣어야 하는 건지도 바로 이해할 수도 있다.
그러나, 궁금했다. 이렇게 열심히 세부 옵션을 넣어 텍스트를 지정해 주면 도대체 UI 어디에서 이 레이블을 활용하고 있는 건지, 이 레이블이 실제로 어디서 어떻게 쓰이는지 조목조목 따져 보고 싶었다. 그렇게 알게 되면 각 레이블에 대한 이해도도 높아지지 않을까?
원래 커스텀 포스트에 대해 포스팅을 하면서 한꺼번에 처리하려고 했지만, 의외로 레이블의 세부 옵션이 많아서 이것만 따로 포스팅하는 것이 낫겠다 싶어 별도로 작성한다.
자, 그럼 지루하기는 하지만 커스텀 포스트의 레이블이 어떤 식으로 사용되나 보도록 하자.
레이블과 관련된 옵션
여기서 레이블(label)이란 것은 UI 상에 표시되는 텍스트를 말한다. 다들 알다시피 워드프레스는 다국어 설정이 가능하다. 그러므로 각 언어 설정에 따라 표시되는 텍스트는 달라질 수 있다. 그래서 어떤 UI 상에 어떤 텍스트를 표시하려면 두 가지 조건이 필요하다. 바로 언어 도메인과 문맥이다. 언어 도메인은 워드프레스가 어떤 언어로 표시될 지를 지시하고 문맥은 어떤 상황의 텍스트를 출력해야 하는지를 지시한다.
레이블과 관련된 커스텀 포스트의 옵션은 크게 두 가지가 있다. label과, labels이다. 사실 두 옵션을 다 사용할 필요는 없고, 커스텀 포스트를 만들 때 둘 중 하나만 써도 된다. label은 아주 심플하게 레이블을 만들고 싶을 때 (다시 말해 레이블에 그다지 신경을 쓰지 않아도 좋을 때), 반면 labels는 각 레이블에 대해 세부적인 문맥에 맞춰 텍스트를 지정해 줄 수 있다.
label 옵션
가장 기본으로 사용할 수 있는 것이 label 옵션이다. 아주 간단한 용도의 커스텀 포스트라면 이 옵션만 사용해도 큰 문제는 없다. 나머지 세세한 레이블 텍스트는 기본 타입인 ‘post’를 기준으로 적절히 메꿔지기 때문이다. 왠만큼 까다로운 사람이 아니라면 이 것 때문에 불편을 호소할 일은 거의 없을 것이다.
이 옵션만을 주고 ‘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로 이름이 만들어질 것이다.
한국어에서는 단수/복수 개념이 뚜렷하지 않긴 하지만, 어쨌든 옵션 이름이 의미하는 것처럼 단수로 지정해 주어야 한다.
참고로 Piklist라는 빠른 개발용 플러그인이 있다. 여기서 별도의 훅을 지정해 커스텀 포스트를 생성할 수 있도록 하는 기능을 제공한다. register_post_type 함수를 래핑하여 보다 편하게 쓸 수 있게 만든 것인데, 여기서는 name 인자를 하나만 넣어 줘도 영어 단수/복수 규칙을 추측해 singular_name, plural name을 지정하는 기능을 갖추고 있다. 왠지 영어권 사용자에게는 나름 유용해 보인다.
add_new 레이블 옵션
메뉴에 나오는 ‘Add New’ 텍스트를 변경할 수 있다. 이 텍스트는 명백하게 두 곳에서 사용되고 있다.
어드민 메뉴의 메뉴 항목과 페이지 h1 문단 제목 우측의 버튼의 텍스트로 사용된다.
add_new_item, edit_item 레이블 옵션
add_new_item은 새 개체 작성 페이지의 h1 문단 제목으로 사용된다. ‘_item’ 접미의 item은 해당 단수 개체 이름이라고 생각하면 된다. 마찬가지로 edit_item은 개체 편집 페이지의 h1 문단 제목으로 사용된다. 아래 그림은 이 옵션을 지정하지 않은 것과 한 것을 비교한 것이다.
new_item 레이블 옵션
UI 전반에서 사용되는 레이블은 아닌 것으로 보인다. 코드의 완전성을 위해 넣은 듯하다.
view_item 레이블 옵션
개체 편집 화면의 어드민 바에는 프론트 화면에서 작성된 개체가 어떻게 보이는지 바로 확인하는 메뉴가 삽입된다. 이 옵션으로 그 텍스트를 변경할 수 있다.
또 참고로 이 레이블 텍스트는 댓글(Comments) 목록 화면의 ‘댓글이 달린 글(In Response To)’ 칼럼에서 사용된다. ‘글 보기(View Post)’ 링크 텍스트가 바로 그것이다. 물론 이것은 기본인 ‘post’ 타입에만 해당되겠지만.
search_items 레이블 옵션
개체 목록을 검색하기 위한 검색 버튼의 텍스트를 변경할 수 있다.
not_found, not_found_in_trash 레이블 옵션
not_found 레이블 옵션은 개체 검색 결과가 없는 경우에 이를 알리기 위한 텍스트를 위해 주어진다. 마찬가지로 not_found_trash 레이블 옵션은 휴지통에서 검색한 경우의 텍스트이다.
parent_item_colon 레이블 옵션
포스트 타입을 계층적(hierarchical)으로 설정한 경우 개체의 부모 개체를 설정해 줄 수 있다.
사실 이 레이블은 UI 상에서 찾아 보기가 어렵다. 이 텍스트가 명시적으로 보이는 한 예는 이렇다. 포스트 목록에서 부모 포스트가 삭제되거나 휴지통으로 이동하여 부모-자식 위계에 영향을 주는 사안이 발생한 경우 자식 포스트에서 부모 포스트가 어떤 것인지 표시해 줄 때 사용된다.
아래 그림처럼 부모 – 자식 관계가 있는 페이지 목록이 있다고 보자.
그리고 부모 페이지인 ‘Parent Page’를 휴지통으로 보내고 난 다음 목록을 다시 불러오면 아래처럼 화면이 변경된다.
텍스트 구조는 이렇다: | parent_item_colon <부모 개체의 이름>
한편 포스트 타입 구조가 아닌 카테고리에 해당하는 케이스지만 비슷한 사례라 기록을 남겨 본다. 아래 그림은 ‘post’ 편집 화면에서 카테고리 설정을 위한 메타박스 UI이다.
워드프레스에는 시각적으로 표시되지는 않는 여러 텍스트들이 HTML 코드 상에 존재한다. 시각장애인을 위해 존재하는 들리는 텍스트들이다. 그림의 붉은 상자로 표시된 쪽의 입력 상자를 위한 들리는 레이블이 존재한다. 이 레이블의 텍스트로 카테고리의 parent_item_colon 속성이 사용된다.
label for=”newcategory_parent” 태그를 보면 Parent Catecory: 라는 텍스트를 확인할 수 있다. 포스트 레이블이 아닌 카테고리 레이블의 용례이지만, 둘 다 엇비슷하므로 같이 남겨 본다.
all_items 레이블 옵션
‘모든 개체들’의 의미를 지닌 텍스트를 출력하기 위해 지정할 수 있다.
커스텀 포스트를 생성하면 나오는 하위 메뉴로 ‘모든 글’, ‘모든 페이지’ 같은 ‘모든 개체’ 항목이 자동으로 생성된다. 이 때 이 레이블이 사용된다.
한편 커스텀 포스트의 ‘show_in_menu’ 옵션은 불리언 혹은 문자열을 넣을 수 있다. 불리언의 경우는 메뉴에 보일 것인지 숨길 것인지 정하기 위해 사용한다. 그런데 인수로 문자열로도 입력할 수도 있는데, 이렇게 되면 해당 메뉴의 하위 메뉴에 커스텀 포스트 UI가 삽입된다.
예를 들어 ‘show_in_menu’의 값으로 ‘tools.php’ 문자열을 입력하면 HackPosts UI는 도구(Tools) 메뉴의 하위 메뉴로 이동하게 된다. 이 때에도 레이블의 텍스트로 all_items 레이블 옵션이 사용된다.
다른 예로 두 유저간에 개체 편집 충돌이 일어나는 경우에 출력된다. 가령 사용자 A가 편집 중인 개체를 사용자 B도 편집하기 위해 해당 URL로 접근한다고 생각해 보자. 그러면 사용자 B는 다음과 같은 메시지를 받음으로써 사용자 A가 편집 과정에 있음을 통지받는다.
이 때 사용자 B가 Take Over 단추를 눌려 편집권을 이양받게 되면, 편집 중이었던 사용자 A는 이런 메시지를 실시간으로 전달받게 된다. 이 때 all_items 레이블이 사용된다. (설명은 장황한데, 정말 너무 사소한 부분이다…)
archives 레이블 옵션
메뉴 편집 화면에서 해당 타입의 아카이브를 위한 레이블이다. 아래 그림을 보자.
‘All HackPosts Items’라고 나온 것은 archives 레이블 옵션이 정해지지 않았을 때이다. 이 값이 없다면 all_items레이블 옵션에서 가져오기 때문에 all_items 레이블이 출력된다.
archives 레이블을 적절히 조절하면 위 그림처럼 별도의 레이블을 가지게 된다.
insert_into_item 레이블 옵션
미디어 삽입 화면에서 개체에 미디어를 삽입시키는 버튼 텍스트로 사용된다.
uploaded_to_this_item 레이블 옵션
‘미디어 추가(Add Media)’ 버튼을 클릭하면 나오는 미디어 관리 창에 속한 미디어 필터의 한 레이블 텍스트. 아래 그림과 같은 위치에 있다.
이 아이템으로 업로드된 미디어란 의미는 로 연결되었다는 의미이다. 현재 편집 중인 화면에서 미디어 추가를 눌러 파일을 업로드하면, 해당 업로드 파일은 편집 중인 개체에 특정지어 업로드 되었다고 간주된다. DB에서는 attachment 포스트의 post_parent 필드가 현재 편집 중인 post ID로 대입된다는 의미로 볼 수 있다.
featured_image, set_featured_image, remove_featured_image 레이블 옵션
support 옵션에 ‘thumbnail’을 추가하면 커스텀 포스트도 특성 이미지를 지정할 수 있다. 특성 이미지라는 이름을 다른 것으로 변경하기 위해 featured_image를 사용한다. 이름으로 추측 가능하다. 다들 특성 이미지 관련 UI에서 레이블로 사용된다.
use_featured_image 레이블 옵션
이 레이블이 직접적으로 사용된 UI를 찾기가 쉽지 않다. 왜냐하면 이 레이블이 코어에서 직접적으로 쓰이는 곳은 단 한 곳, wp-admin/includes/media.php 파일의 get_media_item() 이라는 함수 뿐이다.
get_media_item 함수는 이미지 첨부 메타데이터를 수정할 수 있는 HTML 폼을 출력하는데, 워드프레스 관리 UI 상에서 일반적인 방법으로는 이 주소가 명시적으로 나타난 곳이 없다. 최근 버전의 워드프레스는 미디어 파일의 조회와 수정은 post.php에서 모두 처리하도록 되어 있다.
그렇지만 이 화면을 완전히 볼 수 없는 것은 아니다. 직접 주소를 쳐서 들어가면 된다. 다음과 같은 경로로 접근하면 된다.
action, attachment_id 두 파라미터는 정상적으로 화면이 출력되기 위해 필수적으로 있어야 하며, 특성 이미지 속성까지 출력시키려면 post_id 파라미터가 별도로 필요하다. 이 때 attachment_id는 attachement 포스트의 ID, post_id는 특성 이미지를 집어 넣고 싶은 포스트의 ID이다. 올바르게 입력하면 아래 그림과 같은 화면이 출력된다. 목록 마지막에 use_featured_image 레이블 옵션이 나오는 것을 확인할 수 있다.
menu_name 레이블 옵션
기본값이 name 옵션과 같은 이것은 관리자 메뉴의 레이블을 별도로 지정하고 싶을 때 사용된다.
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은 개체의 수가 많아 몇 개의 페이지로 나뉘어 질 때 페이지네이션을 위한 스크린 리더 텍스트로 사용된다.
어드민 바의 ‘+ 새로 추가 (+ New)’ 메뉴에 새 개체를 삽입하는 명령을 추가할 수 있다. 이 때의 레이블 옵션을 별도로 지정할 수 있다. 이 옵션이 없다면 singular_name 레이블 옵션을 가져와 사용한다.
마치며
어떤 레이블이 UI 어디에서 어떻게 쓰이는지에 대해 굉장히 궁금했었다. 물론 코덱스에 있는 대로 적당히 뉘앙스나 문맥만 파악한 후 개체에 맞게 말을 고쳐 주면 그만일 수도 있지만, 그 레이블이 나오는 정확한 문맥을 알 수 없었기에 과연 올바른 의미를 가진 레이블로 제공되는지 확실히 알 수가 없었다. 이번 기회로 각 레이블이 어떤 상황에서 어떻게 쓰이는지 명확히 파악해 보았다. 이젠 보다 자신있게 커스텀 포스트의 레이블을 다룰 수 있으리라 생각한다.
결제 관련 정보(Billing details)에서는 결제자의 이메일과 전화번호를 적는 란이 있다. 그런데 배송 정보(Shipping details)에는 그렇지 않다. 조금 전 생짜 워드프레스에 우커머스만 올려 놓고 테스트를 해 배송 정보에는 이메일과 전화번호 필드가 포함되지 않음을 확인했다.
간혹 우커머스의 결제와 배송이 서로 다른 경우에는 어떻게 하란 말인가? 한국에서는 배송을 할 때 배송 상자에 받는 사람의 전화번호도 같이 적게 되어 있다. 그리고 택배 기사님들이 보통 수신인이 부재중일 경우 이 전화로 전화를 걸기도 하고. 결국 한국 상황에 맞춰 일괄적으로 기능 확장을 하는 수 밖에 없지 않은가?