WordPress mu-plugin으로 자동화 문제 해결하기

약 18분 읽기

플러그인도 테마 수정도 답이 아니었다

처음에는 블로그 자동화가 그렇게 어려운 일은 아닐 줄 알았다. WordPress의 기본 기능을 활용하면 충분하리라 생각했다. 하지만 막상 시작해 보니, 플러그인 설치로는 해결되지 않는 문제가 많았다. 업데이트가 있을 때마다 기능이 깨지거나, 원하지 않는 버그가 발생했다. 플러그인을 사용하는 것은 매번 지뢰 찾기를 하는 기분이었다.

테마를 수정하는 것도 시도해봤다. 처음에는 꽤 괜찮아 보였다. 원하는 대로 블로그의 외관과 기능을 조절할 수 있었으니까. 하지만 테마를 업그레이드하거나 다른 테마로 바꿔야 할 때마다 문제가 생겼다. 수정된 부분이 날아가는 건 기본이고, 새로운 버전이나 테마와의 호환성 문제도 있었다. 일일이 다시 수정하는 건 큰 부담이었다.

플러그인이나 테마 수정이 아니라 다른 방법이 필요하다는 것을 깨달았다. 지속적인 유지보수와 호환성에 신경을 덜 쓸 수 있는 방법이 필요했다. 결국, mu-plugins을 사용하게 되었고, 이게 정답이었다. 관리자 화면에서 비활성화할 수 없어서, 누군가 실수로 기능을 끄는 일은 없었다. 그리고 무엇보다도 업데이트나 테마 변경에도 안전했다.

플러그인과 테마 수정이 항상 최선의 해결책은 아니었다. 문제를 깊이 있게 이해하고 직접 구현하는 것이 더 나은 결과를 가져왔다. 시간이 더 걸리더라도, 안정성과 지속 가능성을 확보할 수 있는 방법을 찾는 게 중요했다.

mu-plugins를 고른 이유

WordPress 환경에서의 자동화는 종종 예기치 않은 도전 과제를 제시한다. 나는 이러한 문제를 해결하기 위해 mu-plugins를 선택했다. mu-plugins는 활성화나 비활성화가 불가능하다는 독특한 장점이 있다. 이 특성 덕분에 핵심 기능이 실수로 비활성화되는 것을 방지할 수 있다. 특히, 블로그 자동화 스크립트가 의존하는 기능이기에 더욱 중요했다.

mu-plugins는 업데이트와 테마 변경에도 안전하다는 점에서 큰 장점이 있었다. 일반적인 플러그인이나 테마를 수정하는 방법은 업데이트 시 위험을 초래할 수 있다. 그러나 mu-plugins는 WordPress가 시작될 때 자동으로 로드되므로, 이러한 위험에서 비교적 자유롭다는 점이 매력적이다. 블로그와 관련된 중요한 기능들이 안정적으로 유지될 수 있다는 것은 큰 이점이었다.

또한, mu-plugins는 나만의 환경을 구축하는 데 있어서도 유리했다. 다른 개발자나 사용자로부터 독립적으로 작동할 수 있기 때문에, 내가 원하는 방식으로 기능을 설계하고 구현할 수 있었다. 이렇게 고유한 방식으로 시스템을 구성할 수 있었던 것은 mu-plugins를 사용하면서 얻게 된 또 다른 이점이다. 무엇보다도, 관리자 화면에서 쉽게 비활성화되지 않으므로, 자동화된 기능의 지속적 운영이 가능했다.

물론 mu-plugins를 사용하는 데에도 한계가 있다. 복잡한 기능이나 외부와의 연동이 필요할 때는 일반 플러그인처럼 독립적인 설정이나 관리 UI를 제공할 수 없다는 점이 아쉬웠다. 그러나 내가 필요한 자동화 기능에는 이러한 제약이 크게 영향을 미치지 않았고, 오히려 이러한 간결함이 나의 개발 철학과 잘 맞아떨어졌다.

REST 라우트가 계속 404였던 이유

처음에는 REST API의 라우트를 등록하는 위치가 문제가 될 줄 몰랐다. 단순히 코드를 작성하고 저장하면 알아서 동작할 줄 알았는데, 계속해서 404 에러가 발생했다. 원인을 파악하기 위해 로그를 뒤져봤지만 뚜렷한 해결책이 보이지 않았다. 서버 설정이나 경로 문제라고 생각해 이리저리 수정해봤지만 반나절이 지나도 상황은 나아지지 않았다.

