개요
기본 개념
이 글은 독자들이 워드프레스의 기본적인 작동 원리를 안다는 가정하게 작성되었다. 어쨌거나 직접 ftp를 통해서든, 로컬 개발 환경을 만들어 개발하든 직접 php 파일을 개선시켜나가는 사람들에게 유용한 글이다.
babel이나 node.js, webpack, wp-cli 등은 사용하지 않았다. 제대로 개발하려면 위와 같은 개발환경을 세팅하고 빌드 및 배포의 과정까지 자동화하면 더욱 더 생산성 높게 개발할 수 있겠지만 필자의 내공은 그렇게 높지가 않다. 거기까지는 무리이다. 필자는 docker를 이용해 로컬 개발 환경을 구성하여, 자식 테마를 직접 수정해나가며 개발한 뒤, wp-migration이라는 블로그 이전 플러그인을 이용해 통째로 웹호스팅 서버에 덮어씌우는 다소 고전적인 방식으로 개발중이다. 아예 ftp를 이용해서 웹호스팅 내의 파일을 직접 수정할 수도 있겠으나, ftp를 한번 거쳐야 한다는 번거로움이 있어서, 이왕 하는 김에 로컬에서 해보자 하고 아주 소심하게 docker를 이용해보았다.
워드프레스가 어떻게 돌아가는지 모른다면, 이 글을 읽기에 다소 힘들 수도 있다. 필자 또한 워드프레스에 조예가 깊지 않으므로 많이 엇나갈 수도 있지만 간단하게 설명해보겠다. 우선 워드프레스를 php 위에서 돌아가는 프로그램으로 생각하자. 만약 유저가 적절한 주소를 쳐서 요청을 보내면 php 위에서 돌아가는 워드프레스가 알아서 그 요청을 처리하고 그 결과를 html으로 조합하여 응답한다. 유저는 워드프레스가 생성한 html를 브라우저에서 보게 되는 것이다.
php는 요청을 처리하고 적절한 결과를 만들어내는 일을 하기 때문에 서버에서 할 일이 많다. 즉 자원이 상당히 한정적이라고 이야기할 수 있다. 시대의 흐름은 웹앱의 추세로 넘어가고 온갖 상태가 동적으로 관리되는 중에 php의 처리방식으로는 버거워진다. 그래서 자바스크립트도 수레 당기기에 적극적으로 동참한다. Gutenberg (한국어로 읽자면 구텐베르크)와 같은 신식 에디팅 환경에서는 태반이 자바스크립트다.
브라우저에서만 작동하는 자바스크립트는 그렇다면 어떻게 필요한 정보를 서버에서 받아오고, 서버에게 변경된 사항을 전달할까? 본래 워드프레스에도 클라이언트로부터의 세세한 요청을 처리하기 위해 ajax 요청 응답 처리를 할 수 있도록 해놓았는데, 최근에는 더 세련되게 REST API로 아예 다 받을 수 있도록 해놓았다. 그러니까 예전에는 php에서 데이터를 처리하고 보여주는 것까지 모두 담당했다면 지금은 워드프레스 코어가 돌아가는 부분을 php에서 처리하고 데이터를 보여주는 쪽을 자바스크립트가 담당하며 그 사이의 통신을 REST API로 통일시킨다는 것이다! REST API 요청도 우리가 필요할 때만 해도 되고, 왠만한 경우는 워드프레스에서 제공해주는 자바스크립트 라이브러리가 일을 다 한다.
옛날의 워드프레스
요즘의 워드프레스
계속되는 추세가 php 부분을 덜어낸다고 들었다. 왜 덜어내는지, 어떤 걸로 다시 채워넣을지는 잘 모르겠지만, 어쨌건 워드프레스가 기능을 분리하고 새로운 걸로 만들고 하니 계속해서 변할 것 같다. 이 글도 곧 고전이 될 것이다.
파일들의 역할
필자는 카테고리 분류에 따라 최근 글 하나를 특정 템플릿으로 출력하는 블록을 만들고자 한다. 아래 파일들은 테마 폴더에 위치한다.
functions.php
: 여기서는 새로운 블록에 따른 js 파일 등록을 하고, 블록에서 저장된 attribute 등을 불러와 렌더링하는 render 함수를 구현한다.editor/recent-posts-block.js
: 구텐베르크에서 잘 작동하는 블록을 만들 것이다.functions.php
에서 불러올 예정이다.withSelect
를 통해 카테고리 목록을 백엔드에서 가져온 뒤 콤보박스 창으로 선택할 수 있게 할 것이다. 선택하면 해당 카테고리의 id 값을 attribute로 저장한다.recent-widget-template.php
: 출력되는 부분을functions.php
와 독립시키기 위해 별도의 파일로 만들었다. 이 부분을functions.php
파일 내에 두어도 무방하나, 코드가 지저분해질 수 있다.
보통 블록을 새롭게 만들고자 할 때, 사용자 정의 플러그인을 만들어서 하는 경향이 있다. 왜냐하면 그 기능을 떼고 붙이기가 더 편리하기 때문이다. 하지만 플러그인은 필자 스타일이 아니다. 필자는 테마 메커니즘을 적극적으로 활용할 것이다.
전체 코드
전체 코드 먼저 보자.
// mytheme/functions.php
function ezkorry_recent_posts_render($attributes, $content)
{
// 출력 버퍼 켜기. 지금부터 출력되는 것들을 따로 저장한다.
ob_start();
// show vars for debug
// print_r($content);
// print_r($attributes);
// 실행할 쿼리.
$args = array(
'cat' => $attributes['category_id'],
'posts_per_page' => 1,
'offset' => $attributes['offset']
);
// 쿼리 생성
$query = new WP_Query($args);
// 생성한 쿼리를 기반으로 루프 돌기
if ($query->have_posts()) :
while ($query->have_posts()) :
$query->the_post();
set_query_var('widget_type', $attributes['widget_type']);
get_template_part('template-recent-posts');
endwhile;
endif;
// 쿼리 초기화
wp_reset_postdata();
// 출력된 내용 저장
$output = ob_get_contents();
// 출력 버퍼 끄기
ob_end_clean(); // Turn off ouput buffer
// 결과 리턴
return $output;
}
function ezkorry_recent_posts_register()
{
wp_register_script(
'ezkorry_recent_posts',
get_stylesheet_directory_uri() . '/editor/recent-posts-block.js',
array('wp-editor')
);
register_block_type('ezkorry/recent-posts', array(
'editor_script' => 'ezkorry_recent_posts',
'render_callback' => 'ezkorry_recent_posts_render'
));
}
add_action('init', 'ezkorry_recent_posts_register');
// mytheme/editor/recent-posts-block.js
(function(wp) {
var el = wp.element.createElement,
registerBlockType = wp.blocks.registerBlockType,
withSelect = wp.data.withSelect;
const { InspectorControls, RichText } = wp.blockEditor;
const { SelectControl, PanelBody, TextControl } = wp.components;
registerBlockType("ezkorry/recent-posts", {
title: "ezkorry recent posts",
icon: "megaphone",
category: "widgets",
attributes: {
category_id: {
type: "string",
selector: "power-overwhelming",
default: 1
},
content: {
type: "string",
selector: "js-guten-content"
},
offset: {
type: "string",
default: 0,
},
widget_type: {
type: "string"
}
},
edit: withSelect(function(select) {
// select에서 어떤 데이터를 긁어올 수 있는가에 대한 테스트용
return {
posts: select("core").getEntityRecords("postType", "post"),
blocks: select("core").getEntityRecords("postType", "wp_block"),
pages: select("core").getEntityRecords("postType", "page"),
attachments: select("core").getEntityRecords("postType", "attachment"),
categories: select("core").getEntityRecords("taxonomy", "category"),
tags: select("core").getEntityRecords("taxonomy", "post_tag"),
medias: select("core").getEntityRecords("root", "media"),
post2: select("core").getEntityRecords("root", "postType")
};
})(function(props) {
var { category_id, offset, widget_type } = props.attributes;
const { attributes, className, setAttributes } = props;
if (!props.categories || !props.attributes.category_id) {
return "로딩중";
}
console.log(props);
var options = props.categories.map(function(item) {
return { label: item.name, value: item.id };
});
const widget_options = [
{ label: "좌 썸네일", value: "left-thumbnail" },
{ label: "상단 썸네일", value: "top-thumbnail" }
];
return [
el(
InspectorControls,
null,
el(PanelBody, { title: "설정" }, [
el(SelectControl, {
label: "위젯 타입",
value: widget_type,
options: widget_options,
onChange: function(value) {
setAttributes({ widget_type: value });
}
}),
el(SelectControl, {
label: "카테고리",
value: category_id,
options,
onChange: function(value) {
setAttributes({ category_id: value });
}
}),
el(TextControl, {
label: "오프셋",
value: offset,
type: 'number',
help: "적힌 숫자만큼 포스팅이 생략됩니다.",
onChange: function(value) {
setAttributes({ offset: value });
}
})
])
),
el(RichText, {
className: "js-guten-content",
value: attributes.content,
tagName: "h3",
placeholder: "호호",
onChange(value) {
setAttributes({ content: value });
}
})
];
})
});
})(window.wp);
<?php
// mytheme/recent-widget-template.php
$widget_type = get_query_var('widget_type');
$col = 12;
if ($widget_type == 'left-thumbnail') {
$col = 6;
}
?>
<div class="recent-posts-category <?php echo get_query_var('widget_type'); ?>">
<!--<div class="container">-->
<div class="row">
<div class="col-sm-<?php echo $col; ?> align-self-center">
<div class="thumbnail img-container">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail(); ?>
</a>
</div>
</div>
<div class="col-sm-<?php echo $col; ?> align-self-center">
<p class="category"><span><?php the_category(', '); ?></span></p>
<h2 class="entry-title"><a href="<?php the_permalink(); ?>"> <?php the_title(); ?></a></h2>
<div class="excerpt">
<?php the_excerpt(); ?>
</div>
<?php if ($widget_type == 'left-thumbnail'): ?>
<div class="read-more"><a href="<?php the_permalink(); ?>">READ MORE</a></div>
<?php endif; ?>
<!--<div class="date"><span><?php the_time(get_option('date_format')) ?></span></div>-->
</div>
</div>
<!--</div>-->
</div>
구현
대락적인 흐름도
recent-posts-block.js
우선 js 파일부터 살펴보자. js 파일은 워드프레스의 신식 에디터인 구텐베르크로 작업할 때에만 불러온다. 실제 사이트의 페이지를 요청할 때 이 js파일은 로딩되지도 않고 실제로 쓰임새도 없다.
이 파일에서 가장 중요한 점은 핵심 기능인 registerBlockType
함수를 제대로 호출하는 데 있다. 이 함수를 호출할 때의 인자는 다음과 같다.
- 첫 번째 인자는 블록의 이름이다. 커다란 대분류(네임스페이스라고 생각하면 편하다)를 왼쪽에, 세부 블록 이름을 오른쪽에 하여 이름을 정하면 된다. 필자는
ezkorry/recent-posts
라고 정했다. - 두 번째 인자는 블록에 대한 자세한 사항을 적는 인자이다.
블록의 자세한 사항은 다음과 같은 방법으로 세부사항을 결정하면 된다.
title
: 에디터에서 편집할 때 겉으로 드러나는 이름을 지정한다.icon
: 보이는 아이콘을 지정한다. 아이콘의 이름만 지정하면 알아서 아이콘이 보여진다. 아이콘 목록은 대쉬콘 참조 (예를 들어dashicons-randomize
아이콘을 사용하고 싶다면icon: 'randomize'
로 지정하면 된다 .)category
: 블록이 데이터의 어느 분류에 위치해있을지를 정한다. 그 목록은 공식 문서 참조.attributes
: 블록과 함께 저장될 속성을 먼저 지정해준다. 속성이라고 해도 의미가 잘 통하기는 하지만 아직 정식으로 국내 용어가 정립된 것이 아니므로attribute
라고 계속 이야기할 것이다.attribute
가 어떤 식으로 저장될지, 혹은 데이터에서 어떻게 불러올지도 여기서 정하는데, 관련된 키는source
,selector
등이다. 아래에서 다시 설명하도록 한다.edit
: 블록이 에디터에서 수정될 때 어떻게 보여질지를 결정한다. 여기에서는 실제 글을 쓰는 칸, InspectorControls(사이드바), 툴바 등에서 보일 요소를 모두 설정할 수 있다. edit은 인수를 하나 받는 함수이다. 대개 이 인수의 이름을props
라고 하며, 블록과 관련된 정보가 담겨있다.edit
은wp.element.createElement
함수의 호출 결과를 반환하여야 한다. 이 특별한 함수는 위의 예제에서el
이라는 약칭으로 하여 계속 호출하고 있다. 이 함수에 대한 자세한 설명은 아래에서 계속한다. 어쨌거나 저쨌거나edit
은 다음과 같은 형태를 보일 것이다.
registerBlockType('...', {
...
edit: function(props) {
...
return el(
....
);
},
...
});
save
: 블록이 실제로 어떻게 보여질지를 결정한다. edit 함수와 마찬가지로props
를 인수로 받고 값 하나를 리턴한다. 이 리턴값은 편집이 끝나는 시점에 데이터베이스의 포스트 내용에 특정한 형태로 저장된다. (이 글 하단 참조)
attributes
설정
다음과 같은 기본 구조이다.
attributes: {
속성1: {
type: string
source: ...,
selector: ...,
default: ...,
...
},
속성2: {
...
}
}
속성 이름은 그냥 키 값으로 설정하면 된다. 속성을 저장하는 방식과 읽는 방식을 결정하는 source
, selector
등은 이 글에서 다루지 않는다. 그냥 type
을 string
으로 설정하고 default
로 기본 값을 설정하자. attribute를 올바르게 설정했다면 edit
함수에서 props.attributes.속성1
과 같이 접근할 수 있다.
withSelect
사실 잘 모른다. 여기서는 워드프레스에게 정보를 요청하고, 그 정보를 props
에 저장시키는 역할로 withSelect
를 사용했다. withSelect
는 edit
의 래퍼 함수로 볼 수 있고, edit
에 해당하는 함수는 withSelect
의 두 번째 인수로 전달한다. 첫 번째 인수로 들어가는 함수는 select
라는 인자를 직접 호출해서 여러가지 정보를 불러오겠다 하는 것이다. 뭔 말인지 나도 모르겠다. 아래 예제를 참조하자. 두 번째 함수에서 props
를 출력해보면 대략적으로 작동하는 원리를 알 수 있다.
edit: withSelect(function(select) {
// select에서 어떤 데이터를 긁어올 수 있는가에 대한 테스트용
return {
posts: select("core").getEntityRecords("postType", "post"),
blocks: select("core").getEntityRecords("postType", "wp_block"),
pages: select("core").getEntityRecords("postType", "page"),
attachments: select("core").getEntityRecords("postType", "attachment"),
categories: select("core").getEntityRecords("taxonomy", "category"),
tags: select("core").getEntityRecords("taxonomy", "post_tag"),
medias: select("core").getEntityRecords("root", "media"),
post2: select("core").getEntityRecords("root", "postType")
};
})(function(props) {
console.log(props)
})

