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

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

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

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

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

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

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

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

  • 백업할 때 기억할 것 노트

    마운트할 디스크가 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 인 건 자명하겠지?

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

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

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

    (더 보기…)
  • 워드프레스 플러그인에서 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)
  • 아파치 워드프레스 관리자 IP 접속 제한

    주로 보안 제약이 강한 대기업 블로그에서 주로 요구하는 사항인데, 관리자나 로그인 페이지의 접근을 IP 대역으로 막는다.

    주의할 점은 관리자 페이지를 막아도 admin-ajax.php, admin-post.php 의 접근은 열어 두어야 한다는 거다. 그것까지 다 막으면 워드프레스의 일반적인 폼 제출과 AJAX 요청까지 다 틀어막혀 사이트 동작이 되지 않을 수 있다.

    아래 코드를 참고하여 아파치 설정에 추가한다. LocationMatch 지시자는 .htaccess 에 넣을 수 없다는 것에 유의한다. 그리고 .htacess 파일에 넣지 못하고 일반 설정 파일에 삽입하니, 수정 후 서버 재시작을 꼭 해주는 것을 잊지 말자.

    <LocationMatch "wp-admin/(?!admin-ajax|admin-post).*\.php$|wp-signup\.php$|wp-login\.php$">
    Require all denied
    Require ip 127.0.0.1
    </LocationMatch>
    # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
  • 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'));
    
  • 자동 업데이트 후 이메일 생략하기

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

  • 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초로 설정하라는 부분이다. 적절히 변경해서 사용하면 될 것이다. 당연히 이게 불편하면 플러그인으로 대체해도 무방하다.

  • 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. 새로운 도메인으로 접속하여 워드프레스 설치를 마친다.

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

    ‘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을 이용하는 방법도 있으나, 이 방법의 장점은 플러그인 헤더까지 번역문으로 넣어준다는 것이다.

  • 페이지 목록 관리의 깨알 팁

    페이지 목록 관리의 깨알 팁

    사이트 페이지 목록을 만들 때 정돈되어 있으면 더욱 효율적입니다. 메인 페이지나 중요한 페이지를 1페이지에 빠르게 로드할 수 있다면 더 좋겠지요? 이럴 때 쓰는 것이 페이지 “순서” 속성입니다.

    페이지 순서는 기본이 0입니다. 그래서 보통보다 뒤로 보내려면 양수를, 반대로 앞으로 가져오려면 음수로 둡니다. 그러면 1페이지 상위에 가장 자주 관리하는 메뉴를 먼저 볼 수 있게 되어 편리합니다.

    메인 페이지나 자주 편집하는 주요 페이지를 먼저 출력해 두면 편리합니다.
  • 디버그 로그는 가려놓읍시다!

    디버그 로그는 가려놓읍시다!

    워드프레스는 흔히 카페24 같은 호스팅을 사용한다든지, 이런 오픈된 개발 서버에서 작업되는 경우도 흔합니다. 그리고 워드프레스의 동작을 보다 정밀하게 파악하기 위해, 아래 같은 설정을 사용하기도 합니다.

    (더 보기…)