[카테고리:] 개발 레시피

  • 워드프레스 플러그인에서 HMR 사용하기

    이게 정확한 방법인지 확실한 문서화된 자료는 찾지 못했지만, 소스 코드를 뜯어 보다가 발견한 방법이다. 적당히 기록해 둔다. 공식적인 방법을 찾으면 수정하기 바란다.

    H.M.R (Hot Module Replacement)은 정말 쓸만하다. 그래서 워드프레스에서 wp-script를 활용한 리액트 컴포넌트 개발할 때 사용하고자 한다.

    스크립트 설치

    우선, 워드프레스의 공식 스크립트를 활용한다.

    pnpm add -D @wordpress/scriptsCode language: Bash (bash)

    포스트 작성시 @wordpress/scripts 의 버전은 23.0.0 이다.

    명령어 등록

    package.json 에서 ‘scripts’ 에 ‘start’ 명령어를 등록한다.

    {
      ...
      "scripts": {
        "start": "NODE_ENV=development wp-scripts start --hot",
        "build": "wp-scripts build"
      },
      ...
    }Code language: JSON / JSON with Comments (json)

    이 때 ‘–hot’ 파라미터를 붙이는 것이 필수. start에는 위 예시처럼 NODE_ENV 환경 변수를 ‘development’로 맞춘다.

    WP-CONFIG 상수 등록

    define( 'WP_ENVIRONMENT_TYPE', 'development' );
    define( 'SCRIPT_DEBUG', true );Code language: JavaScript (javascript)

    NPM 페이지에 따르면 ‘SCRIPT_DEBUG’를 설정하라고 되어 있다.

    WEBPACK 설정 수정

    const defaultConfig = require('@wordpress/scripts/config/webpack.config')
    const isProduction = process.env.NODE_ENV === 'production';
    
    if (!isProduction) {
        module.exports = {
            ...defaultConfig,
            devServer: {
                ...defaultConfig.devServer,
                allowedHosts: [
                    '127.0.0.1',
                    'localhost',
                    '.dev.site', // NOTE: match your domain.
                ],
                hot: true
            }
        }
    } else {
        module.exports = defaultConfig;
    }Code language: JavaScript (javascript)

    보통 localhost나 127.0.01 같은 IP 그대로는 개발하지 않으므로 주석으로 ‘NOTE’ 친 부분을 개발 도메인으로 설정한다. 앞에 ‘.’ 이 붙어 와일드카드가 적용된다. 그러므로 이 부분을 변경하기 위해 플러그인의 루트에 webpack.config.js 를 넣어 설정을 오버라이드한다.

    코드 작성

    리액트 코드를 작성한다. 예를 들어, 나의 index.js의 최상위 콤포넌트는 아래처럼 작성했다.

    import React from 'react';
    import ReactDOM from 'react-dom';
    import Dummy from "./dummy";
    
    function App(props) {
        const {title, you} = props;
    
        return (
            <div>
                <p>Oh! Hello, new world!</p>
                <p>You are {title}. {you}.</p>
                <Dummy />
            </div>
        )
    }
    
    const wpHmrSample = Object.assign({
        you: 'Unknown',
        title: '',
    }, window.hasOwnProperty('wpHmrSample') ? window.wpHmrSample : {})
    
    ReactDOM.render(<App {...wpHmrSample} />, document.getElementById('wp-hmr-sample'))
    
    if (module.hot) {
        module.hot.accept();
    }Code language: JavaScript (javascript)
    결과 화면

    스크립트 의존성

    wp-scripts에 의해 생성된 build/index.js 파일은 ‘wp-react-refresh-runtime’ 스크립트에 의존성을 가진다. 이를 확인하려면 build/index.asset.php 파일을 열어 보면 된다.

    구텐베르크 플러그인을 활성화하거나, 혹은 활성화 하지 않아도 해당 스크립트의 위치를 찾아 내 플러그인에서 강제로 넣어버린다. 나는 후자를 취했다. 어쨌든 두 방식 다 구텐베르크 플러그인은 설치된 상태여야만 한다.

    결과 비디오와 남은 문제

    작성한 플러그인 샘플 코드는 github에 올려 두었다. 그리고 실행 결과도 캡처하여 비디오로 남겨 둔다.

    아직 잘 모르는 부분까지 같이 비디오로도 남겼다. 영상 후반에 보면 App 컴포넌트 안에 있는 Dummy 컴포넌트를 수정하는 경우, 수정된 상황을 잘 감지는 하는 것 같지만 적용되지가 않는다. 왜 그런지는 아직 잘 모르겠다. 리액트가 변경된 컴포넌트를 렌더링하지 않아서 그런건지, 아니면 설정이 잘못된 것인지. 이것은 차차 알아가도록 하자.

  • 성가신 메시지 Xdebug: [Step Debug] Could not connect to … 제거

    XDebug 3 이후 계속 이런 메시지가 나온다.

    Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:9003 (through xdebug.client_host/xdebug.client_port) :-(

    디버깅과 관련 없는 부분인데도 자꾸 나와 성가시다. 이럴 때는 php 설정에 아래 사항을 하나 추가해 보자.

    xdebug.log_level=0

  • PDF 직접 다운로드 처리

    웹브라우저에서 PDF 링크를 열면 내장 PDF 뷰어가 뜬다.

    이 때 PDF의 주소 도메인이 현재 도메인과 같다면, 아래처럼 간단하게 처리 가능하다.

    <a href="{URL}" download="">PDF Download</a>Code language: HTML, XML (xml)

    이 때 download 속성에 파일 이름을 넣어서 별도의 이름을 줄 수도 있다.

    그런데 이 방법은 외부 URL에는 통하지 않는다. 다른 도메인에 있는 PDF를 굳이 다운로드 처리하고 싶은 변태들을 위해서는 자바스크립트를 사용해 보자.

    (function () {
        function checkDomain(url) {
            if (url.indexOf('//') === 0) {
                url = location.protocol + url;
            }
            return url.toLowerCase().replace(/([a-z])?:\/\//, '$1').split('/')[0];
        }
    
        function isExternal(url) {
            return ((url.indexOf(':') > -1 || url.indexOf('//') > -1) && checkDomain(location.href) !== checkDomain(url));
        }
    
        function downloadFile(url, callback) {
            var filename = url.split('/').pop()
                , req = new XMLHttpRequest()
            ;
    
            req.open('GET', url, true);
            req.responseType = 'blob';
            req.onload = function () {
                var blob = new Blob([req.response], {
                    type: 'application/pdf',
                });
    
                var isIE = !!document.documentMode;
    
                if (isIE) {
                    window.navigator.msSaveBlob(blob, filename);
                } else {
                    var windowUrl = window.URL || window.webkitURL;
                    var href = windowUrl.createObjectURL(blob);
                    var a = document.createElement('a');
                    a.setAttribute('download', filename);
                    a.setAttribute('href', href);
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                }
    
                if (callback) {
                    callback();
                }
            };
    
            req.send();
        }
    
        window.downloadControl = {
            checkDomain: checkDomain,
            isExternal: isExternal,
            downloadFile: downloadFile
        }
    })();
    Code language: JavaScript (javascript)

    위 스크립트에서 전역 변수 downloadControl 을 지정했다. 이 스크립트는 공통으로 사용하고, 다른 스크립트 파일에서 아래처럼 써 보자.

    $('.link').on('click', function (e) {
        var target = e.currentTarget
            , url = target.href;
    
        if (downloadControl.isExternal(url)) {
            e.preventDefault();
            target.href = '';
            downloadControl.downloadFile(url, function () {
                target.href = url;
            });
        }
    });Code language: JavaScript (javascript)

    대충 이런 식으로 처리한다. IE11에서도 동작하게끔 처리했다.

  • 우커머스 상품 ‘처리중’을 자동으로 ‘완료됨’으로 변경

    <?php
    add_filter( 
      'woocommerce_payment_complete_order_status',
      function () {
        if ( 'processing' === $status ) {
          $status = 'completed';
        }
        return $status;
      }, 10, 3
    );
    Code language: PHP (php)
  • 자동 업데이트 후 이메일 생략하기

    요즘 자동 업데이트는 잘 되는 편인데, 굳이 의미없는 이메일을 받는 건 좀 부담스럽다. 이메일 끄는 mu 플러그인을 아래처럼 만들어 붙일 수 있겠다.

    <?php
    /**
     * Plugin Name: Disable Auto-Update Email
     */
    
    add_filter( 'auto_plugin_update_send_email', '__return_false' );
    add_filter( 'auto_theme_update_send_email', '__return_false' );
    

  • 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 이다. 말하자면 여기가 공략 포인트다.