[카테고리:] ES Next

ES Next (ES 2015이후), 블록 에디터 관련 자습서.

  • ESNext #07: Redux Hello, World

    Redux는 어플리케이션의 상태 관리를 위한 라이브러리이다. 하도 이 리덕스를 리액트랑 같이 엮어 들었던 탓에 리액트 라이브러리인줄 알았으나, 그렇지는 않더라. 그냥 독립적으로도 사용 가능하다.

    얼마 전에 상태 변화가 빡센 웹 앱을 작성한 프로젝트가 있었는데 이런저런 문제로 인해 jQuery 스타일로 매우 빡빡하게 만들었다. 어떻게는 완료하였으나 Redux를 조금 더 일찍 알았더라면 보다 깔끔하고 완성도 높은 결과물이 나왔을걸 하고 아쉬움이 들었다.

    이번에도 리포지터리에 결과물을 업데이트한다. 물론 Redux는 React를 위한 여러 세련된 방법이 있기는 한데, 나는 그냥 hello, world! 같은 것을 하고 싶을 뿐. 핵심적인 기능만 이용해 단순한 장난감을 하나 만들어 보았다.

    워드프레스에서 작성했으므로 결과물을 보려면 숏코드 wes07을 사용해야 한다. 프로그램은 PHP 스크립트로부터 시작값을 받아 출력한다.

    이 출력값은 +/- 버튼을 눌러 증가, 감소시킬 수 있다. 단순하다. 이 정도면 React.Component 안에서 this.state 객체 안에서 상태 관리를 할 수 있을 정도로 단순하지만, 이번에는 Redux에게 양보해 보자.

    초간단 reducer이다.

    function reducer(state = {value: 0}, action) {
        switch (action.type) {
            case 'wes07/increase':
                return {value: state.value + 1};
    
            case 'wes07/decrease':
                return {value: state.value - 1};
    
            case 'wes07/set':
                return {value: action.value};
    
            default:
                return state;
        }
    }

    그리고 이 reducer로 store를 생성한다.

    import {createStore} from 'redux'
    
    //...
    
    const store = createStore(reducer)

    그리고 component가 mount 완료되었을 때 초기값을 설정하고, 또 store로부터 값을 받아 내부 state에 적용한다.

    class ReduxHelloWorld extends React.Component {
        constructor(props) {
            super(props);
    
            this.state = {
                value: 0
            }
        }
    
        componentDidMount() {
            store.subscribe(() => this.setState({value: store.getState().value}))
            store.dispatch({
                type: 'wes07/set',
                value: this.props.initialValue
            })
        }
    
    // ...
    
    }
  • ESNext #6: Hooks

    훅 기능은 워드프레스에서 구현한 기능이며 일반적인 자바스크립트가 아니지만, 카테고리를 간결하게 유지하고 싶어 이 곳에 작성한다.

    PHP에서 작성된 add_action, add_filter 같은 훅 제어가 JavaScript에서도 아주 유사하게 작성되어 있다! 자바스크립트가 비동기 방식이고, 이벤트 핸들링을 위해 콜백 함수를 쓰는 것이 매우 일상적이긴 하지만, 자바스크립트 고유의 콜백 방식을 쓰기는 조금 난감할 때가 있는데, 이 패키지를 사용하면 문제 없이 훅을 다룰 수 있다.

    리포지터리에 이 혹을 사용해 보는 코드를 작성해 봤다. 아주 간단하게 자바스크립트에서 액션과 필터가 동작하는 코드이다. 기대되는 동작은 기존에 사용하던 PHP 쪽 버전과 동일하게 동작하기 때문에 사용하는 법만 대충 테스트하면 충분할 듯하다.

    예제를 위해 슬러그가 ‘hooks-test’인 포스트를 생성한다. 스크립트는 이 포스트에서만 동작하도록 처리했다. 결과는 아래 그림과 같다.

    액션과 필터는 아래처럼 사용한다.

    addAction('wes06-test-action', 'changwoo/wes06-hooks/index', actionCallback01, 30);
    addFilter('wes06-test-filter', 'changwoo/wes06-hooks/index', filterCallback01);

    다 같은데, 두번째 인자가 네임스페이스라는 점이 다르다. 이 네임스페이스는 hasFilter(), hasAction() 체크시에 사용되는 것 외에는 크게 의미는 없다. 저걸 다르게 준다고 해서 doAction, applyFilters 함수 호출시 특별한 동작을 하게 되는 것은 아니다. 즉 네임스페이스가 다르되, 훅 이름이 같다 하더라도 각 훅이 구분되는 일 따위가 없을 것이다.

    아무튼 이 훅 관련 함수는 이미 코어 여러 부분에 적용되어 있다. 응용하면 자바스크립트에서 그동안 아쉬웠던 훅을 통한 기능 수정이 편안하게 될 수 있으리라 기대한다.

  • ESNext #5: 동적 렌더링과 사이드바에 커스텀 필드 편집

    지난 포스트에서는 블록의 속성값에 대해 간단하게 알아 보았다. 블록이 어떻게 자신의 값을 저장하고 표현하는지 알 수 있었다.

    블록은 HTML 주석 부분에 블록이 필요한 데이터를 JSON 형태로 저장하거나, 자신이 가지고 있는 HTML 마크업 구조에서 가져올 수 있고, 또 커스텀 필드로부터 가져올 수 있다. 단, 블록 자체는 HTML 코드 그대로 기록하기 때문에 이런 동적인 값들을 표현하려면 생짜 HTML 코드를 사용할 수 없을 것이다.

    마크업 구조나 주석에 표현된 JSON을 사용하려면 리액트의 콤포넌트를 활용하거나, 메타 값을 표현하려면 동적인 렌더링을 활용해야 할 것이다.

    이번에는 이런 것들을 표현할 수 있도록 해 보자. 이번에 만들 블록의 요구 조건은 다음과 같이 정리해 볼 수 있을 것이다.

    • 블록은 커스텀 필드의 값을 받아 동적 렌더링을 한다.
    • 블록은 마크업의 요소, 주석의 JSON 데이터를 받아 동적으로 값을 처리한다.
    • 블록의 속성값은 사이드바를 활용할 수 있도록 한다.

    완성된 코드는 Github 레포지터리에 올려두었다.

    예제 설명

    예제는 ‘Dynamic Render’라는 블록을 생성한다. 이 블록은 이전처럼 name과 anotherName 속성을 가지고 Hello, World! 처럼 문자를 출력하는 단순한 역할을 한다. 그러나 이 값은 보다 동적이다.

    name은 따로 텍스트 입력 상자로부터 가져오며, anotherName은 사이드바로부터 온다. 또 name은 주석에 있는 블록 속성에 저장하며 anotherName은 커스텀 필드 _another_name에 저장한다.

    커스텀 블록의 edit() 메소드 결과.
    사이드바에서 Another Name을 편집한다.
    프론트에 그린 것.

    사이드바나 커스텀 블록을 만드는 것은 기존 예제와 크게 다를 것이 없다. 그러나 사이드바에 넣은 값이 메타 필드와 연동되어야만 한다는 점이 이 예제의 키 포인트라고 할 수 있겠다. 그리고 이번에는 save() 메소드는 사실상 아무것도 하지 않는다. 프론트 렌더링을 PHP 측에서 담당하기 때문이다.

    렌더 콜백

    우선 프론트 쪽 렌더링을 살펴보자. PHP 코드에서 register_block_type() 호출시 ‘editor_script’ 뿐 아니라 ‘render_callback’ 또한 지정해 준다.

    register_block_type(
    	'wp-estnext-study/wes05-dynaic-render',
    	[
    		'editor_script'   => 'wes05-dynamic-render',  // 필수!
    		'render_callback' => 'wes05_render_callback', // 동적 렌더링.
    	]
    );

    render callback 함수를 실제 작성해 보니, 이 함수는 관리자 페이지에서 포스트 편집 화면에서도, REST API 호출 때에도, 프론트에서도 다 불리는 것을 확인했다. 단순히 프론트에서 호출할 때만을 상정하고 이 함수를 작성하면 프론트 이외 작업에서 에러가 날 수 있다.

    render_callback을 프론트에서만 호출한다고 생각하고 작성하면 포스트 수정시 다음처럼 오류가 발생한다. 출력 내용이 REST API 응답시 JSON 호출 전에 먼저 불려버려 클라이언트가 JSON 문서 파싱시 에러를 일으키게 한다.

    또한 관리자에서 포스트 편집 화면에 들어갈 때 잠깐의 화면 공백이 발생하는데, 그 때에도 이 출력이 나온다. 그러므로 렌더 콜백은 다음과 같은 if … else … 구문을 사용하여 구분하는 것이 좋을 것이다.

    if ( is_admin() ) {
    	// 관리자 편집 화면 진입시
    } elseif ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
    	// REST API 호출시
    } else {
    	// 프론트 호출은 여기서.
    }

    사이드바 컨트롤과 커스텀 필드 연동

    사이드바에서 TextControl 하나를 넣어 onClick 이벤트와 value 속성을 커스텀 필드의 값과 이어지도록 처리 한다. 그런데 커스텀 필드는 블록 속성이 아니고, 코어가 포스트 편집 화면을 위해 이미 불러와져 있는 상태이다. register_meta() 함수를 부를 때 우리는 ‘show_in_rest’ 항목에 true를 주었기 때문이다.

    그런데 이 커스텀 필드를 불러오거나 업데이트해야 할 때는 조금 복잡하다. 아무튼 현재 수준에서 가능한 설명을 기록한다. 우선 TextControl을 하나 감싸 ‘DynamicRender’라고 하자.

    let DynamicRender = ({anotherName, onChange}) => {
        return (
            <TextControl
                label="Another Name"
                value={anotherName}
                onChange={onChange}
            />
        );
    }

    이제 DynamicRender에 마법을 부려 보자. 메타 값을 불러와 컨트롤의 속성으로 넣어 주려면 아래 코드를 참고한다.

    // withSelect 로 DynamicRender 함수를 래핑.
    DynamicRender = withSelect(select => {
        const {getEditedPostAttribute} = select('core/editor');
        return {
            anotherName: getEditedPostAttribute('meta')['_another_name']
        };
    })(DynamicRender);

    한편 메타 값을 다시 업데이트하려면 아래 코드를 작성한다.

    // withDispatch 로 DynamicRender 함수를 다시 래핑.
    DynamicRender = withDispatch(dispatch => {
        const {editPost} = dispatch('core/editor');
        return {
            onChange(value) {
                editPost({meta: {_another_name: value}});
            }
        }
    })(DynamicRender);

    Redux와 워드프레스 자바스크립트 코어가 겹치는 부분이라 어렵고 이해가 잘 안가는 부분이 있다. 이해하려 해도 파야 할 부분이 많아 보여 현재는 코드 조각 수준밖에는 이해하지 못하는게 불만이다. 보다 이해를 높이려면 리액트나 리덕스를 보다 이해해야만 알 수 있을 것 같다. 보다 시간과 노력이 필요한 부분인듯.

  • 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>

    결과는 아래와 같다.

  • ESNext #2: 블록 제작

    블록과 블록 에디터 환경에서 자유자재로 원하는 기능을 만들고 싶다.

    기존에는 페이지를 제작할때 주로 숏코드를 사용했다. 물론 숏코드는 여전히 유용하고 유효하다. 페이지의 모든 내용이 고정되며 페이지 전반에 어떤 특정 기능을 구현해야 할 때, 그리고 그 때 서버 사이드 스크립트가 더 편리할 때는 그렇게 작성하는 것도 편리하다.

    그러나 엘레멘터 같은 비주얼 빌더들이 워드프레스 제작 환경에는 사실상 표준이 되어가고 있고, 구텐베르크 또한 마찬가지다. 더구나 엘레멘터는 구텐베르크의 블록또한 가져와 사용할 수 있도록 호환성마저 제공하기도 한다. 아무튼 이제는 블록 에디터를 개발에 활용해야 할 때다.

    블록 에디터에 익숙해지기 위해 우선 이 버전의 hello, world! 부터 해 보자. 이것도 이전과 마찬가지로 github 리포지터리에 코드를 커밋해 두었다.

    블록 등록 절차

    커스텀 블록을 사용하려면 크게 두 가지 과정을 거치면 된다. 코어에 블록을 등록한다. 그리고 그 블록이 어떻게 동작할지 정의한다. 각각의 역할은 PHP와 JS 에서 나눠 구현된다.

    대략 API 함수를 보면 PHP 측에서 등록은 자바스크립트 의존성, 인큐잉의 순서 같은 올바른 스크립트 삽입을 위한 코드인 것 같다. 반면 JS 는 등록된 커스텀 블록이 에디터에서, 프론트엔드에서 어떤 동작을 취하는지에 대한 동작을 상세하게 지정하는 성격이 강하다. 뭐, 사실 당연한 말인 것 같다.

    보통 워드프레스 스크립트를 등록(register), 인큐(enqueue) 처리할 때 ‘init’, ‘wp_enqueue_scripts’, ‘admin_enqueue_scripts’ 세 훅을 사용한다. 한편 블록 에디터를 위한 ‘enqueue_block_editor_assets’ 훅이 추가되었다. 이 훅은 ‘init’ 보다는 뒤에, 그러나 ‘wp_enqueue_scripts’ 보다는 앞에 불릴 것이다. 보통 예제에서는 init을 많이 사용하는데 상황에 따라 적절히 선택하면 될 것 같다.

    PHP 사이드에서 블록 등록 절차

    1. wp_register_script()를 호출해 블록을 등록하는 스크립트를 등록 처리한다.
      • 이 때 빌드된 디렉토리 쪽에 *.asset.php 파일이 있을 것이다. 이 파일은 하나의 배열을 리턴한다. 이 배열에는 해당 스크립트이 의존성 목록과 버전 정보가 담겨있다.
      • wp-script가 작성된 코드를 분석해 적절히 의존성을 결정해준 것이다.
      • 버전은 매번 변경되는 코드가 캐싱되지 않도록 하는 목적이 크다.
    2. register_block_type() 함수를 사용해 블록을 코어에 등록시킨다.
      • 첫번째 인자로 쓰는 블록 이름은 소문자로만, 케밥 케이스로, 또 ‘{네임스페이스}/{블록 이름}‘ 식으로 작명하도록 한다. 네임스페이스는 플러그인 고유의 구분자로, 블록 이름은 블록의 구분자로 생각할 수 있다.
      • 두번째 인자에 배열이 들어가는데 ‘editor_script’는 반드시 지정한다. 이 값으로 1단계에서 등록한 스크립트의 핸들을 입력한다.
    3. 추가적으로 번역 파일이 있다면 wp_set_script_translations() 함수를 사용해 등록한다.
      • 번역 파일의 이름은 {텍스트도메인}-{언어 코드}-{핸들}.json 이다. 혹은 {텍스트도메인}-{언어 코드}-{해시}.json 로도 사용할 수 있다.
      • 첫번째 인자는 스크립트 핸들, 두번째는 텍스트 도메인, 세번째는 번역 파일이 있는 디렉토리의 절대경로이다.
      • .json 파일을 생성하는 것이 고역일 수 있다. 그러나 이후 설명할 WP CLI를 사용하면 어렵지 않다.

    JS 사이드에서 블록 등록 절차

    아주 기본적인 함수를 불러오는 것(import … from …)부터 시작하자.

    • ‘@wordpress/blocks’ 패키지에서 ‘registerBlockType’ 함수
    • ‘@wordpress/i18n’에서 번역을 위한 ‘__’ 함수를 불러온다.

    이 두 패키지를 devDependencies에 등록시키자. 그리고 가져온 registerBlockType 함수를 호출하면 된다.

    • 첫번째 인자는 이 블록의 이름이다. PHP의 register_block_type() 함수에서 썼던 것과 동일한 이름, 즉 ‘{네임스페이스}/{블록 이름}‘을 입력해야 한다.
    • 두번째 인자는 이 블록이 어떤 속성을 가지는지 상세하게 정의하는 부분이다. 이 부분은 많은 설명이 필요하며 여기서는 다 다룰 수가 없다. 차후 개발 문서를 자주 방문하면서 숙련도를 쌓아야 할 것 같다.
    • 예제를 위해 사용한 속성은 총 5개인데 아래에서 간단히 설명한다.
      • title: 블록의 제목. 문자열.
      • description: 블록의 상세 설명. 문자열.
      • category: 어떤 분류에 속하는지 지정한다. 텍스트, 미디어, 임베드 등등이 있다. 문자열.
      • edit: 콜백 함수. 에디터 화면에서 어떻게 그려져야 할지를 지정한다.
      • save: 저장된 후 프론트에서 어떻게 그려져야 할지를 결정한다.

    안녕, 세상!을 위해 만든 간단한 블록은 아래 코드처럼 처리했다.

    registerBlockType('wp-esnext-study/wes02-hello-world', {
        title: __('WP ES Next #2 Hello, World!', 'wp-esnext-study'),
        description: __('"Hello, World!" sample for block editor.', 'wp-esnext-study'),
        category: 'common',
        edit() {
            /* 이 블록이 관리자 편집시 보이는 내용입니다. */
            return (
                <p>Hello, World!</p>
            )
        },
        save() {
            /* 이 블록이 저장될 때 내용입니다. */
            const helloText = __('WP ES Next #2 Hello, World!', 'wp-esnext-study');
            return (
                <p>{helloText}</p>
            )
        }
    });

    관리자 페이지에서 보면 이렇게 나온다.

    이것을 프론트에서 보면 이렇게 나온다.

    번역문 처리

    번역 파일을 로드하려면 주로 ‘init’ 훅의 콜백에서 다음처럼 코드를 작성한다.

    PHP에서 번역문 로드

    load_plugin_textdomain( 'wp-esnext-study', false, wp_basename( dirname( __DIR__ ) ) . '/languages' );
    • 첫번째 인자는 번역 텍스트도메인.
    • 둘째는 사용하지 않는다.
    • 셋째는 번역 파일이 있는 곳의 디렉토리. wp-content/plugins 디렉토리로부터 상대 경로이다. 주로 wp_basename( dirname( plugin_dir_path( MAIN_FILE ) ) ) . '/languages' 식으로 입력하곤 한다.

    pot 파일 생성

    poedit 같은 프로그램을 통해 디렉토리를 분석해 po 파일을 생성하는 방법도 있지만, 이렇게 하면 플러그인의 헤더 부분이 제대로 번역되지 않는다. 플러그인 이름, 제작자 같은 플러그인의 헤더 정보까지 올바르게 번역하려면 WP CLI i18n 커맨드를 이용하는 것이 좋다. 커맨드는 플러그인 루트에서 아래처럼 입력한다.

    mkdir languages # languages 디렉토리가 없다면 입력.
    wp i18n make-pot . languages/{텍스트도메인}.pot --domain={텍스트도메인}

    그러면 languages 디렉토리에 텍스트도메인으로된 .pot 파일이 생성될 것이다. 여기에 JS, PHP에서 사용한 모든 번역문이 담겨있을 것이다.

    이 POT 파일을 가져다 languages/{텍스트도메인}-{언어코드}.po 파일을 작성해야 한다. 보통 poedit 같은 프로그램을 이용한다. 내 예제를 보면 languages/wp-esnext-study-ko_KR.po 파일이 생성된 것을 알 수 있다.

    poedit을 사용하면 .po 파일과 함께 컴파일된 .mo 파일도 생성할 수 있다. PHP에서는 결국 이 .mo 파일을 이용하여 번역 텍스트를 처리할 것이다.

    번역문 JSON 생성

    그런데 .mo 파일의 번역은 PHP로 작성한 코드에서만 유효하다. 자바스크립트에서 번역문이 올바르게 처리되려면 .mo 파일이 아닌, .json 파일을 사용해야 한다. 이것은 pot 때와 마찬가지로 WP CLI를 사용하면 보다 편리하게 작업 가능하다.

    JSON 생성을 위해 번역을 두 벌 해야 할 필요가 없다. JSON 번역 파일은 .po 파일을 기본으로 하여 작성되기 때문이다. 그러므로 우선 .po 파일의 번역을 끝마친 후에 .json 파일을 생성해야 할 것이다.

    명령은 플러그인의 루트 디렉토리에서 아래처럼 실행한다.

    wp i18n make-json languages

    이 명령을 실행하면 .po 파일로부터 JS 쪽의 번역문만 따로 뽑아내 .json으로 만든다. 이후 .po 파일에서는 JS 쪽 번역문은 사라지게 된다. 이런 파일의 수정을 일어나지 않게 하려면 뒤에 --no-purge 옵션을 더해 주면 된다.

    번역 JSON 파일의 해시값

    생성된 JSON 파일의 이름을 보면 {텍스트도메인}-{언어}-{해시}.json으로 되어 있다. 워드프레스는 이보다 {텍스트도메인}-{언어}-{핸들}.json을 먼저 읽기는 한다. 그러나 WP CLI i18n make-json 명령으로는 핸들로 된 파일을 만들지 않고 해시로 된 파일을 만든다.

    그런데 의문점이 있다. 해시는 어떻게 생성하는가? 그리고 해당 명령으로 만들면 파일은 2개가 생성된다. 이것은 왜 그러는가?

    우선 파일이 2개 생성되는 이유를 살펴보자. 파일의 JSON 구조를 보면 “source” 키의 값으로 “build/index.js”, “src/index.js” 각각 두개로 나눠져 있는 것을 알 수 있다. 그 외에 두 파일의 내용은 동일하다.

    그리고 src/index.js와 build/index.js 각각의 md5 해시값이 뒤에 붙어 다른 파일이 2개 생성되는 것이다. 이렇게 되는 이유는 pot 파일에서 찾을 수 있다. 아래는 pot 파일의 일부이다.

    #: 02-block-editor-hello-world/build/index.js:115
    #: 02-block-editor-hello-world/src/index.js:11
    msgid "\"Hello, World!\" sample for block editor."
    msgstr ""

    #은 주석인데, 해당 소스 코드 어디에서 이 문자열이 발견되었는지를 기록한다. js 파일에 대해 src와 build 두 곳에서 문자열을 가지고 오게 된다. 그리고 저 위치는 .json 파일을 만들때 ‘source’ 키에 사용하게 된다. 또한 이 ‘source’ 의 값을 md5 해시로 돌려 보면 해당 파일의 이름으로 된 해시값과 동일한 결과가 나옴을 알 수 있다.

    각 md5 해시 값의 결과는 아래와 같다. 만들어진 json 파일의 결과와 비교해 보면 서로 동일하다는 것을 알 수 있다.

    • 02-block-editor-hello-world/src/index.js – 8fd9b2d6decbdc39f7e175586b1ca3a2
    • 02-block-editor-hello-world/build/index.js – e3c4e88d8ce2f5a27f6d3f9da2c228e6

    이런 이유로 language 디렉토리는 플러그인 디렉토리 바로 밑에서 작성하고, 번역문은 플러그인 전역에서 처리하는 것이 좋다. 그렇지 않으면 해시값에 대해 따로 처리를 해 주어야 할 것이다.