워드프레스 동적 블록 만들기 튜토리얼 (Gutenberg)


개요

기본 개념

이 글은 독자들이 워드프레스의 기본적인 작동 원리를 안다는 가정하게 작성되었다. 어쨌거나 직접 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 요청도 우리가 필요할 때만 해도 되고, 왠만한 경우는 워드프레스에서 제공해주는 자바스크립트 라이브러리가 일을 다 한다.

graph TD subgraph 프론트엔드 B["유저(브라우저)"] B --> C[자바스크립트] C --> B end subgraph 백엔드 c["워드프레스 (php)"] c --> |완성된 페이지|B B --> |요청|c end

옛날의 워드프레스

graph TD subgraph 프론트엔드 B["유저(브라우저)"] B --> |유저 행동|C[자바스크립트] C --> |정보 실시간 갱신|B end C --> |REST API를 통해<br>필요한 정보 요청<br>및 정보 갱신|c c --> |정보 전달|C subgraph 백엔드 c["워드프레스 (php)"] c -->|기본 HTML| B B -->|페이지 요청| c end

요즘의 워드프레스

계속되는 추세가 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>

구현

graph TD a["에디터가 로딩된다"] --> b["init 후크가 실행되며 <br>recent-posts-block.js<br>파일도 로딩된다."] b --> c[js 파일의 registerBlockType이<br>실행되고 포스트 내용에 있는 블록 데이터가<br>적절하게 파싱된다. ] c -.- ca[파싱 방법은 js 파일의<br>registerBlockType의 attributes<br>항목 설정된 것을 기반으로 결정한다.] c --> d[js의 edit함수에서 주어진<br>데이터를 바탕으로 에디터에서<br>어떻게 보여질지를 결정한다.] d-->e[변경된 내용이 저장된다.<br>save 함수가 없으므로<br>기본적인 형태로 포스트 내용에<br>attribute를 모두 포함시켜 저장한다. ] j["실제 페이지 요청"]-->k["init 후크가 실행되며<br>php의 register_block_type도 실행된다."] k-->l[포스트 내용에 있는<br>블록 데이터를 적절하게 파싱하여<br>php의 render 함수로 넘긴다.] l-->m[render 함수의 return 값과,<br>기타 등등을 조합하여<br>html을 생성하고<br>유저에게 내보낸다.]

대락적인 흐름도

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라고 하며, 블록과 관련된 정보가 담겨있다. editwp.element.createElement 함수의 호출 결과를 반환하여야 한다. 이 특별한 함수는 위의 예제에서 el이라는 약칭으로 하여 계속 호출하고 있다. 이 함수에 대한 자세한 설명은 아래에서 계속한다. 어쨌거나 저쨌거나 edit은 다음과 같은 형태를 보일 것이다.
registerBlockType('...', {
    ...
    edit: function(props) {
        ...
        return el(
            ....
        );
    },
    ...
});
  • save : 블록이 실제로 어떻게 보여질지를 결정한다. edit 함수와 마찬가지로 props를 인수로 받고 값 하나를 리턴한다. 이 리턴값은 편집이 끝나는 시점에 데이터베이스의 포스트 내용에 특정한 형태로 저장된다. (이 글 하단 참조)

attributes 설정

다음과 같은 기본 구조이다.


attributes: {
  속성1: {
    type: string
    source: ...,
    selector: ...,
    default: ...,
    ...
  },
  속성2: {
    ...
  }
}

속성 이름은 그냥 키 값으로 설정하면 된다. 속성을 저장하는 방식과 읽는 방식을 결정하는 source, selector 등은 이 글에서 다루지 않는다. 그냥 typestring으로 설정하고 default로 기본 값을 설정하자. attribute를 올바르게 설정했다면 edit 함수에서 props.attributes.속성1 과 같이 접근할 수 있다.


withSelect

사실 잘 모른다. 여기서는 워드프레스에게 정보를 요청하고, 그 정보를 props에 저장시키는 역할로 withSelect를 사용했다. withSelectedit의 래퍼 함수로 볼 수 있고, 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)
    })