문제의 원인은 REST API의 라우트를 등록하는 시점에 있었다. 나는 이를 플러그인 파일의 최상단에서 선언했는데, 이는 워드프레스의 초기화 과정 이전에 실행되는 코드였다. 그래서 워드프레스가 REST API 요청을 처리하기 전에 라우트가 등록되지 않아 404 에러가 발생한 것이었다. 이를 해결하기 위해 rest_api_init 훅 안에 라우트를 등록하는 코드를 옮겼다.

이 작은 실수가 큰 혼란을 초래했지만, 결국 해결했다. rest_api_init 훅 안에 라우트를 등록한 후로는 더 이상 404 에러가 발생하지 않았다. 워드프레스의 초기화 시점에 대한 이해가 부족했던 것이 화근이었다. 이 경험을 통해 워드프레스의 구조와 작동 방식에 대해 조금 더 깊이 이해하게 됐다.

읽기는 열고 쓰기는 잠근다

REST API를 개발하면서 가장 큰 고민거리 중 하나는 데이터 보호와 접근 제어였다. 특히, 쓰기 작업을 허용하게 되면 의도치 않은 데이터 조작이 발생할 수 있어 보안 문제가 크게 우려됐다. 이 문제를 해결하기 위해 인증 메커니즘을 강화하고자 했다. API에서 쓰기 작업을 수행할 때 반드시 키를 검사하도록 설계해, 불필요한 접근을 차단하는 방식으로 접근했다.

워드프레스의 REST API는 기본적으로 전 세계에 열려 있다. 그래서 보안상 인증이 필수적이다. 기본 인증 방식이 복잡하다는 점 때문에, 단순한 키 기반 인증을 적용했다. 이 방식은 X-Autoflow-Key라는 커스텀 헤더를 통해 수행되며, 서버에 저장된 상수 값과 비교해 유효성을 검증한다. 코드는 복잡하지 않지만, 이 방식을 도입하니, 보안이 한결 강화되었다.

<?php
// /wp-content/mu-plugins/autoflow-enhancements.php
// mu-plugins 는 활성화가 필요 없고 관리자가 끌 수도 없다.
// 자동화가 의존하는 기능이라 실수로 꺼지면 안 됐다.

define('AUTOFLOW_INTERNAL_KEY', getenv('AUTOFLOW_KEY') ?: 'change-me');

add_action('rest_api_init', function () {

    // 읽기 전용 — 공개해도 무해하다
    register_rest_route('autoflow/v1', '/used-photo-ids', [
        'methods'             => 'GET',
        'callback'            => 'autoflow_used_photo_ids',
        'permission_callback' => '__return_true',
    ]);

    // 쓰기 — 반드시 키를 검사한다. __return_true 를 쓰면 안 된다
    register_rest_route('autoflow/v1', '/internal-links/(?P<id>\d+)', [
        'methods'             => 'POST',
        'callback'            => function ($req) {
            return rest_ensure_response(
                autoflow_insert_inline_links((int) $req['id'])
            );
        },
        'permission_callback' => function ($req) {
            return hash_equals(
                AUTOFLOW_INTERNAL_KEY,
                (string) $req->get_header('X-Autoflow-Key')
            );
        },
    ]);
});
// rest_api_init 훅 안에 넣어야 한다.
// 밖에 두면 라우트가 등록되지 않고 계속 404 가 난다.

mu-plugin에 REST 엔드포인트 — 쓰기는 반드시 키 검사

코드 작성 시, 읽기 전용 엔드포인트와 쓰기 작업을 구분하는 데 주력했다. 읽기 전용 엔드포인트는 누구나 접근 가능하도록 설정했지만, 쓰기 작업에 대해서는 엄격하게 제한을 두었다. 키 기반 인증을 통해 실제로 유효한 요청만 처리되도록 했고, 이로 인해 불필요한 보안의 허점이 사라졌다. 또한, permission_callback을 별도로 지정하여, 쓰기 작업에는 인증이 필수적이라는 것을 명확히 했다. 덕분에 엔드포인트의 안정성과 보안성을 동시에 확보할 수 있었다.

같은 사진이 두 번 나오는 문제