select
나 getEntityRecords
나 이런 함수 안에 들어갈 인자가 무엇이냐에 대한 설명이 참으로 찾기 힘들다. 구글링과 여러가지 시도를 통해 대략적으로 어떤 정보를 가져올 수 있는지 테스트해보았다. 아래는 select("core").getEntityRecords(...)
에 들어갈 인수에 따른 데이터이다. 이 방법들은 모든 것들을 다 긁어오므로 일부만 가져오려면 세 번째 인수에 쿼리를 추가해야 한다. 그 방법들은 연구가 다소 필요하므로 이 글에서는 적지 않겠다. (레퍼런스 참조)
첫 번째 인수 | 두 번째 인수 | 결과 |
postType | post | 글 목록 |
postType | wp_block | 아무 것도 안나옴 |
postType | page | 페이지 목록 |
postType | attachment | 미디어 목록 |
taxonomy | category | 카테고리 목록 |
taxonomy | post_tag | 태그 목록 |
root | media | 미디어 목록 (‘postType’, ‘attachment’와 동일) |
root | postType | 글 타입 목록 |
edit
에서 값이 제대로 로딩됐는지 확인
새로운 props
가 갱신될 때마다 edit
함수가 실행되므로, 만약 원하는 정보가 없다면 그냥 의미없는 값을 return
해버리는 것으로 간단하게 처리를 할 수 있다. 아래는 그 코드이다.
if (!props.categories || !props.attributes.category_id) {
return "로딩중";
}
요소를 만들자 (createElement)
함수로 어떤 요소를 계속해서 만들어내는 형태는, 필자도 자세히는 모르지만 react에서의 쓰임새와 비슷하다고 한다. wp.element.createElement
함수(줄여서 el
)가 받는 세 가지 인수에 대한 간략한 설명은 다음과 같다.
- 첫 번째 인수 : 해당 element가 어떤 종류인지 그 타입을 정한다. 미리 만들어진 컴포넌트를 이용할 수도 있고 사용자 정의 템플릿을 이용할 수도 있다.
- 두 번째 인수 : 해당 element를 생성할 때 필요한 정보를 넣는다.
- 세 번째 인수 : 해당 element의 자식(children)을 넣는다. 자식이 하나만 있다면
el
을 다시 호출할 수도 있고 자식이 여러 개라면el
호출을 담은 배열을 넣을 수도 있다.
위 예제에서 el
부분만 뽑아서 본다면 다음과 같다.
function(props) {
...
return [
el(
InspectorControls,
null,
el(PanelBody, ..., [
el(SelectControl, {
...
}),
el(SelectControl, {
...
}),
el(TextControl, {
...
})
])
),
el(RichText, {
...
})
];
}
이 예제에 쓰인 컴포넌트(element 타입)는 다음과 같다.
InspectorControls
: 사이드바에 해당한다.PanelBody
: 사이드바 안의 그룹에 해당한다.SelectControl
: 목록 중 하나를 선택할 수 있는 ui이다.TextControl
: 텍스트를 적을 수 있는 ui이다.RichText
: 에디터의 본 화면에서 텍스트를 적을 수 있도록 한다.
보면 InspectorControls
와 RichText
가 동일한 배열에 있는 것을 확인할 수 있는데, 저렇게 해놓기만 해도 InspectorControls
는 사이드바 자리에, 그리고 RichText
는 에디터의 본래 편집 자리에 각각 잘 위치하게 된다.
유의하여야 할 점은 요소마다 value
, onChange
등을 적절하게 잘 설정하여야 현재의 값을 잘 표시하고 변경될 값을 무사히 적용시킬 수 있다.
save
를 안쓰는 이유
save
는 이 글이 어떻게 외부로 보여질지 결정한다고 했는데, 이 글에서는 save
를 만들지 않는다. 왜냐하면 서버사이드에서 렌더링하기 때문이다. 우리가 자바스크립트 단에서 하는 역할은 값을 저장할 attribute
들을 설정하고 그 attribute
들을 수정할 수 있는 에디터 ui를 만드는 것이다. 이 값들을 조합하여 실제 페이지로 만드는 건 php로 역할을 넘긴다.
functions.php
워드프레스에서는 사용자가 작성한 코드를 워드프레스의 실행 흐름 속으로 녹이기 위하여 filter
, action
등의 개념을 만들고, 그러한 filter
와 action
이 실행되는 후크를 사전에 세팅해두었다. 이 예제에서는 init
이라는 후크에다가 우리의 블록 등록과 관련된 코드를 연결시킬 것이다. 기본 구조는 아래와 같다.
function ezkorry_recent_posts_register()
{
...
}
add_action('init', 'ezkorry_recent_posts_register');
ezkorry_recent_posts_register
함수에서는 우리가 작성한 js 파일과, render_callback
함수를 지정한다.
wp_register_script(
'ezkorry_recent_posts',
get_stylesheet_directory_uri() . '/editor/recent-posts-block.js',
array('wp-editor')
);
register_block_type('ezkorry/recent-posts', array(
'editor_script' => 'ezkorry_recent_posts',
'render_callback' => 'ezkorry_recent_posts_render'
));
render_callback
인 ezkorry_recent_posts_render
함수도 만들어준다.
function ezkorry_recent_posts_render($attributes, $content)
{
// 출력 버퍼 켜기. 지금부터 출력되는 것들을 따로 저장한다.
ob_start();
// 실행할 쿼리.
$args = array(
'cat' => $attributes['category_id'],
'posts_per_page' => 1,
'offset' => $attributes['offset']
);
// 쿼리 생성
$query = new WP_Query($args);
// 생성한 쿼리를 기반으로 루프 돌기
if ($query->have_posts()) :
while ($query->have_posts()) :
$query->the_post();
set_query_var('widget_type', $attributes['widget_type']);
get_template_part('template-recent-posts');
endwhile;
endif;
// 쿼리 초기화
wp_reset_postdata();
// 출력된 내용 저장
$output = ob_get_contents();
// 출력 버퍼 끄기
ob_end_clean(); // Turn off ouput buffer
// 결과 리턴
return $output;
}
여기서는 크게 두 가지 흐름이 있다.
- 출력 버퍼 설정 :
ob_start()
를 호출하고ob_end_clean()
을 호출하기 전 까지 출력 버퍼를 활성화시켜서 출력된 내용을 모두 하나의 문자열에 저장하겠다는 흐름이다. - 워드프레스 쿼리 : 저장된 글을 불러오기 하여 새로운 쿼리를 만드는 흐름이다.
new WP_Query()
를 이용해 쿼리를 만들고$query->have_posts()
를 이용해 글이 있는지 체크를 한다.$query->the_post()
로 현재 글을 설정하여 따로 만들 템플릿 파일 내에서the_title()
등의 함수를 쓸 수 있도록 한다. 모든 작업을 마치면wp_reset_postdata()
를 호출하여 본래의 흐름으로 돌아간다.
템플릿 파일을 로딩하기 위해 get_template_part('template-recent-posts')
라고 작성했다. 이렇게 하면 테마 폴더에 있는 template-recent-posts.php
파일이 불러와진다.
템플릿 파일로 데이터를 전달하기 위해 set_query_var
함수를 이용했다. 템플릿 파일 내부에서는 get_query_var
함수로 값에 접근할 수 있다.
template-recent-posts.php
그냥 위 코드를 참조해주세용. 어려운 내용은 아님.
유의사항
골때리는 점은, php의 registerBlockType
과 js의 register_block_type
의 짝짜꿍이 아주 잘 들어맞아야 문제없이 작동한다는 것이다. 우여곡절이 좀 많았다. 아래는 유의해야 할 항목들이다.
- js의
registerBlockType
과 php의register_block_type
에서 쓰인 첫번 째 인수 (블록의 이름)이 완전히 동일해야 한다. - js에서,
registerBlockType
할 때save
함수가 정의가 되어있지 않거나null
을return
해야 한다.save
함수는 저장 과정에서 작동하므로 나중에 php가 저장된 데이터를 읽을 때 제대로 작동하지 않을 여지가 크다. - js에서, 해당
attribute
에서source
가 정의되지 않고selector
가 정의되어 있어야 한다.source
가 정의되어 있지 않아야 php에서 잘 읽히더라.selector
의 역할은 사실 잘 모르겠다. 본래 css 실렉터처럼 실제 저장된 데이터에서 데이터를 뽑아낼 때 쓰는 건데, (예를 들어source
를attribute
로 하고selector
를href
로 하면 어떤 엘리먼트의href
속성 데이터를 불러온다.) 실제 데이터 저장된 것을 보나 php의 render 함수에서 보나 selector는 찾아볼 수 없었다. - js의 해당 attribute에서
tpye
은string
이어야 한다. 공식 문서를 살펴보면 지원되는type
이null, boolean, object, array, number, string, integer
로 굉장히 다양하다. category id가 숫자라서integer
를 썼는데 제대로 작동하지 않아 도대체 원인이 무엇인가 별 쓸 데 없는 짓을 다 해보고 이type
을string
으로 고쳐보니 문제 없이 작동되었다. 원인은 잘 모르겠다. 서버사이드와 짝짜꿍 하려면 마음 편하게string
을 쓰는 걸 추천한다. - php에서,
register_block_type
할 때attributes
가 정의되어 있지 않아야 한다. 설정하기만 해도 js에서 설정한attributes
와 충돌하는 모양이다. 마음 편하게 아무 정의도 하지 말자.
참고사항
블록의 attribute
들은 어디에 어떻게 저장되나?
그냥 포스트 내용에 특별한 형식으로 저장된다. 데이터베이스로 따지면 wp_posts에 post_content에 글 내용과 함께 저장된다. 저장되는 방식은 다음과 같다.
<!-- wp:ezkorry/recent-posts {"category_id":"2","content":"특별1","widget_type":"left-thumbnail"} /-->
<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:ezkorry/recent-posts {"category_id":"3","content":"미드1","widget_type":"top-thumbnail"} /--></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:ezkorry/recent-posts {"category_id":"4","content":"미드2","widget_type":"top-thumbnail"} /--></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:ezkorry/recent-posts {"category_id":"5","content":"미드3","widget_type":"top-thumbnail"} /--></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column {"verticalAlignment":"top"} -->
<div class="wp-block-column is-vertically-aligned-top"><!-- wp:paragraph -->
<p>파워오버웰밍~~~호우 맨 파워스튜</p>
<!-- /wp:paragraph -->
<!-- wp:latest-posts {"displayPostContent":true} /--></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->
이런 데이터를 워드프레스에서 읽어서 $attributes
등을 설정하고, render 함수에 적절하게 인수로서 전달해주는 것이다. 어쨌거나 어떻게 저장되는지 신경쓸 필요는 없다. 워드프레스에서 알아서 이 정보들을 읽으니까.
withSelect
를 적절히 이용해서 save
함수를 구현하면 굳이 서버사이드에서 렌더링할 필요가 없지 않나?
그렇다고 볼 수 있다. 하지만 필자는 withSelect
의 정확한 쓰임새도 모르고, 특히 워드프레스에서 제공해주는 REST API를 통해 적절한 쿼리를 수행하는 예제는 (열심히 찾아보지는 않았지만) 잘 찾아볼 수 없었다. 아마 이런 흐름이 최근의 흐름이기도 하여 관련된 정보가 많이 없는 탓일 것이다.
save
함수를 적절히 구현할 수 있다면 정말 좋다. 왜냐하면 굳이 php에서 렌더링 함수를 짜줄 필요가 없기 때문이다. 다양한 프론트엔드 자바스크립트 라이브러리를 활용할 여지가 커진다는 것도 장점이겠다.
기타
- js 파일을 등록하고
register_block_type
함수를 실행하는ezkorry_recent_posts_register
함수에서는 사이트 관리자가 Gutenberg를 사용 중인지 아닌지 별도로 체크하지 않고 있다. 즉 구형 에디터를 쓰는 사람이라면 에러가 날 수도 있다는 뜻.
레퍼런스
- Create Your Own Dynamic Gutenberg Block for WordPress, Part 1 (https://davidyeiser.com/tutorials/wordpress-create-dynamic-gutenberg-block)
- 위 게시물의 소스코드(https://github.com/davidyeiser/detailer/tree/tutorial/part1/blocks/book-details)
- Using withSelect for WordPress Block Components (https://wpdev.life/using-withselect-for-wordpress-block-components/)
- 블록의 카테고리 목록(공식 문서)(https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#category)
- 블록 attribute(공식 문서)( https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/ )
- 대쉬콘 (공식 문서) ( https://developer.wordpress.org/resource/dashicons )
One thought on “워드프레스 동적 블록 만들기 튜토리얼 (Gutenberg)”