콘솔에 나타난 결과

selectgetEntityRecords나 이런 함수 안에 들어갈 인자가 무엇이냐에 대한 설명이 참으로 찾기 힘들다. 구글링과 여러가지 시도를 통해 대략적으로 어떤 정보를 가져올 수 있는지 테스트해보았다. 아래는 select("core").getEntityRecords(...)에 들어갈 인수에 따른 데이터이다. 이 방법들은 모든 것들을 다 긁어오므로 일부만 가져오려면 세 번째 인수에 쿼리를 추가해야 한다. 그 방법들은 연구가 다소 필요하므로 이 글에서는 적지 않겠다. (레퍼런스 참조)

첫 번째 인수두 번째 인수결과
postTypepost글 목록
postType wp_block아무 것도 안나옴
postType page페이지 목록
postType attachment미디어 목록
taxonomycategory카테고리 목록
taxonomypost_tag태그 목록
rootmedia미디어 목록
(‘postType’, ‘attachment’와 동일)
rootpostType글 타입 목록

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 : 에디터의 본 화면에서 텍스트를 적을 수 있도록 한다.

보면 InspectorControlsRichText가 동일한 배열에 있는 것을 확인할 수 있는데, 저렇게 해놓기만 해도 InspectorControls는 사이드바 자리에, 그리고 RichText는 에디터의 본래 편집 자리에 각각 잘 위치하게 된다.

유의하여야 할 점은 요소마다 value, onChange 등을 적절하게 잘 설정하여야 현재의 값을 잘 표시하고 변경될 값을 무사히 적용시킬 수 있다.


save를 안쓰는 이유

save는 이 글이 어떻게 외부로 보여질지 결정한다고 했는데, 이 글에서는 save를 만들지 않는다. 왜냐하면 서버사이드에서 렌더링하기 때문이다. 우리가 자바스크립트 단에서 하는 역할은 값을 저장할 attribute 들을 설정하고 그 attribute 들을 수정할 수 있는 에디터 ui를 만드는 것이다. 이 값들을 조합하여 실제 페이지로 만드는 건 php로 역할을 넘긴다.


functions.php

워드프레스에서는 사용자가 작성한 코드를 워드프레스의 실행 흐름 속으로 녹이기 위하여 filter, action등의 개념을 만들고, 그러한 filteraction이 실행되는 후크를 사전에 세팅해두었다. 이 예제에서는 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_callbackezkorry_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 함수가 정의가 되어있지 않거나 nullreturn해야 한다. save 함수는 저장 과정에서 작동하므로 나중에 php가 저장된 데이터를 읽을 때 제대로 작동하지 않을 여지가 크다.
  • js에서, 해당 attribute에서 source가 정의되지 않고 selector가 정의되어 있어야 한다. source가 정의되어 있지 않아야 php에서 잘 읽히더라. selector의 역할은 사실 잘 모르겠다. 본래 css 실렉터처럼 실제 저장된 데이터에서 데이터를 뽑아낼 때 쓰는 건데, (예를 들어 sourceattribute로 하고 selectorhref로 하면 어떤 엘리먼트의 href 속성 데이터를 불러온다.) 실제 데이터 저장된 것을 보나 php의 render 함수에서 보나 selector는 찾아볼 수 없었다.
  • js의 해당 attribute에서 tpyestring이어야 한다. 공식 문서를 살펴보면 지원되는 typenull, boolean, object, array, number, string, integer로 굉장히 다양하다. category id가 숫자라서 integer를 썼는데 제대로 작동하지 않아 도대체 원인이 무엇인가 별 쓸 데 없는 짓을 다 해보고 이 typestring으로 고쳐보니 문제 없이 작동되었다. 원인은 잘 모르겠다. 서버사이드와 짝짜꿍 하려면 마음 편하게 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를 사용 중인지 아닌지 별도로 체크하지 않고 있다. 즉 구형 에디터를 쓰는 사람이라면 에러가 날 수도 있다는 뜻.

레퍼런스

One thought on “워드프레스 동적 블록 만들기 튜토리얼 (Gutenberg)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Scroll to top