블로그 글을 작성하다 보면 동일한 사진이 반복적으로 사용되는 문제가 발생할 수 있었다. 특히 많은 글을 자동으로 생성하면서, 같은 이미지를 여러 글에 실수로 삽입하는 경우가 종종 있었다. 이는 이미지가 중복되면 사용자 경험을 해칠 뿐만 아니라 블로그의 전문성을 떨어뜨릴 수 있는 문제였다. 이런 문제를 해결하기 위해 본문과 첨부파일을 모두 확인하여 중복된 이미지를 피할 수 있는 방법을 구상했다.

Unsplash에서 이미지를 가져오는 과정에서, 이전에 사용했던 이미지가 다시 선택되는 일을 방지하기 위해 새로운 엔드포인트를 만들었다. 이 엔드포인트는 이미 사용된 이미지의 ID를 전부 수집하여 반환한다. 이렇게 해서 포스트를 작성할 때마다 이미지를 신중하게 선택할 수 있도록 하였다. 특히 본문 내용에 삽입된 이미지 URL뿐만 아니라 미디어 라이브러리에 저장된 첨부파일까지 훑어 중복된 이미지를 골라내는 로직을 설계했다.

<?php
function autoflow_used_photo_ids() {
    global $wpdb;
    $ids = [];

    // 1) 본문에 박힌 사진 URL
    $rows = $wpdb->get_results(
        "SELECT post_content FROM {$wpdb->posts}
          WHERE post_status = 'publish' AND post_type = 'post'",
        ARRAY_A
    );
    foreach ($rows as $row) {
        preg_match_all('#/photos/(\d+)/#i', $row['post_content'], $m);
        foreach ($m[1] as $id) $ids[] = (int) $id;
    }

    // 2) 첨부파일 파일명에 심어둔 ID
    //    업로드할 때 autoflow-uniq-{포스트ID}-{사진ID}.jpg 로 저장해둔다.
    //    본문에서 지워져도 여기서 잡힌다.
    $att = $wpdb->get_results(
        "SELECT meta_value FROM {$wpdb->postmeta}
          WHERE meta_key = '_wp_attached_file'
            AND meta_value LIKE 'autoflow-uniq-%'",
        ARRAY_A
    );
    foreach ($att as $row) {
        if (preg_match('/autoflow-uniq-\d+-(\d+)\./', $row['meta_value'], $m2)) {
            $ids[] = (int) $m2[1];
        }
    }

    return rest_ensure_response(array_values(array_unique($ids)));
}

같은 사진이 두 번 나오지 않게 — 본문과 첨부파일 둘 다 훑는다

이미지의 중복 여부를 확인할 때, 본문과 첨부파일을 둘 다 확인하지 않으면 중복이 생길 수 있다. 두 가지 경우를 모두 고려해야 완전한 중복 방지를 구현할 수 있었다. 이 과정에서 이중으로 확인하는 로직이 필요했지만, 한 번 체크하는 데 걸리는 시간과 리소스를 줄이기 위해 빠른 알고리즘을 고민했다. 결과적으로 이 기능을 통해 같은 사진이 두 번 나오는 것을 막을 수 있었고, 자동화된 블로그 생성 과정에서 이미지 중복 문제를 효과적으로 해결할 수 있었다.

테마와 독립적으로 UI 붙이기

워드프레스 테마를 수정하다 보면 테마 업데이트나 교체 시 변경 사항이 사라지는 문제가 발생한다. 특히 UI 요소를 추가하는 과정에서 각 테마의 구조와 스타일에 의존하게 되면 유지보수가 어렵다. 이런 이유로 테마를 수정하지 않고도 카테고리 탭 바를 추가할 방법을 고민했다. mu-plugins를 활용하면 테마와 독립적으로 기능을 추가할 수 있다는 점에 착안하여 이 문제를 해결했다.

이 방법은 테마에 의존하지 않고 카테고리 탭 바를 사이트에 표시할 수 있게 해준다. mu-plugins에 코드를 추가하여, 사이트가 로드될 때마다 특정 위치에 카테고리 탭 바가 자동으로 생성된다. 이렇게 하면 테마 변경 시에도 해당 기능이 그대로 유지되며, 새로운 테마에서도 일관된 사용자 경험을 제공할 수 있다.

