[카테고리:] 워드프레스 개발

  • 엘리멘터 프로 라이센스 불일치 알림 없애기

    엘리멘터 프로 라이센스 불일치 알림 없애기

    개발 사이트에서 원래 사이트를 복제하면, 도메인이 당연히 달라지게 된다. 그러면서 상용 라이센스 플러그인인 엘리멘터 프로 같은 플러그인은 라이센스 불일치 알림을 띄운다. 크게 사용상에 불편은 없지만 이 불일치 알림을 계속 보는 것은 성가시다.

    엘리멘터 프로의 라이센스 불일치 알림. 관리자 페이지에서 볼 수 있다.

    위에 보이는 이 그림 말이다. 이걸 안 보이게 만드는 플러그인을 따로 만들어 GIST에 올려둔다.

    https://gist.github.com/chwnam/0d43b8e919fad972a679d0537117f566

    강조한다. 이 플러그인을 결코 엘리멘터 프로의 불법복제를 장려하려고 만든 것이 아니다. 단지 개발 사이트에서 개발을 할 때 불편을 덜기 위한 임시 용도임을 명확히 한다. 단지 눈에 보이는 알림만을 보이지 않게 하는 것일 뿐이다. 만일 엘리멘터 프로가 라이센스 불일치를 감지해 기능에 제약을 걸 경우, 이 플러그인은 그런 제한을 푸는 – 소위 크랙(crack) 과 같은 행위는 절대 하지 않는다. 그러니 그냥 개발 용도라만 사용하시라. 라이센스는 별도로 걸지 않는다. 자유롭게 받아 사용하시라.

  • WASMER 이용한 워드프레스 로컬에서 구동하기

    https://wasmer.io/templates/wordpress-starter 에서 말한 WASMER를 이용해 PHP를 구동하고, 워드프레스까지 구동하는 획기적인 물건을 시험해 봤다.

    현재 실험 중인 SQLite 데이터베이스 엔진을 사용해 워드프레스를 구동하고, 실제로 약간의 버그는 있다. 뭐 돌아가는 것처럼 굴다가, 몇 번 해 보면 이내 에러를 내고 있다. 이건 나중에 차차 고쳐지겠지.

    DB에 내용이 저장된다면, WASMER 샌드박스에 관해 좀 더 알게된다면 무리없이 쓸 수 도 있을지 모르겠다. 그러나 아직은 개발환경으로 삼기에는 턱없이 무리이다. 그냥 이런 게 있다는 즐거움으로 알면 될 듯.

  • 워드프레스 커버 이미지 버그

    스튜디오 JT의 정재철 님이 제보하신 버그

    증상 재현

    • 멀티사이트, 에디터 이하의 권한으로 글 작성, 블록 에디터 편집 중 발생.
    • ‘이미지’ 블록을 선택해 ‘현재 미디어 URL’을 다음과 같은 URL로 변경한다.
      • https://images.unsplash.com/photo-1591273703337-fbede2a94c25?ixid=M3w0NzI4ODN8MHwxfHNlYXJjaHwxMnx8R3llb25nYm9rZ3VuZ3xlbnwwfHx8fDE3MTA3NDkwNDd8MA&ixlib=rb-4.0.3&w=1816&h=800&fit=crop
      • 포인트는 이미지 URL 에 쿼리 스트링으로 & 가 들어가야 한다는 것.
    • ‘커버’ 블록으로 바로 변환한다.
    • 저장하면, 저장은 되지만 프론트로 가 보면 이미지가 깨져 있다.
    • 다시 편집 화면으로 돌아오면 블록이 정상적으로 나오지 않는다.

    원인

    unfiltered_html 권한과 더불어 /wp-includes/kses.php 파일의 safecss_filter_attr() 함수에서의 style 속성 처리에서 약간의 문제가 겹쳐 일어남. 앞서 포인트에서 지적했듯 URL에 & 처럼 html entity 사용시 발생.

    우선 발생 조건을 다시 요악하면,

    1. 싱글 사이트에서 에디터의 권한 중 unfiltered_html 권한을 강제로 제외시키거나, 멀티사이트로 전환한다. Roles and Capabilities 문서에 따르면 에디터는 멀티사이트에서 unfiltered_html 권한을 가지지 않는다.
    2. set_current_user 액션 때 , 편집하는 사용자가 unfiltered_html 권한을 가지지 못하면 엄격한 kses 체크를 받도록 kses_init_filters() 함수가 동작한다.
    3. kses_init_filters() 함수 안에,
      add_filter( 'content_save_pre', 'wp_filter_post_kses' );
      라는 부분이 있다. 포스트 저장 시, kses 발동시키는 부분이다.
    4. kses 동작 중 wp_kses_attr_check() > safecss_filter_attr() 함수로 호출이 이어지고, safecss_filter_attr() 함수 안에서 문제가 일어난다.

    에디터에서 background-image: url();을 쓰는 것은 전혀 문제가 없는데, 이미지 URL에 파라미터가 붙게 되면, 가령 파라미터를 포함한 이미지 URL이 https://sample.image.com/flower.jpg?a=foo&b=bar 처럼 되어 있었다고 치자 (CDN인 경우 종종 이런 경우가 발생). 그러면 이것이 POST 전송되면서 https://sample.image.com/flower.jpg?a=foo&b=bar로 강제로 변경된다.

    그런데 safecss_filter_attr() 함수에서 HTML 태그 style 속성을 나눌 때 단순히 ‘;’ 만을 기준으로 나누게 구현되어 있다.

    function safecss_filter_attr( $css, $deprecated = '' ) {  
        if ( ! empty( $deprecated ) ) {  
           _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.  
        }
        ...
        $css_array = explode( ';', trim( $css ) );Code language: PHP (php)

    그러므로 여기서 CSS 속성들이 잘못 분리된다. 그래서 background-image 속성이 손상되고, 따라서 저장 이후에는 올바르게 블록이 불러와지지 않는 문제가 발생한다. 다만 블록 자체는 해당 URL을 잘 기억하고 있어, 복구가 가능하다. 이는 블록의 속성값들이 보통 post_content 에 HTML 주석 형태로 저장되기 때문으로 주석에서는 이런 URL 필터링이 동작하기 않기 때문이다.

    해결책

    • 옵션 1: 멀티사이트에서 계정에게 unfiltered_html 권한을 특별히 부여한다.
    • 옵션 2: url() 부분에서 & 같은 게 나오지 않도록 URL을 관리한다.
    • 옵션 3: 필터를 사용해 강제로 url() 에 & 같은 것을 &로 변환한다. PHP html_entity_decode() 함수가 도움이 될 것이다.

    대충 이렇게 mu-plugin으로 구현한다.

    <?php  
    /**
     * mu-plugin 디렉토리에 resolve-url.php 로 저장.
     */
    function resolve_url_set_current_user() {  
        if ( ! current_user_can( 'unfiltered_html' ) ) {  
           add_filter( 'pre_kses', 'resolve_url_fix_url', 9 );  
        }  
    }  
    
    function resolve_url_fix_url( $data ) {  
        return preg_replace_callback( '/url\(([^)]+)\)/', 'resolve_url_replace', $data );  
    }  
    
    function resolve_url_replace( $matches ) {  
        if ( isset( $matches[1] ) ) {  
           $parsed = parse_url( $matches[1] );  
           if ( isset( $parsed['query'] ) && str_contains( $parsed['query'], ';' ) ) {  
              $decoded = html_entity_decode( $matches[1], );  
              return "url($decoded)";  
           }  
        }  
        return $matches[0];  
    }  
    
    add_action( 'set_current_user', 'resolve_url_set_current_user' );
    Code language: PHP (php)
  • 백업할 때 기억할 것 노트

    마운트할 디스크가 NTFS 파티션을 썼어요!

    sudo mount -t ntfs /dev/sdc /mnt/ext-hdd

    파일이 겁나 많아서 파일 목록을 텍스트 파일로 뽑아서 편집한 다음, 그 목록에서 읽어오고 싶어요!

    ls -1 /path/to/src > ~/list.txt
    # ~/list.txt 파일을 열어 목록을 검토한다.
    xargs -a ~/list.txt cp -v -t /path/to/dstCode language: PHP (php)

    아, 참고로 내 현재 디바이스의 전면 USB는 제대로 동작하지 않는다. USB 케이블 연결은 케이스 후면으로 해야 한다.

  • Swapfile 조정하기

    메모리를 많이 먹는 작업을 하다 보니 메모리 부족 현상이 발생한다. 이럴 때 swap 크기를 조정하여 문제를 해결해 보자.

    우선, swap 크기를 확인한다.

    $ swapon -s
    Filename				Type		Size		Used		Priority
    /swapfile                               file		2097148		0		-2

    2097148 / 1024 = 2048 해서 2GiB 정도가 할당된 것을 확인할 수 있다. 우선 swap을 내려놓자.

    sudo wapoff -a

    그리고 16GiB 정도로 사이즈를 늘려 보자.

    sudo dd if=/dev/zero of=/swapfile bs=1G count=16

    16이 16GiB 할당한다는의미로 보면 된다. 이제 swap을 위한 파일로 만든다.

    sudo mkswap /swapfile

    그리고 swap을 다시 올려놓자.

    sudo swapon /swapfile

    이제 확인해 보자.

    grep SwapTotal /proc/meminfo
    SwapTotal:      16777212 kB

    좋아!

    출처: How to increase the size of your swapfile

  • WP REST API, fetch(), 그리고 CORS 허용하기

    WP API를 불러다 사용할 때, 도메인이 다르면 CORS 제한에 걸리게 된다. 물론 서버에서 직접 부르면 이 제한은 없지만, 브라우저에서 직접 호출할 때는 성가신 문제가 생긴다.

    그래서 보통은 서버에서 직접 Access-Control-Allow-Origin 헤더를 추가하도록 하는 팁을 발견할 수 있다. 하지만 워드프레스 WP REST API 사용할 때 굳이 이렇게까지 할 필요는 없다.

    간단하다. 요청할 때 Origin 헤더를 추가하고, 거기에 브라우저에서 접속한 URL을 입력하면 된다.

    원리는 이렇다. 아래는 WP REST API 응답 메시지 출력 중, CORS 헤더를 만드는 함수 구현이다.

    function rest_send_cors_headers( $value ) {
    	$origin = get_http_origin();
    
    	if ( $origin ) {
    		// Requests from file:// and data: URLs send "Origin: null".
    		if ( 'null' !== $origin ) {
    			$origin = sanitize_url( $origin );
    		}
    		header( 'Access-Control-Allow-Origin: ' . $origin );
    		header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
    		header( 'Access-Control-Allow-Credentials: true' );
    		header( 'Vary: Origin', false );
    	} elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) {
    		header( 'Vary: Origin', false );
    	}
    
    	return $value;
    }Code language: PHP (php)

    코드에 get_http_origin() 함수에서 $origin 을 발견하면 알아서 Access-Control-Allow-Origin 헤더를 추가해 주는 흐름을 볼 수 있다.

    그리고 get_http_origin() 함수 구현은 아래와 같은데,

    function get_http_origin() {
    	$origin = '';
    	if ( ! empty( $_SERVER['HTTP_ORIGIN'] ) ) {
    		$origin = $_SERVER['HTTP_ORIGIN'];
    	}
    
    	/**
    	 * Change the origin of an HTTP request.
    	 *
    	 * @since 3.4.0
    	 *
    	 * @param string $origin The original origin for the request.
    	 */
    	return apply_filters( 'http_origin', $origin );
    }Code language: PHP (php)

    요청 헤더에서 Origin 헤더 값을 발견하면 그 값을 리턴하도록 되어 있다. 그러면 js/ts 코드에서 fetch()를 호출할 때 아래처럼 하면 된다.

    const url = 'https://...' // 요청할 URL.
    
    fetch(url, {
      mode: 'cors',
      headers: {
        'Content-Type': '...',
        'Origin': 'https://....', // 브라우저의 URL.
      }
    })Code language: JavaScript (javascript)

    이렇게 하면 워드프레스가 알아서 CORS 허용 헤더를 추가해준다. 끝!

  • Cinnamon DE CLI 명령으로 VPN 접속하기

    아래처럼 명령을 입력할 수 있다.

    nmcli con up "VPN_NAME"Code language: JavaScript (javascript)

    그리고 연결 이름을 확인하려면,

    nmcli con

    연결이 up이니, 연결을 끊는 것이 down 인 건 자명하겠지?

  • 메타 테이블 체계 확장하기

    그러니까, 워드프레스에서 메타 테이블 체계는 상당히 매력적이다. 물론 단점도 많긴 하다. 그러나 간단하게 확장 가능한 유연한 저장 장소가 필요할 때 메타 키 – 메타 값 식으로 테이블을 구성해서 쓰고 싶은 생각이 들 때가 한두번이 아니다.

    다행히 워드프레스 코어 또한 메타데이터 코드는 한 벌만 만들고 포스트, 텀, 유저 등에서 확장하여 재활용하고 있다. 플러그인에서도 동일하게 확장하여 메타데이터 스타일의 테이블을 사용할 수 있다. 한 번 해 보자.

    (더 보기…)
  • 테마 style.css 파일의 대안 위치가 있었군

    보통 테마는 테마의 루트에 style.css 를 둔다. 그러면 코어는 style.css 파일에서 테마의 정보를 추출한다. 이게 정석이다.

    그러나 오늘 sage라는 테마를 보다 보니, sage/resources 디렉토리에 functions.php 파일과 style.css 파일이 발견되었다. 이게 어떻게 가능한 가 싶어 뜯어 보니, wp-includes/themes.php 파일에 정의된 search_theme_directories() 함수에서 이런 동작을 지원하는 것이었다.

    1. wp-content/themes 의 디렉토리를 찾아, 해당 디렉토리에 있는 style.css 파일을 발견하면 메타데이터를 읽는다.
    2. style.css 파일이 발견되지 않는다면 해당 디렉토리의 하위 디렉토리에서 style.css를 검색한다. 파일이 빌견되면 그렇다면 테마의 루트는 해당 디렉토리가 된다.

    예를 들어 wp-content/themes/foo 라는 테마 루트 디렉토리에 bar라는 하위 디렉토리를 두고 거기에 style.css를 둔다. 그러면 wp-content/themes/foo/bar/style.css로 경로가 만들어질 것이다. 이 때 코어는 wp-content/themes/foo/bar를 테마 루트로 인식하게 된다.

    최근 테마 루트에 composer.json, package.json등 여러 설정 파일들이 존재하게 되고 이것들은 이 디렉토리를 포기할 수 없다. 그런데 이런 설정 파일과 테마의 템플릿이 서로 뒤섞이면 매우 지저분하다. 이것을 의식한 건 아닌가 모르겠다. 아무튼 이렇게 하면 템플릿 코드와 여러 설정 파일을 깔끔히 분리할 수 있어 좋은 방법이라고 생각한다.

  • Good Job! PHPUnit-Polyfills

    코어의 유닛 테스트 코드들은 아직 완전히 최신 PHPUnit을 지원하는 건 아닌 것 같다. 대신 Yoast의 PHPUnit-Polyfills 라이브러리를 사용하여 호환성을 보장하는 것 같다. 덕분에 PHPUnit 9 최신 버전을 사용할 수도 있고, 그 덕분에 PHP 8.1 최신 버전이 사용 가능하다.

    최신 버전의 PHP와 PHPUnit을 사용하여 유닛 테스트! 이제는 정상적으로 실행된다!

    이게 된다면 앞으로 플러그인 개발시 7.4보다 더 높은 버전을 도입해서 사용할 수 있겠다! 그동안 8.0의 기능을 사용하지 못해 정체되었는데, 이제는 가능할지도.

  • 액션 스케쥴러, 함 잡솨봐

    워드프레스에는 WP-CRON이라는 스케쥴에 맞춰 특정 작업을 실행하는 API가 있다. 액션 스케쥴러는 이것에 기반해 사용하기 편리한 비동기 실행 큐 라이브러리이다. 오토매틱에서 제작하고, 우커머스나 구독 플러그인 등 다수 플러그인에서 이미 사용되고 있다.

    이번에 진행하는 프로젝트에서 이것을 사용해 볼 기회가 있었다. 간단하게 비동기 큐가 필요했고, 구조를 복잡하게 하는 것보다는 간단한 프로토타이핑으로 단순한 구성을 원했기 때문이다.

    액션 스케쥴러는 워드프레스 기반으로 개발할 때, 비동기 큐를 써야 한다면 훌륭한 선택이라고 생각한다. 하여 차후 액션 스케쥴러를 다시 써야 할 때를 위해 간단히 사용한 것을 정리한다. 공식 홈페이지에서 5분이면 간단하게 얻을 수 있는 정보의 복제 말고, 조금 애매했던 개념을 설명하는 포스트이다.


    액션이란 특정 시간에 특정 작업을 하기 위한 수단이다.


    액션의 청구라는 개념이 있다. 영어로는 대략 stake claim 정도로 표현되는데, 액션 테이블에서도 claim_id 라는 것이 발견되기도 하고, 공식 문서에서도 한 두번 정도 언급될 뿐 이것이 무엇인지 자세히 설명하지 않는다. 여기서 간단히 설명하자.

    액션은 이벤트를 예약하는 수단이다. 그러나 이것이 실행 큐에서 실제로 실행되기 위해서는, 먼저 큐가 액션의 스케쥴을 파악하여 적절한 액션을 골라 실행하기로 결정하는 작업이 먼저 필요하다. 해당 시점에 적절한 액션을 골라 실행을 결정하는 과정이 액션의 청구(claim)이며, 실행 큐가 한번에 작업하기로 결정한 액션에게는 동일한 청구 ID (claim ID)를 부여한다.


    실행 큐가 실행되면서 한 번에 액션 뭉치를 가져와 일괄적으로 작업하는 것을 배치(batch)라고 한다.

    배치에는 배치 크기 (batch size)라는 개념이 있다. 액션을 청구할 때 한번에 이 갯수만큼의 액션을 정한다. 배치 크기의 기본값은 25이다. 기본값 대로라면 실행 큐는 한 번에 최대 25개의 액션을 가져와 작업을 진행할 것이다. 이 때 25개의 작업은 동시 작업(multitasking)이 되는 것은 아니고, for-loop 을 돌면서 순차 진행된다.


    실행 큐는 WP CLI를 통해 커맨드라인으로 실행이 가능하다. 커맨드라인에서는 제한 시간의 제약기 없기 때문에 상당히 오랜 시간 동안 작업을 해도 에러만 없다면 아무 문제 없다.


    실행 큐는 기본적으로는 WP-CRON의 ‘매 분 (every minute)’ 스케쥴에 맞춰 동작한다. 그러므로 리눅스 기준 매 분마다 정확하게 실행 큐가 동작하는 것을 보장하려면 아래처럼 작업을 한다.

    1. wp-config.php에 define( 'DISABLE_WP_CRON', true ); 를 삽입.
    2. crontab에, 아래 코드 삽입.
    * * * * * wget --delete-after http://YOUR_SITE_URL/wp-cron.php >& /dev/nullCode language: JavaScript (javascript)

    참고로 훅 이름은 ‘action_scheduler_run_queue’ 이다.


    실행 큐는 훅의 콜백으로 단 1개만 실행된다. 즉, 기본값 대로라면 매 분 1개의 실행 큐가 생성되어, 25개의 액션만을 청구하여 배치를 하는 것이다.

    이 기본값들은 액션 스케쥴러 개발자들이 다양한 상황을 염두에 두고 매우 동작에 제약을 둔 설정이다. 만약 운영하는 서버가 성능이 보다 괜찮다면 큐를 좀 더 둘 수 있다. 이 때 ‘Increasing Initialisation Rate of Runners‘에서 말하는 것처럼 ‘action_scheduler_run_queue’ 훅에 맞춰 admin-ajax.php 에 원하는 갯수만큼의 실행 큐를 증가시켜 볼 수 있다. 간단한 로그를 찍는 코드로 테스트 해 봤을 때, 저렇게 만들어진 큐에서 작업이 진행되며 AJAX 요청을 보낸 쪽에서는 더 작업을 하지 않는 듯. 그러므로 N개를 추가시켰으면 N+1의 실행 큐가 아닌 N개의 실행 큐가 생성되는 모양이다.


    여러 개의 실행 큐가 admin-ajax.php 요청을 통해 실행한다면, 비동기적으로 동시 실행되는 그림이 만들어진다. 이 때 동시 배치 (concurrent_batch)라는 정수값이 중요한 역할을 한다. 이 값의 디폴트는 1이다.

    각각의 실행 큐는 각자 작업을 진행하면서, 현재 청구(claim)되어 있는 액션의 개수가 이 동시 배치를 넘는지 넘지 않는지 체크하여 초과하면, 작업을 하지 않도록 프로그래밍 되어 있다.

    디폴트인 1을 바꾸지 않으면 다음과 같은 문제점이 발생한다. 설명이 복잡하니 불릿으로 나누어 설명한다.

    • 첫번째 실행 큐가 현재 청구된 액션 수와 동시 배치 수를 비교한다. 가장 처음이므로 청구된 액션 수는 0이다.
    • 동시 배치값 1은 0보다 크므로, 첫번째 실행 큐는 배치를 시작한다.
    • 첫번째 실행 큐가 배치를 하기 위해 액션을 청구한다. 배치 크기만큼 (기본값이라면 최대 25개) 액션을 잡을 것이다.
    • 두번째 실행 큐가 동작을 시작한다. admin-ajax.php를 통해 실행되므로, 첫번째 실행 큐와는 서로 독립적인 프로세스에서 동작한다.
    • 두번째 큐와 현재 청구된 액션 수와 동시 배치 수를 비교한다. 첫번째 큐가 이미 25개의 액션을 청구한 상태이다. 엄청나게 빠른 속도로 액션들이 처리되고 있는 것이 아니라고 가정하자. 즉 아직 여전히 25개가 청구된 상태이다.
    • 동시 배치값 1은 25보다 작다. 두번째 큐는 실행을 멈춘다.
    • 즉 두번째 큐는 아무 일도 하지 않고 일을 마친다. 뭐, 세번째, 네번째 큐가 있더라도 액션 청구가 1보다 줄지 않는 한 일을 하지 않을 것이다.

    그러므로 동시 처리 수는 (총 실행 큐의 수) * (배치 사이즈) 에 근접하게 설정해야 제대로 동시성을 기대할 수 있다. 이게 가장 핵심인 부분이다.


    이 부분은 나중에 덧붙인 글이다. 동시 처리 수에 대한 개념이 부족해 보충 설명을 적는다. 위 설명은 액션이 충분히 길 때에만 적절한 설명이다. 동시 처리 수를 실행 큐 수 * 배치 사이즈에 맞추더라도 하나의 큐가 모든 작업을 하려고 들 수도 있다. 하나의 실행 큐는 자기에게 허락된 메모리량, 처리시간의 여유가 되는 만큼 최대한 액션들을 많이 가져려고 한다.

    예를 들어 보자. 총 실행 큐 수는 3개, 배치 사이즈 5개, 동시 처리 수를 15개로 잡았다고 보자. 그리고 처리해야 할 액션 수는 15개라고 가정하자. 그러면 총 실행 큐 3개가 각각 액션 5개를 맡아 처리할 것 같지만, 실제로는 그렇게 동작하지 않을 수 있다.

    만약 주어진 액션이 너무 간단해 하나의 실행 큐가 5개의 액션을 모두 처리한 다음엔, 그 실행 큐는 메모리와 실행 제한 시간이 허락된 선에서 추가적인 액션을 불러와 일을 한다. 만약 액션 처리가 너무 간단했다면 여전히 하나의 실행 큐가 모든 액션을 혼자 담당할 수도 있다. 다른 실행 큐가 비동기적으로 실행되기도 전에 말이다. 정말 너무 일이 간단하다면 하나의 실행 큐가 동시 처리 수와 무관하게 스케쥴 된 모든 액션들을 가져와 작업을 할 수도 있다!


    액션 스케쥴러는 유연성이 상당히 높다. 액션 스케쥴러는 플러그인으로도, 내 플러그인이나 테미의 서브 프로젝트로도 사용 가능하다.

    액션 스케쥴러는 원래는 wp_posts 테이블에서 액션을 관리했던 모양이다. 처음 실행하면 마이그레이션 훅을 돌리는 것이 있는데 아마 예전의 포스트 테이블의 내용을 별도 테이블로 전환하는 과정을 돌리는 것 같다. 굳이 예전으로 돌릴 필요는 없는 듯.

    작업을 돌리는 데 분 단위로 동작한다는 점이 약간 제약으로 생각될 수 있다. 그러나 정말 다량의 작업량이 있다거나 지정된 스케쥴에 동작하는 것이 우선적인 목적이라면 해당 단점이 크게 부각될 것 같지는 않다. 액션 스케쥴러는 확실히 워드프레스 환경에서 상당히 괜찮은 라이브러리임에 틀림없다고 생각한다.

  • dbDelta() 함수 똑바로 쓰기

    기본적인 것들

    $wpdb->prefix 꼭 사용하라. 보통 접두에 언더바 붙어 있으니 언더바 중복하지 말고.


    $wpdb->get_charset_collate() 메소드는 자동으로 CREATE TABLE (...) 구문 뒤에 들어갈 DEFAULT CHARACTER SET {CHARSET} COLLATE {COLLATE} 구문을 만들어 준다.


    dbDelta() 호출 전 반드시, require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 해야 한다. 안그러면 fatal error 먹는다.

    테이블 변경

    코덱스를 참고하면 대략 다음과 같이 쓰라고 되어 있다.

    • 필드 각각 한 줄씩 나눠서 써라.
    • PRIMARY KEY 선언과 주 키 선언 사이에 2개의 스페이스로 띄워라.
    • INDEX 보다는 KEY를 써라. 최소 하나의 키를 첨부해라.
    • KEY는 반드시 다음 괄호 사이에 하나의 스페이스를 두어야 한다.
    • 필드 이름 앞뒤로 어깨점이나 백틱으로 감싸지 마라.
    • 필드 타입은 항상 소문자.
    • CREATE TABLE, UPDATE, KEY 같은 SQL 키워드는 항상 대문자.
    • 필드에 길이 파라미터를 지정할 수 있는 필드는 항상 써야 한다. 가령, int(11) 이나 bigint(20) 같은 것.

    dbDelta() 는 꽤나 복잡한 프로세스를 거쳐 SQL 문을 분석해 테이블의 변화를 감지한다. 그리고 자신이 어떤 일을 했는지를 배열로 리턴한다.

    Array
    (
    [wp_temp_table] => Created table wp_temp_table
    )

    위 예시는 wp_table_table 을 만들었을 때의 결과이다. 그리고,

    Array
    (
        [wp_temp_table.middle_name] => Added column wp_temp_table.middle_name
    )

    이 예시는 wp_temp_tablemiddle_name 이라는 필드를 추가했을 때 결과이다. 이런 식으로 필드 타입, 기본값 등등이 잘 변경되는지 확인할 수 있다.


    필드가 삭제나 인덱스 삭제는 되지 않는다. 별도로 필드를 없애는 구문을 추가해야 할 것 같다.


    코덱스에서는 INDEX 보다는 KEY 쓰리고 하지만, INDEX 가 안 되는 건 아니다. 당연히. 그리고 이미 만들어진 인덱스 변경도 잘 되지 않는다. 별도로 구문을 써야 한다. 마찬가지로 PK 선언시 두 칸을 띄워 쓰라고 하는데, 딱히 왜 그래야 하는지는 잘 모르겠다.


    복잡한 변경은 잘 인지하지 못하는 것 같다. 예를 들어 필드의 타입과 기본값까지 동시에 변경 처리하는 경우, 그 둘의 변경점을 잘 감지하지 못하는 것 같다.


    CREATE TABLE 선언시 CREATE TABLE IF NOT EXISTS 나, 별도로 테이블이 있는지 없는지 체크하는 따위의 삽질을 하지 않아도 된다. dbDelta 가 적절히 CREATE TABLE 구문과 현재 선언된 테이블의 구조를 파악해 CREATE TABLE을 그대로 사용하기도 하지만, CREATE TABLE 대신에 ALTER TABLE 를 스스로 만들어 내거나, 변경점이 없다면 아무 일도 하지 않기 때문이다.


    이런 것도 한 번 체크해 보라. dbDelta() 는 까다로운 녀석이다. SQL 구문에 변경점이 전혀 없어, 딱히 변경할 것이 없어 보인다. 그래도 진짜 아무 일도 실행하지 않는지 (즉, dbDelta()가 빈 배열을 리턴하는지) 확실히 체크해 보라. 간혹, 예를 들어, 타입을 대문자로 썼다거나 하는뭔가 실수 아닌 실수가 있다면 dbDelta()는 변경할 것이 없는데도 계속 ALTER TABLE 구문을 만드는 삽질을 하고 있을 수도 있다.


    db 버전을 설정하고 잘 관리하라. 예를 들어,

    function update_my_table() {
      $current_version = '1.1.0';
      $installed_version = get_option( 'my_db_version' );
    
      if ( $current_version !== $installed_version ) {
        global $wpdb;
    
        $sql = "CREATE TABLE .....";
    
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
        update_version( $current_version );
      }
    }Code language: PHP (php)

    같은 패턴을 쓰면 된다. 이 때 사용자는 테이블을 업데이트하기 위해 플러그인을 비활성화/활성화를 할 수 없다. 그러므로 plugins_loaded 같은 액션의 콜백에서 이 함수를 한 번 호출해야 한다.

    만약 이렇게 되면 activation callback 에서 굳이 테이블을 생성하는 작업을 중복해서 할 필요는 없는 것 같다. 아, 물론 플러그인 활성화 시 바로 초기화 작업 같은 것을 해 준다면 이야기가 달라지겠지만.

    결론

    급하게 테스트해보고 메모하듯이 글을 적었다. 내가 틀린 것을 적었을 수도 있다! 어쨌든 변경점이 생길 때 dbDelta()가 리턴하는 결과와 테이블의 구조를 실제로 잘 체크해야 한다. dbDelta()가 기능 개선이 될 수도 있으니 말이다.

    암튼 현재 dbDelta() 함수는 다른 웹 프레임워크의 마이그레이션 솔루션 (예를 들어, 장고) 만큼 테이블 변경을 세련되게 챙겨주지는 못하지만, 그래도 간단하게 테이블 변경을 찜쪄먹기에는 나름 유용한 도구로 보인다. 다만, 이 녀석이 진짜 까다로우니 조심해서 쓰도록 하자.

  • 워드프레스 플러그인에서 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 컴포넌트를 수정하는 경우, 수정된 상황을 잘 감지는 하는 것 같지만 적용되지가 않는다. 왜 그런지는 아직 잘 모르겠다. 리액트가 변경된 컴포넌트를 렌더링하지 않아서 그런건지, 아니면 설정이 잘못된 것인지. 이것은 차차 알아가도록 하자.

  • WP CLI 캐시에 대해 메모

    WP CLI 캐시에 대해 메모

    WP CLI로 많이 사용하는 명령어 중 하나는 워드프레스 코어 파일을 다운로드 받는 것이다.

    워드프레스 루트 디렉토리를 만들고, 거기서 wp core download를 입력하면 최신 버전의 코어 파일이 다 준비되니 편리하다.

    그런데, 나는 개인적으로 문제가 하나 있다. SK 브로드밴드 인터넷을 사용하면 해외 사이트로의 다운로드 속도가 미칠듯이 느리다. 대략 워드프레스 코어 파일을 압축하면 20메가바이트 내외인데, 대략 5분 이상의 시간이 걸린다. 그렇다. 나는 SK 인터넷을 사용한다. 어쩌다 집에서 다운로드를 받으면 타임아웃에 걸릴 정도로 느리다.

    이 때 한가지 요령이 있다. ~/.wp-cli/cache 디렉토리에 WP CLI는 미리 파일을 캐시해 두었다가 매번 다운로드 받지 않고 여기의 파일을 재활용한다.

    이 cache 디랙토리에는 core, plugin, translation 등의 디렉토리가 있는데, 이 core 디렉토리에 코어 파일을 버전과 언어별로 압축 파일을 저장해 둔다.

    예를 들어, 워드프레스 5.8.3버전의 한국어 버전은 wordpress-5.8.3-ko_KR.zip으로 저장된다. 한편 영문 버전은 wordpress-5.8.3-en_US.tar.gz로 저장된다. 왜 영문 버전은 .tar.gz로 받아지고 한국어는 .zip으로 받아지는건지, 이게 규칙인지는 잘 모르겠다.

    참고로 영문 코어를 받으려면 https://wordpress.org/wordpress-{version}.{zip,tar.gz} 처럼 URL을 입력하면 된다.

    한국어 코어는 https://ko.wordpress.org/wordpress-{version}-ko_KR.{zip,tar.gz} 처럼 URL을 입력하면 된다.

    영문 최신 버전은 https://wordpress.org/latest.tar.gz 으로 받을 수 있다. 그러므로 한줄짜리 워드프레스 설치 스크립트를 아래처럼 응용해 볼 수도 있긴 하다.

    mkdir ~/wp_root && cd ~/wp_root
    wget -O - 'https://wordpress.org/latest.tar.gz' | tar xzf - --strip=1

    암튼 정리하자. wp core download 다운로드 속도가 너무 느려 타임아웃까지 걸리는 문제가 생길 때는 이렇게 하자.

    1. 워드프레스 코어 파일을 웹브라우저나 다운로드 관리자 같은 걸로 어쨌든 받는다.
    2. 영문이면 wordpress-{version}-en_US.tar.gz 로, 한국어는 wordpress-{version}-ko_KR.tar.gz 로 이름을 변경한다.
    3. 변경된 파일을 ~/.wp-cli/cache/core로 이동한다.
    4. wp core download 시 캐시 히트가 발생하는지 확인해 보자.

    워드프레스 자동 설치 스크립트와 연동하여 편의성도 제고해보자.

  • 성가신 메시지 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

  • Naran Social Share 0.2.0

    Naran Social Share 0.2.0

    나란 소셜 공유 플러그인을 0.2.0으로 업데이트했습니다.

    • 한국어 번역이 추가되었습니다.
    • 기타 편의성이 향상되었습니다.

    다운로드

  • Naran Social Share 0.1.0

    빡빡하게 짜여져 있지 않고, 개발자가 원하는 대로 맞춰 쓸 수 있는 소셜 공유 플러그인.

    지원하는 서비스

    • 페이스북
    • 트위터
    • 링크드인
    • 카카오톡
    • 네이버 블로그
    • 텔레그램
  • Naran Session

    쿠키 – Transient API를 엮어 만든 간단한 세션.

    간단하게 각 접속자마다 구분된 값을 처리할 때 유용하다.

  • Naran GeoIP

    Naran GeoIP

    Naran GeoIP 는 MaxMind GeoLice2 Free Geolocation Data를 이용하여 서버에 접속하는 접속자의 국가를 파악하는 워드프레스용 플러그인이다.

    나란 프로젝트는 사용자에게 알뜰하게 다양하고 손쉬운 기능들을 제공하는 것보다는 (주로 내가) 개발시 사용하기 위한, 좀 더 개발 친화적인 플러그인들을 제공하기 위함이다.

    그러므로 이 나란 지오아이피 또한 살뜰하게 일반 사용자를 챙기는 기능은 거의 없다. 관리자 패널에서 라이센스 키 입력, 몇몇 테스트 도구가 있는 것 말고는 모두 API를 편리하게 제공하기 위함에 가깝다.

    나중에 국가별 사용자 필터링이 필요한 프로젝트가 있을 때, 이 플러그인을 그대로 쓰든, 코드를 추출하든, 포크를 하든 보다 쉬운 선택지를 제공하는 데 큰 의의가 있다.

    코드는 Naran Boilerplate Code를 기반으로 하고 있다. 예상컨데, 이 boilerplate 기반의 프로젝트 끼리는 원활한 코드 교환이 가능하리라.