[작성자:] changwoo

  • 혼잣말

    나는 귀찮은 것이 싫다. 뭐든 대충대충 하는 것이 좋다.

  • 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로 저장된 값을 가져와 표시하려면 지금처럼 정적인 렌더링은 알맞지 않다. 이것은 차후 동적 렌더링에서 따로 알아보도록 하자.

  • 최근 N 개의 파일을 남기고 모두 삭제

    아래처럼 실행:

    ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --
    # 이 예제는 최근 5개를 남기고 삭제. N+1을 입력해야 함.

    출처: Stack Overflow

  • 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은 이런 흐름을 생각했을 것입니다.

    1. 사용자가 포스트 편집을 위해 관리자 화면에서 영어를 선택합니다.
    2. 관리자 화면에 표시되는 언어가 영어로 표시됩니다. 또 포스트 목록에는 언어가 영어인 포스트가 나옵니다.
    3. 이렇게 되면 쿠키에 기록된 언어가 모두 영어로 기록되므로, 태그 검색, 작성 때에는 모두 영어로 번역된 태그를 검색할 것입니다.

    그러나 문제는 아래처럼 작업하지 않을 때 발생합니다.

    1. 사용자는 언어를 한국어로 선택한 상태(즉, 쿠키에 저장된 값은 ‘ko’)입니다.
    2. 동료로부터 영어로 번역된 포스트의 점검을 위해 영어로 작성된 포스트 편집 URL을 전달받았습니다. 이때 불행히도 lang 파라미터가 누락되어 있었습니다.
    3. 대시보드의 언어는 한국어지만, URL을 통해 직접 접근하였고 lang 파라미터가 누락되어 쿠키의 값은 ‘ko’가 그대로 변하지 않습니다.
    4. WPML은 이 때 쿠키 값을 변경하지 않습니다. 그리고 관리자 표시 화면은 어떨까요? 포스트 편집 화면으로 들어왔고, 포스트의 언어는 영어이므로 영어로 표시가 될 겁니다.
    5. 그리고 태그 검색시 쿠키 값이 ‘ko’이므로 태그 검색은 한국어 태그에서 검색을 할 겁니다.
    6. 빰! 버그 발생. 포스트 언어는 영어인데, 태그는 한국어 태그가 달리는 현상이 발생합니다.

    사용자가 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;
                }
            }
        }
    );
  • 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 디렉토리는 플러그인 디렉토리 바로 밑에서 작성하고, 번역문은 플러그인 전역에서 처리하는 것이 좋다. 그렇지 않으면 해시값에 대해 따로 처리를 해 주어야 할 것이다.

  • ESNext #1: 리액트 사용

    블록 에디터가 워드프레스 생태계에 주는 여파는 꽤 크다. 단순히 쓰기 편한 새로운 에디터가 등장한 것 뿐만이 닐 것이다. 이제 새로운 프론트엔드 개발 기법들을 적극적으로 반영해야 한다.

    리액트는 새로운 워드프레스 코어 코드의 중요한 의존성이 되었다. 물론 필수는 아니겠지만 개발자는 보다 리액트를 잘 이해해야 할 필요가 있다. 아마 꽤 오랫동안은 그 영향력이 유지되겠지만 점차 jQuery는 폐기될 것이다. 정말 많은 요소들이 변화하고 있으며, 나 같은 개발자는 따라가기 벅차다.

    이번 기회에 기존의 기술과 대두되는 새로운 기술들을 어떻게 사용해야 할지 좀 정리해야 할 필요가 있다고 생각했다. 새로운 기술들은 아직 사용하기 익숙치 않아 계속 실무에 투입하기 어려웠다. 하지만 이제 점점 사용해야 하지 않으면 안 될 시기가 온 것 같다. 고객들도 점점 구텐베르크를 위시한 새로운 프론트엔드 개발에 대한 사용 경험을 쌓아 왔고, 나도 그에 맞추지 않으면 안 된다고 생각한다.

    우선 블럭 에디터나 리액트를 플러그인 개발에서 사용하기 위한 절차? 방법들에 대해 정리해보자. 대략 알고 있지만, 숙련도가 높지 않은 탓에 자연스럽게 되지 않는다.

    플러그인 기본 셋업

    기존과 동일하게 플러그인 메인 파일에 헤더를 생성한다. 이러면 코어는 플러그인을 인식한다. 그러나 이제는 자바스크립트 기반의 개발 환경을 위해 package.json 파일의 추가를 반드시 해 줘야 한다.

    package.json은 NPM으로 가능하지만, 나는 yarn을 더 선호한다. 혹시 개발하는 시스템에 yarn이 설치되지 않았다면 yarn 설치하기 페이지를 보고 우선 진행하기 바란다.

    Yarn이나 NPM이 설치되었으며 ‘npm init’ 혹은 ‘yarn init’으로 package.json을 시작하자. package.json은 단순한 JSON 파일이므로 스키마를 잘 안다면 수동으로 만들어도 무방하다.

    아래 JSON 일부분은 라이센스와 기본 빌드 스크립트를 지정하는 예제이다. 아마 이 셋업은 자주 반복될 것이다.

    {
      ...
      "scripts": {
        "start": "wp-scripts start",
        "build": "wp-scripts build"
      },
      ...
      "license": "GPL-2.0-or-later"
    }

    @wordpress/scripts 설치

    솔직히 내가 프론트엔드 프레임워크에서 매우 짜증내는 부분 중 하나이다. 바로 과도한 설정 홍수. 프론트를 위한 온갖 잡다한 부분이 package.json을 위시한 별별 js 파일 설정에 뒤범벅이 되어 있는 것. 나도 한 사람의 개발하는 사람으로서 그런 이유와 결과에 대해 이해하지만, 내 개인적으로는 덕지덕지 붙어있는 이런 잡다한 설정 덩어리들은 괴물같아 싫다.

    물론 더욱 정밀한 설정, 고급 설정을 하려면 그런 세세한 설정이 없으면 안되는 것은 알지만… 적어도 나한테는 프론트엔드 개발에 큰 장벽이 된다.

    그래도 좀 다행이다. 이러한 개발 설정들은 한 번 잘 설정해 두면 잘 복사해서 재사용할 수 있다는 점이고, 워드프레스 코어 개발자들은 아예 그 재사용성을 활용해 그러한 설정마저 하나의 패키지로 묶어 놨다는 사실. 보다 세세한 설정에 눈뜨게 될 때 까지는 훌륭한 조력자가 될 것이다.

    이 조력자는 yarn add --dev @wordpress/scripts라는 한 줄의 명령어로 쉽게 설치된다. 그리고 위 package.json 의 scripts 부분에 미리 설정을 해 두었으니 npm start, npm build 명령과 연계하여 사용하면 된다. 보다 복잡한 설정은 문서를 참고하면 된다.

    리액트로 Hello, World! 시작하기

    블록 에디터를 위한 커스텀 블록 생성 전에 먼저 그냥 먼저 리액트를 사용해 보자. 최초의 예제는 언제나 ‘Hello, World!’니까 그것부터 시작해 보자.

    예제는 github에 작성했고, 앞으로도 거기에 내용을 업데이트할 예정이다. 코드가 워낙 간단하니 디테일한 설명은 생략한다. 자세한 설명은 소스 코드에 주석으로 대신한다. 여기서는 중요한 포인트 몇가지만 기술하고자 한다.

    • 리액트가 프론트엔드 부분의 마크업까지 작성하게 되므로, PHP 코드 자체에서는 그다지 많은 할 일이 없다. PHP 코드는 껍데기라고 느껴질 정도로.
    • 중요. 보통 ‘depencencies’에 ‘react’, ‘react-dom’를 넣게 된다. 그러나 워드프레스에서는 ‘devDependencies’에 둘을 넣는다. 워드프레스 코어 자체에 리액트가 내장되어 있고 wp-script가 서로 다른 버전의 리액트가 중구난방하지 않도록 제어하기 때문이다. 물론 스크립트가 알아서 잘 처리하니 그냥 dependencies에 넣어도 문제가 되지는 않는다.
    • 리액트 뿐 아니라 블록 에디터를 위해 만들어진 여러 패키지, 예를 들어 UI 요소들 같은 노드 패키지들도 코어에 이미 지정되어 있다면 코어의 것을 쓰지, node_modules로 임의 설치된 것을 사용치 않는다. 즉, 패키지 설치는 단지 코드를 참조하기 위함으로 알아두자.

  • 워드프레스 코어 #3

    2020년 08월 04일 네번째 모임 발표 자료

    워드프레스 데이터베이스와 회원 로그인에 대한 자료입니다.

    슬라이드 링크

  • 워드프레스 코어 #2

    지난 시간에 이어 코어 두 번째 이야기를 준비했습니다. 이번에는 wp() 함수에서 일어나는 라우팅에 대해 알아봅니다.

    URL을 입력하면 각 주소에 따라 서로 다른 콘텐츠가 나옵니다. 어떤 주소로는 단일 포스트 세부 내용이 나오고, 또다른 주소로는 아카이브 페이지가, RSS 피드가 나옵니다. 이게 어떻게 가능할까요? 이 원리를 간단히 알아봤습니다.

    슬라이드 보기

  • 워드프레스 코어 #1

    2020년 7월 2일 두번째 모임 발표

    워드프레스가 사용자로부터 요청을 받으면, 응답을 보내기까지 어떤 일들이 일어나는지 간략하게 알아본 자료입니다.

    슬라이드 링크

  • 워드프레스 개발자 관련 커뮤니티를 만들고 싶습니다

    강력한 워드프레스 개발자 커뮤니티를 구축하고 싶습니다.

    워드프레스 개발자니까 기반은 워드프레스로 작성하고 싶습니다. 탄탄한 사이트를 원합니다. 그래서 테마와 플러그인은 직접 개발하고 싶습니다. 개발자끼리 정보를 공유하고 개발 관련 이슈를 많이 논하는 커뮤니티로 만들고 싶습니다.

  • 내가 CPT UI나 ACF를 싫어하는 이유

    CPT UI의 문제점

    CPT UI를 쓰면 커스텀 포스트나 택소노미를 빠르게 생성할 수 있다. 하지만 빠른 것이 무조건 좋은 것만은 아니다. 콘텐츠의 성격이 커스텀 포스트로 잘 반영되어야 한다. 즉 커스텀 포스트 생성시 여러 속성을 통해 조절해야만 하지만 UI로 접근하는 사람들은 그런 디테일을 잘 고려하지 않는다.

    다만 CPT UI에서는 충분히 그런 디테일을 제공한다. 누차 얘기하지만 툴을 비난하는 것이 아니다. 나는 쓰는 사람들의 무심함을 꼬집어 이야기하고 싶은 것이다. 대개 이 플러그인을 쓰는 사람들은 디테일에 지나치게 무심하다.

    이름, 여러 레이블, 공개 여부, 쿼리 변수나 다시쓰기 슬러그, 위계성, 편집 화면의 지원 요소, 그리고 RSS 피드 제공 여부… 많은 요소들이 있지 않은가?

    한편 택소노미와 포스트 타입 생성을 위한 파라미터들은 정형화되어 있으므로 적절히 샘플 코드 정도로 을 만들어 두면 된다. 조금 길어 보일 수는 있어도 콘텐츠에 대한 상세한 고려를 위해서는 이렇게 하는 것이 좋다. 또 다르지만 비슷한 타입들도 굳이 UI를 통해 일일이 클릭하는 것보다는 코드로 빠르게 복제하는 것이 능률적이다. 굳이 플러그인까지 도입해서 처리해야 만들어야 하는 건지?

    ACF의 문제점

    ACF는 CPT UI보다 문제가 심각하다. ACF를 사용하면 나름 깔끔한 관리자 UI를 생성할 수 있다. 그러나 쉽고 편한 것이 반드시 좋다고 말하기는 어렵다. 사이트의 기능이 많고 복잡할수록 ACF는 그 복잡합을 더 증가시킨다.

    ACF의 가장 나쁜 점은 코드 곳곳에 흐름의 공백을 만들어낸다는 점이다. 개발자는 코드를 보면서, 코드의 흐름을 따라가면서 프로그램을 완성시킨다. ACF가 생성하는 커스텀 필드는 그 실체가 모두 데이터베이스에 저장되어 있다. 그래서 커스텀 필드를 이해하려면 관리자 페이지에서 ACF UI를 보면서 그 설계를 이해해야만 한다. 코드를 읽다가 UI로 넘어가는 일이 잦다는 점. 사이트가 커지면 커질 수록 이 문제는 점차 감당하기 쉽지 않게 된다.

    또한 데이터와 UI가 ACF에서 강하게 결합되어 버린다. 대개 ACF의 유연한 옵션 때문에 일반적으로는 나름 괜찮은 UI로 결론이 나지만, 때때로 복잡하거나 특정한 상황에서는 좀 더 효율적인 형태로 만들어져야 할 때가 있다. 그럴때 ACF는 개발자의 창의력을 붙잡아버린다. 더 이상의 무엇이 나오지 않는 것이다.

    뭐가 불만이었니?

    CPT UI, ACF(Advnced Custom Field)는 워드프레스 개발하면서 많이 찾는 플러그인이고, 인기 있는 플러그인이다. 둘을 쓰면 쉽고 빠른 제작에 도움이 된다.

    그러나 그럼에도 불구하고 나는 CPT UI, ACF를 좋아하지 않는다. 아, 그러나 오해는 없기를 바란다. 나는 그 두 플러그인 제작자에 대한 폄하는 추호도 없다. 내 호불호를 떠나 객관적으로 이 둘은 멋진 플러그인이다. 진심으로 나도 이런 플러그인을 만들면 좋겠다. 비유를 들면 CPT UI, ACF는 마치 칼 같은 그저 도구라는 것. 이 칼들에 그렇게 심각한 문제가 있다고는 생각하지 않는다. 다만 이 칼이 너무나 무분별하게 남용하고 있다는 것.

    이들은 작은 웹사이트, 혹은 사이트의 소유자가 직접 관리하면서 개발하려는 의지가 있을 때 엄청난 축복이다. 그러나 웹사이트를 전문적으로 제작하려는 사람, 좀 더 고도화된 기능을 개발하려는 사람에게는 이들은 좋은 옵션이 될 수 없다고 생각한다. 불행하게도 나는 후자에 있고, 후자의 시선으로 판단하기에 이런 의견을 블로그에 포스팅하는 것이다.

  • Astra 테마 분석 노트

    아스트라 테마 특징

    • 장점: 엄청나게 많은 부분에 디테일하게 액션과 필터가 정의되어 있어 커스터마이즈 할 수 있는 여지가 풍부하다.
    • 단점: 부분부분 디테일하게 1:1 파트별로 커스터마이즈가 가능하나 지나치면 너무 디테일한 것이 독이 되어 나중에 돌아가는 것 파악이 어려움. 사실 일반적인 테마 커스텀들의 문제점이긴 하지만. 아스트라는 액션 필터가 너무 세밀해서 이 문제가 부각된다.

    아스트라 테마 동작 포인트 노트

    제작자들이 커스터마이즈를 염두에 두고 제작하여 여러 부분들이 흩어져 있다. 이 흐름을 잘 파악하지 않으면 커스터마이즈 개발시 갈피를 잡기 매우 어렵다. 아스트라 테마를 사용한 커스터마이즈시 이 노트를 보고 주요 흐름을 복기하는 것이 중요할 것으로 보인다.

    주요 페이지 진입점은 테마의 기본인 archive.php, index.php, page.php, single.php 이며 여기서부터 테마의 흐름이 시작된다.

    index.php, single.php, page.php 셋은 거의 동일한 구조이다. 단, page.php 의 본 콘텐트 함수는 astra_content_page_loop() 으로 조금 다르다.

    메인 루프를 돌기 전후에 위치한 ‘astra_primary_content_top’, ‘astra_primary_content_bottom’ (액션 태그 이름과 콜백 함수 이름이 동일하다) 두 액션은 커스터마이징 전용으로 보인다. 부모에서는 이 부분에 별도로 정의하는 액션 콜백이 없다.

    astra_content_loop(), astra_content_page_loop() 흐름 노트

    1. 메인 콘텐트를 출력하는 부분인 astra_content_loop, astra_content_page_loop 액션에서 보다 복잡한 레이아웃과 구조를 따라 분기하게 된다. 여기서 테마 UI 설정에서 적용된 여러 옵션을 따라 가게 될 것이다.

    2. astra_content_loop, astra_content_page_loop 둘 다 기본으로 ‘Astra_Loop’이라는 루프 전용 클래스에서 콜백 처리한다. 두 함수 다 Astra_Loop::loop_markup( $is_page ) 함수를 호출한다. 페이지면 $is_page = true 인 차이점 뿐이다.

    3. Astra_Loop::loop_markup() 에서 일괄적으로 main#main 태그를 출력한다. 그리고 페이지/비-페이지에 따라 다른 템플릿 조각을 부른다.

    페이지:
    Astra_Loop::template_parts_page().
    –> template-parts/content-page.php.

    비-페이지:
    Astra_Loop::template_parts_post().
    –> template-parts/content-single.php.

    4. content-*.php 에서 article#post-<post-ID> 태그 출력됨.

    페이지: template-parts/content-page.php 내에서 the_content() 함수가 호출되면서 메인 내용 출력.

    비-페이지:
    astra_entry_content_single() 호출.
    astra_entry_content_single_template() 호출.
    –> template-parts/single/single-layout.php.

    5. the_content() 앞뒤로 astra_entry_content_{before,after}() 호출이 있는데, 이와 관련된 콜백은 아스트라 테마 자체에서는 발견되지 않는다. 커스터마이즈용으로 판단한다.

    기타 사항

    template-parts/blog/blog-layout.php 라는 부분이 있는데, 이것은 검색용 템플릿이다.

    메인 루프를 분석하는 핵심인 클래스는 astra/inc/class-astra-loop.php 에 있는 Astra_Loop 이다. 말하자면 여기가 공략 포인트다.


  • 워드프레스 코어 한 벌로 여러 싱글 사이트 세팅하기

    워드프레스는 멀티사이트로 운용할 수 있다. 한 벌의 코어로 여러 사이트를 제작할 수 있다. 이 때 site1.example.com 같은 서브도메인이나 아니면 example.com/site1 처럼 패스로 각 사이트를 생성할 수 있으며, 약간의 수정만 거치면 각 사이트마다 독자적인 도메인 설정도 가능하다.

    그러나 이 방법은 한 코어에 wp-config.php와 플러그인, 테마, 그리고 일부 DB 내용을 공유하게 된다. 즉, 완전히 분리된 사이트를 운용하게 되는 것은 아닌 것이다.

    이 포스트에서는 워드프레스 싱글 사이트를 기반으로 하되 완전히 각자의 영역을 독자적으로 가지는 셋팅법을 적고자 한다. 사실 독자적인 셋팅을 가져가므로 설정에 의해 각 사이트는 싱글 사이트일 수도 있고 멀티사이트가 될 수도 있다. 단, 코어와 코어에 위치한 .htaccess 때문에 싱글사이트 전용, 혹은 멀티사이트 전용으로만 운용 가능하다. 도메인별로 .htaccess 처리가 가능하면 좋겠지만 쉽지 않아 보인다.

    이 방법은 코어를 하나로 절약할 수 있다는 장점이 있다. 워드프레스 코어 API는 워낙 성숙해 있고 변화가 뚜렷하지 않기 때문에 버전간의 차이로 인한 문제가 드문 편이기 때문에 꽤 안심할만하다.

    하지만 내 개인적인으로는 코어 이외에 다른 셋업들은 그냥 하나하나 설치하는 스탠드얼론 방법에 비해 이 방법이 아주 이득이 있는지는 모르겠다. 여러 사이트가 산만하게 늘어지는 것보다는 조금 정리할 수 있다는 여지가 있다는 정도? 그냥 이런 방법이 있고, 이런 응용이 가능하니 기록하는 의미로 포스팅한다.

    도메인 설정

    각 도메인마다 로컬 서버인 127.0.0.1로 매핑해야 한다. 가장 간단한 방법은 /etc/hosts 파일을 편집하는 법이다. 이 포스트를 위한 예시로 a.shared.com, b.shared.com 이라는 가상의 도메인을 사용하기로 하자.

    # /etc/hosts
    127.0.0.1 shared.com
    127.0.0.1 a.shared.com
    127.0.0.1 b.shared.com

    경로 설정

    여기서는 shared 라는 디렉토리를 설명을 위한 루트 디렉토리로 한다. shared 아래에는 다음처럼 디렉토리를 설정하였다.

    • configs: 도메인별 설정 저장
    • contents: wp-content 디렉토리를 도메인별로 저장
    • core: 워드프레스 코어 파일

    코어 파일 다운로드

    한 벌의 코어를 사용하기 때문에 코어를 별도의 장소에 격리한다. shared/core가 그 장소이며, 저기에 워드프레스 코어 파일을 다운로드 받는다. 아래 간단한 한 줄 다운로드 예제 코드를 첨부한다.

    # 워드프레스 설치는 이 한줄로!
    wget https://wordpress.org/latest.tar.gz -O - | tar xzf - --strip=1 -C shared/core
    echo 'Completed!'

    아파치 서버 조정

    복잡한 설정이 필요한 nginx보다는 간단하게 쓸 수 있는 아파치를 선호한다. 첫번째 아파치 설정은 디렉토리 접근 허용이다.

    <Directory "shared">
    	AllowOverride All
    	Require all granted
    </Directory>

    두번째는 가상호스트 설정. /etc/hosts를 위한 도메인 설정은 와일드카드가 먹히지 않지만, 다행히 아파치 가상호스트에서는 ServerAlias로 와일드카드를 설정할 수 있다. 요즘 https를 많이 사용하므로 간단하게 자가서명한 인증서를 이용해 https 방식으로도 동작하게 해 뒀다.

    <VirtualHost *:80>
    	ServerName   shared.com
    	ServerAlias  *.shared.com
    	DocumentRoot shared/core
    	Alias        /contents shared/contents
    </VirtualHost>
    <VirtualHost *:443>
    	ServerName   shared.com
    	ServerAlias  *.shared.com
    	DocumentRoot shared/core
    	Alias        /contents shared/contents
    
    	SSLEngine On
    	SSLCertificateFile    apache2/ssl/apache2.crt
    	SSLCertificateKeyFile apache2/ssl/apache2.key
    </VirtualHost>

    wp-config.php 파일 설정 조정

    wp-config.php는 워드프레스 코어에 두어도 되고, 코어 파일이 있는 디렉토리의 하나 위에 두어도 된다. 여기서는 코어 디렉토리보다는 하나 위쪽인 shared/wp-config.php 파일을 두도록 하자. 그리고 여기서는 각 사이트를 위한 설정 파일을 인클루드한다.

    <?php
    $host   = $_SERVER['HTTP_HOST'] ?? '';
    $config = __DIR__ . "/configs/{$host}.php";
    
    if ( $host && file_exists( $config ) && is_readable( $config ) ) {
        require_once __DIR__ . '/configs/common.php';
        require_once $config;
    } else {
        die( 'Config file is missing!' );
    }
    
    /* That's all, stop editing! Happy publishing. */
    
    /** Absolute path to the WordPress directory. */
    if ( ! defined( 'ABSPATH' ) ) {
    	define( 'ABSPATH', __DIR__ . '/' );
    }
    
    /** Sets up WordPress vars and included files. */
    require_once ABSPATH . 'wp-settings.php';
    

    wp-config.php는 각 도메인마다 읽어야 할 설정 파일을 구분해서 읽도록 지시한다. 그리고 shared/config/common.php 파일에는 모든 사이트의 범용 설정을 작성할 것이다. 나는 데이터베이스를 하나로 잡고 마치 멀티사이트처럼 접두만 변경할 것이므로 접속 정보는 common.php에 작성할 것이다. 그리고 디버그 모드 같은 설정도 이곳에 공통 요소로 잡을 것이다.

    <?php
    define( 'DB_NAME', 'database_name_here' );
    define( 'DB_USER', 'username_here' );
    define( 'DB_PASSWORD', 'password_here' );
    define( 'DB_HOST', 'localhost' );
    define( 'DB_CHARSET', 'utf8' );
    define( 'DB_COLLATE', '' );
    
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_DISPLAY', true );
    define( 'WP_DEBUG_LOG', false );
    define( 'WP_DISABLE_FATAL_ERROR_HANDLER', true );
    define( 'SCRIPT_DEBUG', true );
    

    그리고 shared/configs/a.shared.com.php, shared/configs/b.shared.com.php 두 파일을 적절히 생성한다.

    define('AUTH_KEY', '...');
    define('SECURE_AUTH_KEY', '...');
    define('LOGGED_IN_KEY', '...');
    define('NONCE_KEY', '...');
    define('AUTH_SALT', '...');
    define('SECURE_AUTH_SALT', '...');
    define('LOGGED_IN_SALT',  '...');
    define('NONCE_SALT', '...');
    
    $table_prefix = 'a_'

    각 KEY, SALT는 https://api.wordpress.org/secret-key/1.1/salt/ 에서 적절히 생성한 것을 사용한다. 개발 설정에서는 이 키를 common.php에 둘 수 있겠지만, 실서버 설정이라면 분리하는 것이 맞다. 그리고 $table_prefix 는 중요하다. 사이트마다 다른 문자열을 적용하여 다른 테이블을 사용하도록 설정하자. 물론, 의도적으로 같은 접두사를 쓰는 것도 가능하다.

    HTTPS로 리다이렉트

    자가인증서이긴 하지만 HTTPS 환경을 꾸미는데 부족함이 없다. http로 접속시 https로 리다이렉트하는 코드를 삽입하자.

    <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,QSA]
    </IfModule>

    WP-CONENT 디렉토리 변경

    여기까지 하면 도메인마다 다른 설정 파일과 다른 테이블을 이용하되 워드프레스 코어를 하나로 쓰는 것이 가능해진다. 그러나 아직 작업이 하나 더 남아있다. 여기까지 설정하면 모든 테마와 플러그인, 그리고 업로드 디렉토리 같은 wp-content 디렉토리를 공유하기 때문이다. 이것을 도메인별로 분리하는 작업을 더해야 한다.

    우선 shared/core/contents가 비어 있다. 여기에 a.shared.com, b.shared.com 두 도메인을 그대로 딴 디렉토리를 만들고, 생성된 디렉토리마다 wp-content의 index.php 파일과 plugins, themes, uploads 디렉토리를 복사해 넣는다.

    그리고 shared/configs/common.php 에 아래 설정을 덧붙인다.

    define( 'WP_CONTENT_DIR', "shared/contents/{$_SERVER['HTTP_HOST']}" );
    define( 'WP_CONTENT_URL', "https://{$_SERVER['HTTP_HOST']}/contents/{$_SERVER['HTTP_HOST']}" );

    이제 도메인마다 각자 다른 콘텐트 디렉토리를 가지게 된다.

    정리

    길게 늘여쓰기는 했지만 다음이 핵심이다.

    • shared 디렉토리에 configs, contents, core 세 디렉토리 생성후 각각 설정, wp-content, 코어 부분을 담당한다. 설정과 wp-content는 도메인마다 달라지므로 각각의 도메인으로 구분된 디렉토리별 관리를 한다.
    • configs 디렉토리에 common.php로 공통 설정을 두고, 각 도메인.php 파일을 읽도록 wp-config.php 파일 설정을 한다.
    • WP_CONTENT_DIR, WP_CONTENT_URL 도 shared/contents/{$HTTP_HOST}를 읽을 수 있도록 설정한다. 그리고 아파치 서버도 적절히 alias 설정을 해 두어 각 도메인별로 변경된 wp-content 접근이 가능하게 한다.
    • 각 도메인별 접근시 코어가 도메인별로 다른 설정 파일을 읽어 각자 다른 데이터베이스, wp-content (shared/contents/{$HTTP_HOST})를 이용하도록 한다.

    이렇게 하면 한 코어만 고정적으로 두고 새 워드프레스를 만들고 가볍게 삭제하는 것이 가능하다. 코어 업데이트도 단 한번이면 족하고, 사용 환경에 따라 장점이 많을 수 있다.

    그러나 이런 세팅을 사용해 개발을 몇 번 해 봤는데 단점도 있었다. 아무리 코어가 안정적이어도 코어의 환경이 변경되면 전에 없던 에러가 발생할 우려가 있기 마련이다. 때로는 스탠드얼론으로 모든 환경을 한 벌에 두고 안정적으로 두는 것이 더 나을 수도 있다.

    그리고 개발시 WP CLI를 자주 사용하게 되는데, 이 방법을 쓰는 경우 코어와 설정 등이 분리되어 있어 WP CLI를 사용하기 위해 추가적으로 파라미터를 더 지정해 줘야 하기 때문에 조금, 아니 꽤 불편하다는 단점이 있다. 이런 추가적인 설정을 계속해서 요구하기 때문에 오히려 더 번잡스러운 것이 아닌가 하는 생각도 좀 들었다. 워드프레스 코어 소스 파일이 많기는 하지만, 그렇다고 용량이 감당 못할 정도로 엄청난 것도 아니고…

    추가: 새 도메인에 새 워드프레스 추가하기

    이제 앞으로 새로운 도메인으로 워드프레스를 설치하려면 다음 순서대로 하면 된다.

    1. 새로운 도매인과 IP 매핑. e.g. /etc/hosts
    2. shared/contents 디렉토리에 도메인 이름으로 디렉토리 생성. 여기가 새 워드프레스의 wp-content 가 됨. 코어에 있는 wp-content/ 안의 내용을 복사하면 편하다.
    3. shared/configs 디렉토리에 도메인 이름.php 파일 생성. 여기에 적절한 설정을 붙인다. $table_prefix 변수는 반드시 여기서 설정해 두어야 한다.
    4. 새로운 도메인으로 접속하여 워드프레스 설치를 마친다.

  • 보호된 글: 배철수의 음악캠프 30주년 Live at the BBC 녹음본

    이 콘텐츠는 비밀번호로 보호되어 있습니다. 이 콘텐츠를 보려면 아래에 비밀번호를 입력해주세요:

  • flush_rewrite_rules() 함수에 대해

    플러그인/테마 개발시 flush_rewrite_rules()라는 함수를 심심찮게 쓴다. 안 하면 사이트에서 404 에러가 날 수 있어 항상 코드에 집어 넣는 나름 중요한 역할을 하는 녀석이다. 그런데 이 함수가 하는 역할이 정확히 무엇인지 알고 있는가? 이 포스트에서 이 함수에 대해 좀 더 정확히 알아보고 기록해본다.

    (더 보기…)
  • 파이어폭스 고정 탭 유지

    방법1: StartupApps 확장 설치.

    방법2: Reddit 에 설명한 대로 about:config 에서 browser.sessionstore.restore_pinned_tabs_on_demand 값을 true 로 설정.

  • 워드프레스 플러그인 번역

    ‘wp i18n make-pot’ 명령을 이용해서 만들 수 있는데, XDEBUG 사용시 에러가 난다.

    좀 더 예전부터 사용했던 레시피는 이렇게 한다.

    1. http://i18n.svn.wordpress.org/tools/ 여기 리포지터리를 체크아웃한다.
    2. 이 곳에 보면 makepot.php 파일이 있다.
    3. 다음처럼 명령을 준다: php makepot.php wp-plugin [디렉토리] [.pot 파일 경로]

    예시:

    cd /path/to/plugin
    php /path/to/tools/makepot.php wp-plugin . ./languages/lang.pot

    PoEdit을 이용하는 방법도 있으나, 이 방법의 장점은 플러그인 헤더까지 번역문으로 넣어준다는 것이다.

  • 워드프레스 개발 키워드: 문자열

    프로그래밍에서 상수와 참조는 중요하다. 변하지 않는다는 약속을 하는 상수는 에러로 가득한 코드에서 유일한 희망이고, 참조는 효율적인 관리를 위한 기본이다.

    그런데 곰곰히 생각하면 상수은 변하지 않는다는 약속을 전제로 정의하긴 하지만, 그 기반이 부서지면 상수는 효력을 발휘하기 어렵다. 참조 또한 마찬가지. 참조의 대상이 되는 원본이 변화하는 상황이면 참조는 신뢰를 잃게 된다.

    데이터베이스는 ID, 주 키 (primary key)를 기준으로 참조를 진행한다. 해당 데이터베이스 상에서는 유일한 식별자이기 때문에 의미가 있다. 그러나 워드프레스 입장에서 볼 때 이것이 유일하다는 보장을 하기 어렵다. 여러 워드프레스 사이트가 있다고 하자. 각 사이트끼리 비슷한 DB 테이블 구조와 스키마를 가지고 있다. 사이트간에는 이런 ID는 큰 의미가 없다. 그 레코드의 유일함을 보장할 수 없기 때문이다.

    그러므로 우리는 워드프레스에서 유사한 개체임을 식별하기 위한 수단으로 문자열을 사용하게 된다. 텀에서는 슬러그, 포스트에서는 메타 키나 포스트 타입을 이용하게 된다.

    결국 이런 것들은 문자열이다. 규약은 사람들의 약속으로 이뤄지며 중복의 방지는 제안하는 이가 최대한 중복되지 않도록 노력해야 한다.

    개발을 진행하며 플러그인과 테마를 개발할 때도 이 사실은 매우 중요하다. OOP나 기타 세련된 개발 기법을 적용할 때 이 관점을 잘 녹여내는 것이 매우 중요하다. 워드프레스가 가진 기능을 액션, 필터의 콜백을 이용해 확장할 시 깔끔하게 각각의 기능을 모듈화하는 것이 매우 어려워 보인다. 결국 이런 기능들이 여러 조각으로 분산되며 프로그래머들은 이런 분산이 불편해 보일 수도 있다.

    그러나 완벽한 구조는 존재하지 않는다. 워드프레스의 기본은 결국 문자열도 된 식별자이다. 정의된 문자열에 의한 식별자를 통해 각각의 모듈은 서로 교차되고 상호 작용할 수 있다고 생각해야 한다.