<?php
// 테마 파일을 고치면 테마를 바꿀 때 전부 날아간다.
// 훅으로 끼워넣으면 테마와 독립적이다.
add_action('generate_after_header', function () {

    $current  = get_queried_object();
    $cur_slug = ($current instanceof WP_Term) ? $current->slug : '';
    $is_new     = is_page('new');
    $is_popular = is_page('popular');

    echo '<div class="af-cat-wrap"><div class="af-cat-inner">';

    $cls = $is_new ? ' af-on' : '';
    echo '<a href="' . home_url('/new/') . '" class="af-cat-link' . $cls . '">신규</a>';
    $cls = $is_popular ? ' af-on' : '';
    echo '<a href="' . home_url('/popular/') . '" class="af-cat-link' . $cls . '">인기</a>';
    echo '<span class="af-cat-sep">|</span>';

    // 카테고리를 늘릴 때 여기 한 줄만 고친다
    $cats = [
        'all'      => '전체',
        'ai-trend' => 'AI Trend',
        'finance'  => 'Finance',
        'it-tips'  => 'IT Tips',
        'devlog'   => '개발로그',
    ];
    foreach ($cats as $slug => $label) {
        $url = $slug === 'all'
             ? home_url('/all/')
             : get_category_link(get_category_by_slug($slug));
        $active = !$is_new && !$is_popular && (
            ($slug === 'all' && is_home()) ||
            ($slug !== 'all' && $cur_slug === $slug)
        );
        printf('<a href="%s" class="af-cat-link%s">%s</a>',
               esc_url($url), $active ? ' af-on' : '', esc_html($label));
    }

    echo '</div></div>';
}, 10);

테마를 안 건드리고 카테고리 탭 바 붙이기

코드 작성 시, 우선적으로 염두에 둔 부분은 호환성과 유지보수였다. 테마의 헤더나 본문 구조가 변경되더라도 무방하게 UI를 부착할 수 있도록 했다. 또한, CSS 스타일을 최소화하고 필요한 자바스크립트만을 로드함으로써 성능 저하를 방지했다. 결국 이렇게 작성된 코드를 통해 사용자에게 일관된 UI를 제공하고, 관리 측면에서도 편리함을 확보할 수 있었다.

1700줄이 된 지금

코드가 1700줄에 이르렀다. 처음 시작할 때는 이렇게 길어질 줄 몰랐다. 매번 기능을 추가할 때마다 구조를 다시 살펴보고, 불필요한 코드가 없는지 검토하는 과정이 반복되었다. 하지만 모든 기능이 유기적으로 연결되는 걸 보며 나름의 뿌듯함도 느꼈다. 코드를 정리하고 주석을 추가하면서 가독성을 높이는 데도 신경을 많이 썼다. 덕분에 나중에 어떤 부분을 수정하거나 기능을 추가할 때 시간을 크게 절약할 수 있었다.

최근에는 유지보수의 어려움이 조금씩 드러났다. 코드가 방대해지다 보니, 작은 오류 하나가 전체 시스템에 영향을 미치는 경우가 생겼다. 이럴 때마다 디버깅에 소요되는 시간은 상당했다. 테스트의 필요성을 다시 한번 실감하게 되는 순간들이었다. 다양한 시나리오에 맞춘 테스트 케이스를 작성하는 데 시간을 더 투자해야겠다고 마음먹었다.

또한, 새로운 요구사항이 생길 때마다 코드의 확장성을 고민해야 했다. 프로젝트 초기에는 단순히 기능 구현에 집중했지만, 이제는 코드의 모듈화와 유연성도 중요하게 생각하게 됐다. 기존 기능이 깨지지 않도록 하면서도 새로운 기능을 추가할 수 있는 방법을 찾는 것은 늘 어려운 도전이었다. 그 과정에서 코드를 재구성하기도 했고, 더 나은 방식을 찾아 개선하기도 했다.

앞으로는 코드의 유지보수성과 확장성을 더욱 염두에 두고 작업해야겠다는 생각을 했다. 코드가 길어질수록 이를 관리하는 방법이 더욱 중요해진다. 이런 경험을 통해 많은 것을 배웠고, 앞으로의 프로젝트에서도 이런 과정을 잘 살려나가고 싶다.

함께 볼 글

이 글과 이어지는 내용은 아래에 정리해뒀다.

황민

황민 (Hwang Min)

IT·RPA·AI 분야 개발자. 웹앱 개발, UiPath RPA, n8n 자동화 실무 경력 4년. AI·금융·IT 트렌드를 현장 개발자 시각으로 분석합니다.

댓글 남기기

𝕏fin