[작성자:] changwoo

  • gulpfile.js

    스크립트 minification 을 위해 자주 사용하는 gulpfile.js 템플릿 코드

    const gulp = require('gulp')
        , uglify = require('gulp-uglify')
        , uglifycss = require('gulp-uglifycss')
        , rename = require('gulp-rename')
        , sourcemaps = require('gulp-sourcemaps')
        , clean = require('gulp-clean')
    ;
    
    gulp.task('min-js', function () {
        return gulp
            .src([
                'assets/js/**/*.js',
                '!assets/js/**/*.min.js',
                '!assets/js/vendor/*.js',
            ])
            .pipe(sourcemaps.init())
            .pipe(uglify())
            .pipe(rename(function (path) {
                path.extname = '.min.js';
            }))
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('assets/js'));
    });
    
    gulp.task('min-css', function () {
        return gulp
            .src([
                'assets/css/**/*.css',
                '!assets/css/**/*.min.css',
                '!assets/css/vendor/*.css',
            ])
            .pipe(uglifycss())
            .pipe(rename(function (path) {
                path.extname = '.min.css';
            }))
            .pipe(gulp.dest('assets/css'));
    });
    
    gulp.task('clean', function () {
        return gulp
            .src([
                'assets/**/*.min.{js,css}',
                '!assets/css/vendor/*.css',
                '!assets/js/vendor/*.js',
            ], {read: false})
            .pipe(clean())
    });
    
    
    gulp.task('watch', function () {
        gulp.watch([
            'assets/js/**/*.js',
            '!assets/js/**/*.min.js',
            '!assets/js/vendor/*.js',
        ], gulp.parallel(['min-js']));
    
        gulp.watch([
            'assets/css/**/*.css',
            '!assets/css/**/*.min.css',
            '!assets/css/vendor/*.css',
        ], gulp.parallel(['min-css']));
    });
    
    gulp.task('default', gulp.series('clean', 'min-js', 'min-css', 'watch'));
    gulp.task('build', gulp.series('clean', 'min-js', 'min-css'));
    
  • 콘텐츠 입장에서 구성하고 작성하라

    철저히 콘텐츠 입장에서 콘텐츠가 보이는 사람에게 어떻게 전달될지 고민하라. 내가 보는 콘텐츠 관점을 버려라. ‘내’가 콘텐츠를 관리하는 주체라고 생각하여 콘텐츠를 보는 사람과 콘텐츠 사이에 중개체로 나를 끼우는 것은 곤란하다.

    언제나 콘텐츠 스스로가 콘텐츠의 관점에서 스스로의 정보를 설명하게 하라. 내가 그 콘텐츠라면? 나를 소개하는 설명서가 이 콘텐츠라면? 이라고 상상해보자.

  • 속깊은 JavaScript

    도서 정보 링크

    이 책은 작년쯤 사두고 방치해 두었다. 여유있을 때 조금씩 책을 읽고 싶은 마음이 들었는데, 방치해둔 책을 끝까지는 읽어보고픈 맘이 들어 읽게 되었다. 읽고 나니, 읽었던 기록을 남기고 싶었다. 간단히 적어 본다. 독서가 습관이 아닌 나, 글쓰기에 서투른 나다. 블로그에 글 적어 봐야 아무도 관심가지지 않을 거라는 확신 하에 적어 본다.

    2020년 12월 31일. 1년의 마지막이자 또다른 1년의 시작 전이 날이다. 대개 이런 날에는 안하던 짓을 하나씩 하게 된다. 그것 중 하나가 이건가보다. 이 책은 아침저녁으로 책상에 앉을 때 계속 눈길만은 주던 책이고 언젠가 읽어야지 생각만 하던 책이다. 요즘 괴로울 정도로 권태롭다. 일상에 변화를 주고 싶다. 자극을 주고 싶다. 평소와는 다른 것들을 해야한다고 생각한다. 독서가 그런 자극이 될까 모르겠다.

    책의 분량이 있어 하루만에 다 읽지는 못하고 1에서 4장까지 읽어내렸다. 이전에 책을 산 그날 대략 2장인가 3장까지 읽고 방치해뒀던 기억이 난다.

    1장은 초반이라 대략적인 설명이라 큰 감흥은 없고, 2장과 3장도 읽었던 내용이고 어지간히 숙지하고 있는 내용이라 큰 어려움은 없었다.

    4장이 좀 문제인데, 그동안 자바스크립트의 상속 개념, 프로토타입 개념이 조금 난해해서 대강 넘어갔었는데, 이 챕터에서 아주 자세하게 설명을 한다. ECMA 표준까지 인용해가며 설명해주는 것은 좋기는 하지만 읽다 보니 내용이 지나치게 깊게 들어가 이해하기 쉽지 않은 점은 좀 아쉽기도 하다. 내 독해력이 문제인가…? 몇 번 반복해야 할 문제일지도.

    아무튼 지금 생각은 이렇다. 4장의 개념은 원리적으로는 중요할지 모르지만, 결과적으로 그냥 새로운 문법인 class와 extend 키워드로 결국 끝난다는 것 아닌가? 지나치게 설명이 지루했다고 생각한다. 기술은 계속 발전해 ECMA에서 제안한 새로운 자바스크립트 문법은 점점 상용화 되어 가고 있고, 구 브라우저 들은 어지간하면 지원하지 않는 케이스도 늘어가고 있으며, 트랜스파일러나 웹팩 같은 것도 있는 마당에…

    암튼 5장 이후의 내용도 꽤 흥미롭다. 디자인 패턴, 성능 문제, 코딩 스탠다드, 여러 프레임워크.. 한 번쯤 보고 넘어갈 주제라고 생각한다. 만약 계속 읽게 된다면 이 포스트에 업데이트하자.


    새해 첫날 셀독에 와서 5장 ‘디자인 패턴 실용’을 읽었다. 패턴을 많이 공부 안 한 나는 이런 것 대환영이다. 이미 알던 것도 몇 개 있고, 알게모르게 개발에서 이러면 좋겠구나라고 막연히 생각한 것들도 있고 새로운 것들도 있다. 구체적인 이름, 구조, 기대 효과를 볼 수 있어 뜻깊다. 되게 뇌를 자극받는 느낌이다.

    5장에서 제시하는 자바스크립트 대표 디자인 패턴은 다음과 같다.

    • Module
    • Event delegation
    • Proxy
    • Decorator
    • Init-time branching
    • Self-definine function
    • Momoization
    • Self-invoking constructor
    • Callback
    • Currying

    지금 보니 Promise 가 없긴 하네. 딱히 여기서 모든 패턴을 바이블처럼 살펴보자는 것도 아니니 넘어가자. 이 부분 중요한 내용인 것 같으니 챕터 말미의 정리 부분을 인용하며 참고를 위한 웹페이지 링크를 달도록 하자.

    모듈은 간단히 말해 객체를 모듈로 사용해 여러 기능을 하나로 묶은 것. 쉽게 말해 $.ajax() 같은 함수가 $ 안에 있는 것.

    이벤트 전파는 캡쳐, 타겟, 버블 3단계로 구분하여 이벤트가 된다는 것. (참고: 이벤트 전파)

    프록시는 캐시를 활용할 때나 여러 요청을 하나로 합쳐 서버 요청을 최적화하거나 외부 인터페이스를 간편하게 할 수 있다. (참고: 프록시 패턴)

    데코레이터는 반복되는 기능의 조합을 조금씩 변경해 사용하는 경우 유용하다. (참고: 데코레이터 패턴)

    In-time branching 패턴은 브라우저 호환성을 지원하기 위해 많이 사용한다. 브라우저마다 다른 함수를 지원여부에 따라 브랜칭하여 polyfill 하기도 할 때 자주 본다.

    Self-defining function 패턴은 초기화 작업이 매번 필요한 함수나 클로저에 초개화 내용을 저정해야 하는 함수에 활용하면 좋다.

    메모이제이션 패턴은 캐시와 같은 기능을 수행하여 성능과 서버 요청 최적화에 유용하다. (참고: 메모이제이션 패턴)

    콜백 패턴은 늘 보는 바로 그것.

    커링 패턴은 하나의 공용 함수에 세부적인 기능을 함수로 나누고 싶을 때 유용하다. (참고: 커링 패턴)


    6장 브라우저 환경에서 자바스크립트는 setTimer를 예시를 들어 자바스크립트가 단일 스레드에서 운영됨을 알린다. 이 부분 어느 정도 이해하고는 있는데, 필자 특유의 지나치게 디테일한 설명이 좀 어렵게 느껴지기도 한다. 뭐 나중에 시간이 있다면 더 살펴볼까.

    DOM의 repaint와 reflow를 설명하고 더 나은 성능을 위해 reflow를 줄일 수 있는 방안을 제시하는 것은 흥미로웠다. 솔직히 집중력이 떨어진 시점에 독서했고, 지나치게 장황한 코드에 의존한 흐름이라 눈으로만 읽기 힘들어 설렁설렁 넘어간 부분이긴 하다. 뭐 언젠간 체감할 날이 오겠지.

    웹 워커라는 것을 알게 되어 흥미로웠다. UI 이벤트 핸들링과 로직 핸들링을 아예 다른 스레드로 분리하여 처리하는 것은 처음 봤다. 내 생각에는 워드프레스나 유튜브 API 어딘가에서 이걸 쓰지 않을까? 로그 메시지나 언더스코어 테마 코드 어디서 postMessage라는 걸 봤는데 관련 있는 것 같다.

    웹 기술은 계속 발전하고 따라가는 것도 쉽지 않다. 그리고 좋은 성능을 위한 노력은 정말 끝도 없이 깊게 갈 수 있다는 것을 느낀다. 물론 너무 거기에 경도되어서는 안되지만.


    1월 2일 07장 자바스크립트 성능과 사용자 경험 개선, 08잘 08장 자바스크립트 코딩과 개발 환경, 09장 자바스크립트 표준을 대략 봤다. 7장은 자바스크립트 범위를 넘는 최적화 방법도 있고 너무 겉핥기. 8장도 별 의미 없다. 9장은 ES2015 새로운 문법을 좀 자세히 설명해줘 몇 가지는 볼만했다. 하지만 이미 점점 쓰고 있는 것들이 많아 지금의 나한테는 좀 반복된 주제 같다. 다만 몇몇 문법은 잘 모르던 것들이라 다시 한 번 뒤적일 가치는 있다.

    10장은 진짜 몇 페이지 없고, 이 책이 좀 나온지 오래되었구나 라는 생각을 들게 만든다. jQuery, Underscore 같은 오래된 라이브러리 (물론 여전히 좋은 라이브러리라고 생각하지만)도 나오고… 이것은 그냥 읽었다고 치자. 이렇게 이 책을 한번 완독한 셈 치자.

    전반적으로 IT 관련 책은 뜨거울 때 빨리 읽고 소비하고 버려야 하는 것 같다. 그 중에서도 기술의 변화가 가장 빠른 웹 프론트엔드쪽의 책을 연단위로 묵혀 봤다. 당연히 그동안 변한게 많겠지. 그래서 오래된 설명도 좀 있고 지나치게 깊게 들어가는 설명도 있었지만, 그래도 나름 자바스크립트를 깊게 알고픈 사람에게 초반 몇 챕터는 읽어 보면 좋겠다 싶을 정도로 나름 잘 정리된 책이라고 생각한다.

  • 자동 업데이트 후 이메일 생략하기

    요즘 자동 업데이트는 잘 되는 편인데, 굳이 의미없는 이메일을 받는 건 좀 부담스럽다. 이메일 끄는 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' );
    

  • ownCloud에서 NextCloud로 전환

    둘이 은근히 비슷한 점이 있는 것 같은데… ownCloud보다 NextCloud가 훨씬 더 많은 것을 할 수 있는 것 같다. 이제 ownCloud 보다 NextCloud를 사용한다.

  • 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
            })
        }
    
    // ...
    
    }
  • wp_localize_script()로 숫자를 전달할 때

    아래 예제 코드와 결과를 보면 명확하다.

    wp_localize_script(
    	'foo',
    	'bar',
    	[
    		'initial' => [
    			'value' => 0,
    			'true'  => true,
    			'false' => false,
    			'null'  => null,
    		],
    		'value'   => 11,
    		'true'    => true,
    		'false'   => false,
    		'null'    => null,
    	]
    );
    
    // console.log( 'bar', bar ); in js.
    console.log( ‘bar’, bar );

    wp_localize_script() 로 전달한 연관 배열의 최상위 키의 값들은 모두 문자열로 변경된다. 때때로 PHP 측에서 자바스크립트 쪽으로 값을 전달할 때 타입이 달라 의도치 않은 버그를 낼 때가 있다.

    이것은 wp_localizae_script() 함수의 알짜인 WP_Scripts::localize() 함수 구현에서 이유를 찾을 수 있다.

    ...
    foreach ( (array) $l10n as $key => $value ) {
    	if ( ! is_scalar( $value ) ) {
    		continue;
    	}
    
    	$l10n[ $key ] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' );
    }
    ...
    

    함수 내부에서 배열 내부의 키/값을 순회하면서 값을 문자열로 캐스팅한다. 단, 스칼라가 아닌 경우 무시하고 넘어간다.

    전달된 값을 숫자나 불리언으로 다시 변경하려면 스크립트 초기에 parseInt()parseFloat()을 호출하면 되긴 한다. 불리언은 parseInt()! 연산자를 두 번 사용하면 편리하다.

    하지만 일일이 파라미터마다 일일이 이런 짓을 하는 것은 매우 번거롭다. 가급적 문자열로 변경되는 것을 피해야 할 값들은 최상위 키/값에 값을 두지 말고, 위 예제처럼 배열을 중첩하여 중첩된 배열 안쪽에 두도록 하자.

    출처: wp_localize_script unexpectedly converts numbers to strings

  • 우커머스 장바구니 추가 로직에 대해 노트

    우커머스 장바구니 추가 로직에 대해 노트

    우커머스 상거래 중 기본이 되는 작업 중 하나는 장바구니에 물건을 담는 과정일 것이다. 관련 플러그인 개발을 하면서 자꾸 이 부분을 반복하게 된다. 하여 이 과정을 아주 간단하게 노트한다.

    우커머스에는 기본적으로 네 가지 상품 타입이 존재한다.

    • 기본 상품 (simple)
    • 옵션 상품 (variable)
    • 그룹 상품 (grouped)
    • 외부 상품 (external)

    이중 외부 상품은 사이트 외부의 상품을 단순 링크할 뿐 장바구니를 이용하지 않으므로 노트할 대상에서 제외된다. 그러면 나머지 상품들이 싱글 페이지 폼에서 어떻게 처리되는지 노트한다. 상점의 테마는 storefront를 사용했다.

    폼 핸들러

    우선 폼 핸들러에 대해 노트. 상품을 장바구니에 담는 행위는 상품 싱글 페이지에서 사용자가 선택한 상품과 그 갯수를 폼을 통해 전송하는 것으로 구현한다. 이 때 폼을 전송하는 서버의 URL은 해당 페이지와 동일하다.

    한편 우커머스는 ‘wp_loaded‘ 액션의 콜백으로 WC_Form_Handler::add_to_cart_action() 을 등록한다. 우커머스 플러그인 디렉토리 includes/class-wc-form-handlers.php에 위치해 있으며, 여기서 각 상품의 타입에 따라 각각 다른 세부 폼 핸들러에 대응하도록 구현되어 있다.

    폼에서 상품 ID를 인지하려면 반드시 폼에 ‘add-to-cart’ 변수가 설정되어 있어야 한다. 그래야 핸들러가 제대로 동작한다.

    기본상품

    가장 간단한 상품 구조이다. 단일 상품에 대해 갯수만 선택하면 장바구니에 담긴다. 이것의 폼 구조는,

    add-to-cart정수. 상품의 ID.
    quantity정수. 상품의 개수.

    옵션상품

    대체적으로 같은 상품이지만, 상품의 여러 속성에 따라 각자 조금씩 하위 분류로 구분되는 상품들이다. 옷들이 옵션 상품이 될 수 있는 대표적인 상품이다. 한 상품의 색상, 사이즈에 따라 구분되는 것.

    다른 상품으로 상점 관리에서 미리 상품에 대한 여러 속성을 정의하면, 그 속성의 조합에 따라 미리 하위 상품을 만들어 둔다. 예를 들어 옷의 색상이 흰색, 검정, 2가지가 있고 옷의 사이즈는 소, 중, 대로 3가지가 가능하다고 하자. 그러면 옵션 상품의 하위 상품(variation)으로 총 2×3, 6개가 생성된다. 이 하위 상품은 각자 가격, SKU, 재고, 무게나 크기 같은 상품 정보를 다르게 취할 수도 있다.

    attribute-{$attribute}문자열. 사용자가 선택한 속성값이다.
    $attribute는 속성값을 의미한다. 위 그림에서 ‘색상’에 해당한다. 한글의 경우 URL Encode 된다. 예를 들어 ‘색상’은 ‘%ec%83%89%ec%83%81’로 인코드된다.
    quantity해당 상품의 수량
    add-to-cart옵션 상품의 ID와 동일하다. 핸들러는 여기서 기준이 되는 상품 ID를 캐치한다.
    product_id옵션 상품(variable)의 ID. 결국 add-to-cart 값과 동일하다.
    variation_id사용자가 선택한 속성에 해당하는 하위 집합의 상품 ID 중 하나. 폼 핸들러에서 사용하는 값은 이 쪽이다.

    우커머스는 현재 옵션 상품의 옵션을 설정해 한 번에 하나의 하위 상품만을 장바구니에 담는 것만이 가능하다. 다시 말해 옷을 구매할 때, 검정-소 사이즈를 설정한 다음 장바구니에 넣고, 다시 페이지로 돌아와 흰색-중 사이즈를 설정해 장바구니에 넣어야 한다. 검정-소와, 흰색-중 이 두 조합을 한 번에 선택한 다음 한 번에 장바구니에 넣을 수는 없다.

    그룹상품

    그룹 상품은 몇 개의 상품을 하나로 묶어 한 페이지에서 동시에 여러 상품을 구매할 수 있도록 한다. 연결된 상품이라고 해서 동시에 구매했을 때 할인이 주어지게 하는 옵션은 기본적으로는 없는 것 같다. 단지 몇 개의 상품을 한 페이지에서 한번에 장바구니로 넣게 해주는 것일 뿐.

    qty연관 배열. 키는 선택된 상품의 ID. 값은 해당 상품의 개수.
    add-to-cart그룹 상품의 ID.

    기타

    장바구니 추가 로직에서 딱히 강한 제약은 없다. 폼의 구조만 틀리지 않는다면, 폼의 정보는 싱글 페이지와 무관하다. 가령 48번 상품 싱글 페이지에서 값을 강제로 변경해 50번 상품으로 만들어 장바구니에 담아도, 50번 상품의 상태만 올바르다면 막히는 것이 없다.

    또한 폼의 내용은 마음대로 확장할 수 있고, 확장된 폼의 내용은 적절히 폼 핸들러의 동작을 커스텀하여 처리하면 된다.

  • captcha

    구글 reCaptcha가 월 1백만건 초과건에 대한 사용량에 따라 요금을 내야 한다고 한다. hCaptcha는 좋은 대안이 될 수 있을 것 같다. 엔터프라이즈가 아니라면 무제한으로 사용할 수 있다.

    워드프레스나 설치형으로 작성하려면 BotDetect Captcha 가 대안이 될 것이다.

  • Linux Mint Toggle Touchpad

    #!/bin/bash
    # touchpad_toggle
    SETTING='org.cinnamon.settings-daemon.peripherals.touchpad touchpad-enabled'
    ENABLED=$(gsettings get ${SETTING})
    
    if [[ 'true' = ${ENABLED} ]]; then
        gsettings set ${SETTING} false
    elif [[ 'false' = ${ENABLED} ]]; then
        gsettings set ${SETTING} true
    fi
  • SSH Remote Forwarding으로 개발 서버를 임시 노출하기

    개발 서버는 내부 네트워크 안에 있어 외부에서는 접속이 불가하다. 그러나 가끔 외부 네트워크에 노출해야만 하는 경우가 발생한다. 이 때 SSH 터널링으로 외부에 있는 서버를 경유하여 내부로의 접근이 가능하게 할 수 있다. 다음 스텝을 따라해 보자.

    원격 서버에 접속할 때 포워딩할 포트를 지정한다. 여기서는 12000번이라고 가정한다. 만약 원격 서버도 공유기 같은 장비에 물려 있다면 12000번으로 포트 포워딩이 가능한지 확인한다.

    원격 서버에 /etc/ssh/sshd_config 파일을 여러 아래 설정이 있는지 확인한다.

    GatewayPorts clientspecified

    없다면 추가하고 sshd 서버를 재시작하자.

    이제 로컬 서버에서 원격 서버로의 접속을 시도한다. 만약 로컬 서버가 localhost:8000 에서 돌고 있다고 가정하면 아래처럼 연결한다.

    ssh -p <ssh_port> -NR 0.0.0.0:12000:localhost:8000 <server_address>Code language: CSS (css)

    다른 예로, 만약 로컬 서버가 특정 도메인으로, https를 사용한다면 이렇게 할 수 있겠다.

    ssh -p <ssh_port> -NR 0.0.0.0:12000:<local_domain>:443 <server_address>Code language: CSS (css)

    이렇게 하고 https://<server_address>:12000 으로 접속하면 로컬 서버에 접속한 것처럼 동작한다. 물론 원격 서버를 거쳐 우회하는 것이기 때문에 속도는 살짝 느릴 것이다.

    또한 웹 서버의 도메인이 싹 바뀌는 효과가 있기 때문에, 워드프레스 같은 앱을 돌릴 때는 wp-config.php에서 미리 WP_HOME, WP_SITEURL 상수를 미리 지정해야 할 것이다.

    SSH 접속은 일정 시간이 지나면 끊어질 수 있다. 영속적인 접속을 유지하려면 autossh 같은 툴을 사용하면 된다. autossh를 설치, 명령을 ssh로 넣던 것을 autossh로 변경하면 된다.

    autossh -p <ssh_port> -NR 0.0.0.0:12000:<local_domain>:443 <server_address>Code language: CSS (css)
  • 혼잣말

    당뇨는 습관의 질병.

  • Heartbeat 주기 설정하기

    Heartbeat Control 같은 플러그인이 있지만, 굳이 플러그인까지 쓸 필요 없다. 간단한 몇 줄의 코드로 주기 설정이 가능하다.

    우선, heartbeat가 어떻게 동작하는지 간단하게 설명한다. heartbeat는 특정 관리자 페이지 접속시 브라우저와 주기적으로 통신한다. 이 주기는 자바스크립트의 setTimeout() 함수로 구현되며, 기본값은 관리자 페이지에 포커스가 있을 때 15초, 해당 페이지에서 포커스를 잃었을 때 120초로 설정된다. 이 120초는 하드코딩되어 있어 값을 변경하기 어렵지만 포커스가 있을 때 15초는 비교적 손쉽게 변경 가능하다.

    우선 heartbeat을 완전히 없애는 방법이다. 그다지 추천하지는 않지만 개발환경에는 그럭저럭 고려해 볼만할 것이다.

    add_action( 'admin_init', function () {
    	if ( wp_script_is( 'heartbeat', 'registered' ) ) {
    		wp_deregister_script( 'heartbeat' );
    	}
    } );

    heartbeat을 변경하되, 주기를 설정하려면 다음처럼 하면 된다.

    add_action( 'admin_init', function () {
    	// 5, 10, 15, 30, 60, 120, 'long-polling'
    	$script = "jQuery(document).ready(function () {wp.heartbeat.interval(20)});";
    	wp_add_inline_script( 'post', $script );
    	wp_add_inline_script( 'inline-edit-post', $script );
    	add_action( 'customize_controls_enqueue_scripts', function () use ( $script ) {
    		wp_add_inline_script( 'heartbeat', $script );
    	} );
    }, 100 );
    
    add_filter( 'heartbeat_settings', function ( $settings ) {
    	$settings['interval'] = 20;
    	return $settings;
    } );

    wp.heartbeat.interval(20) 부분과 $settings['interval'] = 20; 부분의 20은 heartbeat의 주기를 20초로 설정하라는 부분이다. 적절히 변경해서 사용하면 될 것이다. 당연히 이게 불편하면 플러그인으로 대체해도 무방하다.

  • 멀티사이트의 멀티사이트, 멀티 네트워크

    멀티 사이트는 여러 워드프레스 사이트를 하나의 설치본으로 관리할 수 있게 한다. 이렇게 만들면 데이터베이스에는 추가적으로 테이블이 생기게 된다.

    이 중 wp_blogs, wp_site 가 눈에 뜨인다. 보통 멀티사이트를 만들면 wp_blogs 에는 멀티로 만든 사이트 목록이 기록된다. 그리고 wp_site는 주로 단일 레코드가 기록될 것이다.

    그런데 wp_site의 레코드를 통해 멀티사이트의 확장이 가능하다. 즉, 하나의 워드프레스 설치본으로 멀티사이트를 여러 개 만드는 것까지 가능하다는 소리다. 즉 이런 계층구도가 생성된다.

    • 멀티사이트 #1
      • 사이트 #1-1
      • 사이트 #1-2
    • 멀티사이트 #2
      • 사이트 #2-1
      • 사이트 #2-2

    이렇게 하려면 데이터베이스에 여러 레코드를 편집하면 되는데, 편집해야 할 양이 좀 많아 관리하기 까다롭다. wp_site, wp_blogs, {prefix}_{blog_id}_* 테이블을 괸리해야 하기 때문이다. 이 정도는 WP Multi Network 플러그인에게 맡기는 것이 좋을 것 같다. 멀티사이트 관련 API에 큰 변동이 없어 그런지 이 플러그인은 현재도 잘 동작한다.

    네트워크까지 관리할 수 있다.
    DB는 이렇다. 네트워크 사이트가 하나 더 생겼다.
    site_id가 3인 녀석이 등장했다.

  • 혼잣말

    즐거운 삶을 살자.

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

  • 혼잣말

    나는 뮐 위해 사는 건지?

  • 혼잣말

    검색엔진이 사랑하는 내 블로그. 아니 검색엔진만 사랑하는 내 블로그. ㅋㅋ.