나는 뮐 위해 사는 건지?
Blog
-
혼잣말
검색엔진이 사랑하는 내 블로그. 아니 검색엔진만 사랑하는 내 블로그. ㅋㅋ.
-
혼잣말
나는 귀찮은 것이 싫다. 뭐든 대충대충 하는 것이 좋다.
-
ESNext #4: 블록 에디터 속성
블록 에디터는 post_content 필드에 직접 기록된다. 그리고 기본적으로 블록의 내용 또한 이 필드 안에 본문으로서 기록된다.
예를 들어, ESNext #2: 블록 제작에서 작성한 Hello, World! 블록은 다음처럼 텍스트로 기록된다.
<!-- wp:wp-esnext-study/wes02-hello-world --> <p class="wp-block-wp-esnext-study-wes02-hello-world">WP ES Next #2 안녕, 세상!</p> <!-- /wp:wp-esnext-study/wes02-hello-world -->
HTML 코드로 기록되는 것이 보인다. 그리고 주석으로 이것이 워드프레스 블록임을, 또 어떤 블록 타입인지를 기록하는 것이 보인다.
wp:
로 시작하고/wp:
로 닫는 것으로 보인다. ‘wp-esnext-study/wes02-hello-world
‘는 내가 지정한 블록 타입 이름이다.블록은 워낙 단순해서 어떤 상태나 속성을 가지지 않지만, 이제 간단한 속성을 지정해 보도록 하자. 이것을 활용하면 블록 에디터에 값을 저장하고, 불러와 쓸 수 있게 된다.
Github 리포지터리에 04-attributes 디렉토리로 예제 코드를 작성하였다. 아래부터는 이 예제 코드에 대한 설명을 덧붙인다.
속성값에 대한 설명은 블록 에디터 문서에서 찾을 수 있다. 이 개념은 마치 리액트의 콤포넌트의 state와 약간 비슷한 느낌이다. registerBlockType() 함수의 옵션 인자로 주어지며, 해당 블록 타입 내 속성값의 키-값 쌍들의 목록이다.
registerBlockType('foo/bar', { ... attributes: { name: { type: 'string', default: 'John', } } ... });
키는 해당 속성의 이름, 값은 속성의 성격을 정의한다.
‘type’은 필수적으로 지정해야 하며 다음 문자열 중 하나여야 한다.
- null
- boolean
- object
- array
- number
- string
- integer
‘source’는 이 속성값이 블록 내 어떤 곳에서 오는지 지정한다.
- 생략: source를 생략할 수 있다. 이 때 블록의 주석에 인코딩된다. 명시적으로 주는 방법도 있을 텐데 정확히 알 수가 없다.
- attribute: 속성에서 가져온다.
- html: HTML 코드를 취한다.
- text: 텍스트를 취한다.
- children: 내부 자식 요소에서 취한다.
- meta: 메타 값에서 온다.
- query: 속성 값에 여러 군데에서 값을 가져와야 할 경우 유용하다.
‘source’와 더불어 ‘selector’, ‘attribute’ 속성을 더할 수 있다. ‘selector’는 CSS 선택자로 내부 요소를 선택할 때 쓰고, ‘attribute’는 검색된 노드에서 어트리뷰트 값을 취할 때 쓴다.
{ ... attributes: { ingredients: { type: 'array', source: 'children', selector: '.ingredients', }, url: { type: 'string', source: 'attribute', selector: 'img', attribute: 'src', }, comment: { type: 'string', source: 'meta', meta: '_my_comment' } } ... } // attributes의 예제. 'ingredients' 클래스를 가진 자식 노드의 텍스트를 가져온다. // img 노드의 src 속성에서 url 속성값을 채운다. // comment 속성은 메타 필드 '_my_commnet' 로부터 온다.
예제에서 나는 두 개의 입력 필드를 삽입했다. 이 입력 필드를 통해 위젯의 내용을 동적으로 표현한다. 하나는 주석에 저장하고, 하나는 메타 필드로부터 가져온다.
{ ... attributes: { name: { type: 'string', default: 'John', }, anotherName: { type: 'string', source: 'meta', meta: '_another_name', default: 'Jane', } }, ... }
나머지 edit(), save() 메소드를 완성한다.
edit({setAttributes, attributes}) { return ( <> <p>{__('Hello, World!', 'wp-esnext-study')}, {attributes.name}, {attributes.anotherName}!</p> {/* 첫번째 입력 요소. Hello, World!, {이름}을 출력하게 합니다. 이 요소는 HTML 주석에 JSON 인코드 됩니다. */} <TextControl label={__('Your Name', 'wp-esnext-study')} value={attributes.name} onChange={value => { setAttributes({name: value}); }} /> <TextControl label={__('Another Name', 'wp-esnext-study')} value={attributes.anotherName} onChange={value => { setAttributes({anotherName: value}); }} /> </> ); }, save({attributes}) { return ( <> <p>{__('Hello, World!', 'wp-esnext-study')}, {attributes.name}, {attributes.anotherName}!</p> </> ); }
이렇게 하면, name은 본문 블록의 주석에 저장되고, anotherName은 메타 값으로 저장된다. 단, 메타 값을 REST API에 노출하기 위해 index.php에 register_meta() 함수를 호출해 _another_name 필드를 지정한다.
<?php ... register_meta( 'post', '_another_name', [ 'type' => 'string', 'description' => 'Another name.', 'default' => 'Jane', 'single' => true, 'sanitize_callback' => function ( $value ) { return sanitize_text_field( $value ); }, /** * apply_filters( "auth_{$object_type}_meta_{$meta_key}", $allowed, $meta_key, $object_id, $user_id, $cap, $caps ); * * @see map_meta_cap() */ 'auth_callback' => function ( $allowed, $meta_key, $object_id, $user_id ) { return user_can( $user_id, 'edit_post', $object_id ); }, 'show_in_rest' => true, ] );
편집기에서 값을 저장하고 저장된 상태를 확인해 본다. 값이 잘 저장되는 것을 확인했다.
편집기 화면, 이름으로 Simth, Ashley를 입력했다. 저장 후 프론트에서 본 화면. 해당 포스트의 포스트 메타 상황을 데이터베이스 테이블에서 검사. 단, 값이 잘 저장된 것처럼 보이지만 헛점이 하나 있다. 이 글을 코드 에디터로 봐서 본문을 날것 그대로 보면 아래 그림처럼 나온다.
메타 값으로 지정한 Ashly는 그저 텍스트로 출력되어 있다. 만약 다른 곳에서 메타 값을 Ashly에서 Simpson으로 변경한다 하더라도, 이 텍스트는 변경되지 않는다. 이렇게 DB로 저장된 값을 가져와 표시하려면 지금처럼 정적인 렌더링은 알맞지 않다. 이것은 차후 동적 렌더링에서 따로 알아보도록 하자.
-
ESNext #3: 사이드바
이번에는 사이드바의 제어에 대해 예제 코드를 작성해본다. 사이드바는 문서 전체나 일부 블록, 혹은 플러그인이 확장할 수 있도록 되어 있다. 처음에는 사이드바가 휙휙 변해서 혼동이 많았지만, 몇 번 보고 그 동작에 대해 이해하기 시작하니 나쁘지 않다.
이 사이드바는 이제 매우 중요한 의미가 있다. 클래식 에디터에서는 글 편집 이외의 여러 제어 요소들을 메타 박스로 처리했다. 이 메타 박스들은 여전히 유효하고 때때로는 여전히 현역으로 동작하지만 새로운 블록 에디터를 사용하는 경우, 메타 박스보다는 사이드바 쪽의 동작이 더 필요할 수 있을 것 같다. 특히 블록을 제작하는 경우는 여지없이 사이드바를 익혀야 할 듯.
우선 사이드바에 대한 Hello, World! 출력하는 코드를 작성해 본다. 완성된 코드는 마찬가지로 Github 레포지터리에 코드를 올려 두었다.
서버 사이드 코드 (PHP)
서버에서는 단순하게 js 코드를 큐잉하기 위한 코드로만 되어 있다. 늘 그렇듯 ‘
init
‘ 액션과 ‘enqueue_block_editor_assets
‘ 액션을 사용하고 index.asset.php 파일에서 생성된 정보를 활용해 스크립트를 등록하는 구현이 있다.사이드바에 대해
사이드바에 ‘플러그인’을 넣어 보자. 플러그을 등록하면 플러그인만의 사이드바를 가지게 되고 거기에 원하는 대로 위젯을 꾸밀 수 있게 된다.
플러그인에 등록되면 아래 그림처럼 … 메뉴를 눌렀을 때 플러그인 섹션에 표시된다.
1번 … 메뉴 클릭시 사이드바가 오픈. ‘플러그인’ 섹션에 2번 Jetpack 메뉴가 등록되어 있다. Jetpack을 선택하면 Jetpack의 사이드바 메뉴가 출력된다. 1번의 별 아이콘을 그림처럼 누른 상태로 만들면 2번 영역에 아이콘이 고정되어 언제든지 저 아이콘을 클릭하는 것으로 메뉴를 열고 닫을 수 있다. 이것이 선택되면 우측 사이드바에는 플러그인만의 위젯이 출력된다. 또한 가장 위 우측에 보면 별 아이콘이 있는데, 선택하면 … 메뉴 좌측, 톱니바퀴 아이콘 우측으로 사이드바를 여닫을 수 있는 아이콘이 고정된다. 앞으로는 생성된 아이콘을 눌러 사이드바를 열었다 닫았다를 할 수 있다.
물론 새 사이드바를 만드는 것도 가능하지만, 기존의 ‘문서’ 사이드바에 요소를 추가하는 것도 가능하다. 이것은 차후에 진행하기로 하고 여기서는 새 플러그인을 위한 사이드바 생성만 다룬다.
클라이언트 사이드 코드 (JavaScript)
우선 플러그인을 등록한다. 등록을 위해서는 ‘registerPlugin’ 함수를 사용한다. 이것은 ‘@wordpress/plugins’ 패키지에 있다. packages.json 에 등록하고 import 한다.
registerPlugin 함수는 두 개의 인자를 가진다. 첫번째는 고유한 이름. 두번째는 설정 객체이다. 간단하게 두 개의 키 ‘icon’, ‘render’를 필요로 한다. icon은 간단히 dashicons에서 찾을 수 있는 이름으로 할 수 있다.
render는 함수로서 플러그인의 내용을 그린다. 여기에 사이드바 내용을 넣으려면 PluginSidebarMoreMenuItem, PluginSidebar 둘이 필요하다. 이 둘은 ‘@wordpress/edit-post’에서 찾을 수 있다.
PluginSidebarMoreMenuItem은 … 메뉴 클릭시 ‘플러그인’ 섹션에서 만든 사이드바가 선택될 수 있도록 하는 객체이며, PluginSidebar는 사이드바의 내용을 직접 선언한다.
즉, PluginSidebarMoreMenuItem을 사용하면 아래처럼 결과가 작성된다.
그리고 해당 메뉴를 클릭하면 좌측 사이드바에 나올 내용은 PluginSidebar를 이용해 작성한다.
registerPlugin('wes3-plugin-hello-world', { icon: 'welcome-view-site', render() { return ( <> <PluginSidebarMoreMenuItem target="wes3-plugin-hello-world"> {__('Hello, World!', 'wp-esnext-study')} </PluginSidebarMoreMenuItem> <PluginSidebar name="wes3-plugin-hello-world" title="Hello, World!" icon="welcome-view-site"> </PluginSidebar> </> ) } });
PluginSidebarMoreMenuItem의 target 속성과 PluginSidebar name 속성은 서로 같게 이어준다. title 속성은 사이드바 최상단 제목을 위한 텍스트이고, icon은 사이드바가 즐겨찾기 추가되었을 때 보여질 아이콘이다.
좀 더 레이아웃에 맞춰 내용 출력하기
나머지 사이드바에 넣을 내용들은 <PluginSidebar> 안에 넣으면 된다. 그러나 레이아웃이 잘 정돈되어 나오지 않는다. 레이아웃은 좀 맞춰서 넣어 보자. 이 때 ‘@wordpress/components’ 패키지의 Panel을 이용한다.
<PluginSidebar name="wes3-plugin-hello-world" title="Hello, World!" icon="welcome-view-site"> <Panel header="Hello, World! Panel" > <PanelBody title="Hello, World! Body" icon="welcome-view-site" initialOpen={true}> <PanelRow> <p>Hello, World! Content.</p> </PanelRow> </PanelBody> </Panel> </PluginSidebar>
결과는 아래와 같다.
-
WPML에서 추가한 태그의 언어가 포스트의 언어와 달라요?!
워드프레스 편집 화면에서 아래 위젯을 이용해 기존의 태그를 가져오거나, 새로운 태그를 작성할 수 있습니다.
그런데 WPML을 사용하는 경우에 태그를 추가할 때 작성중인 언어의 태그가 나오는 것이 아닌, 엉뚱한 언어의 태그가 달려 나오는 경우가 종종 있을 것입니다. 이 포스트에서는 그 원인이 무엇인지, 그리고 어떻게 해결할 수 있는지에 대해 작성하려고 합니다. 단, 새 버전의 에디터인 구텐베르크에서 글 작성을 한다고 가정합니다.
새로운 태그를 추가하거나, 기존 태그를 검색할 때는 WP REST API를 사용하게 됩니다. 이 때 엔드포인트는
/wp-json/wp/v2/tags/
입니다. 조회시 GET으로, 생성시 POST로 메시지를 보냅니다.워드프레스가 요청을 받으면 설치된 WPML 플러그인이도 초기화됩니다. 이 때 현재 사용자가 어떤 언어를 사용하는지 결정합니다. WPML이 사용자의 언어를 인식하는 방법은 몇 가지가 있지만, 가장 기본적으로 사용되는 방법은 쿠키입니다.
WPML이 언어를 위해 2개의 쿠키 값을 사용하는데 프론트를 위해서는
wp-wpml_current_language
, 관리자 화면을 위해서는wp-wpml_current_admin_language_{해시}
입니다.WPML이 쿠키값을 설정한 화면. F12키를 눌러 개발자 화면을 보면 확인할 수 있습니다. WPML에서 언어를 선택할 때 이 쿠키 값이 변경됩니다. 관리자 화면 상단 관리 바에서 언어를 선택하면 이 값이 갱신됩니다.
태그 언어가 꼬이는 경우 분석
문제는 이 언어 값을 표시하는 문맥이 한 가지가 아니기 때문에 발생합니다. 이 언어 표시는 두 가지의 문맥을 가집니다.
- 현재 보여줄 언어
- 현재 편집 중인 포스트의 언어
첫번째 보여줄 언어는 포스트 편집 화면이 아닌 곳에서 나타납니다. 예를 들어 포스트 목록에서 나오는 언어는 현재 선택한 관리자 화면의 언어를 보여줍니다. 반면 포스트 편집 화면에서는 현재 편집 중인 포스트의 언어를 보여 줍니다.
포스트 목록에서 어드민 바에 있는 언어 표시를 영어로 맞추었습니다. 영어로된 포스트만 나열됩니다. 로컬 개발 서버에서 진행해서 라이센스가 맞지 않다고 나오네요… 이 때 쿠키값을 보면 wp-wpml_current_admin_language_{해시}
로 된 곳에 ‘en’으로 되어 있을 것입니다.포스트 목록에서 어드민 바에 있는 언어 표시를 한국어로 맞추었습니다. 한국어로만 된 포스트만 나열됩니다. 이 때 쿠키값을 보면 wp-wpml_current_admin_language_{해시}
로 된 곳에 ‘ko’으로 되어 있을 것입니다.사실 WPML은 이런 흐름을 생각했을 것입니다.
- 사용자가 포스트 편집을 위해 관리자 화면에서 영어를 선택합니다.
- 관리자 화면에 표시되는 언어가 영어로 표시됩니다. 또 포스트 목록에는 언어가 영어인 포스트가 나옵니다.
- 이렇게 되면 쿠키에 기록된 언어가 모두 영어로 기록되므로, 태그 검색, 작성 때에는 모두 영어로 번역된 태그를 검색할 것입니다.
그러나 문제는 아래처럼 작업하지 않을 때 발생합니다.
- 사용자는 언어를 한국어로 선택한 상태(즉, 쿠키에 저장된 값은 ‘ko’)입니다.
- 동료로부터 영어로 번역된 포스트의 점검을 위해 영어로 작성된 포스트 편집 URL을 전달받았습니다. 이때 불행히도 lang 파라미터가 누락되어 있었습니다.
- 대시보드의 언어는 한국어지만, URL을 통해 직접 접근하였고 lang 파라미터가 누락되어 쿠키의 값은 ‘ko’가 그대로 변하지 않습니다.
- WPML은 이 때 쿠키 값을 변경하지 않습니다. 그리고 관리자 표시 화면은 어떨까요? 포스트 편집 화면으로 들어왔고, 포스트의 언어는 영어이므로 영어로 표시가 될 겁니다.
- 그리고 태그 검색시 쿠키 값이 ‘ko’이므로 태그 검색은 한국어 태그에서 검색을 할 겁니다.
- 빰! 버그 발생. 포스트 언어는 영어인데, 태그는 한국어 태그가 달리는 현상이 발생합니다.
사용자가 F12 키를 눌러 쿠키 값을 직접 확인하지 않는 이상 이것을 직접 확인할 수 있는 방법이 없습니다. WPML이 여러 기능을 제공하기는 하지만, 너무 산만하고 어렵다는 느낌을 가지게 되는데 이런 일들이 많이 겪지 않은가 생각합니다.
line.dev.site는 제 임의의 로컬 개발 서버 도메인입니다. URL 끝에 lang 파라미터가 없다는 점만 주목해 주세요. 이전에 언어가 한글이었는데, 이렇게 URL을 타고 넘어오면 현재 포스트 언어가 영어임에도 불구하고 쿠키의 값이 ‘en’으로 변경되지 않는 문제가 발생합니다. WPML 버전은 4.3.19입니다. 나중에는 이런 점이 수정되었으면 좋겠네요. 이것을 어거지로 고치는 PHP쪽 코드를 아래 첨부합니다. 동작은 100% 보장하지 못하니, 적절히 의미만 캐치하셔서 사용하세요. 한편 자바스크립트 쪽에서는 미들웨어라는 게 있어서 중간에 기능을 변경하는 방법이 있는 것 같은데, 아직 그 부분은 정확히 알지 못합니다. 혹시 알아내게 되면 후속으로 포스팅하겠습니다.
<?php add_action( 'wpml_language_cookie_added', function ($lang_code) { static $determined = null; if (is_null($determined)) { $uri = parse_url($_SERVER['REQUEST_URI']); $ref = parse_url($_SERVER['HTTP_REFERER']); parse_str($ref['query'] ?? '', $ref_qs); $is_post_edit = ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET' && ($uri['path'] ?? '') === '/wp-json/wp/v2/tags' && ($ref['path'] ?? '') === '/wp-admin/post.php' && ($ref_qs['action'] ?? '') === 'edit' && isset($ref_qs['post']); $is_new_tag = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && ($uri['path'] ?? '') === '/wp-json/wp/v2/tags' && ($ref['path'] ?? '') == '/wp-admin/post.php'; if ($is_post_edit || $is_new_tag) { global $wpml_request_handler; $cookie_name = 'wp-wpml_current_admin_language_' . md5($wpml_request_handler->get_cookie_domain()); $info = wpml_get_language_information(null, $ref_qs['post']); // 나중에 현재 언어를 쿠키 변수에서 인식한다. 이 값을 임시로 변경 처리하여 // 사용자가 현재 선택한 언어가 아닌, 현재 포스트의 언어를 참조하도록 조정한다. $_COOKIE[$cookie_name] = $determined = $info['language_code']; } else { $determined = $lang_code; } } } );