사이트 전역 데이터를 위한 워드프레스 커스텀 옵션 페이지 추가

블로그 또는 사이트의 기본 정보를 등록하는 워드프레스 관리페이지를 보통 옵션 페이지라고 말합니다. 설정 페이지로 부르기도 하며, 메뉴를 클릭해서 나오는 페이지이므로 메뉴 페이지라 불러도 전혀 문제가 없습니다.

옵션 페이지에서 저장한 데이터는 데이터베이스 {prefix}_options 테이블에 저장됩니다. 사이트 주소 등의 기본 정보, 댓글, 미디어, 고유주소 기준, 기한이 있는 캐시 데이터(transient), 워드프레스 스케줄 등의 데이터가 모두 옵션 데이터로 저장됩니다. 옵션 데이터로 처리하면 데이터를 구하는 과정이 간편하고, 다른 데이터 요소와의 관계가 없으므로 많은 플러그인에서 특정 데이터는 옵션 데이터로 추가하여 특정 기능을 구현하는 데 사용하는 것이 일반적입니다.

옵션 데이터는 포스트와 포스트 메타, 사용자와 사용자 메타, 텀(term)과 텀 메타, 댓글(comment)과 댓글 메타, 특정 사용자의 포스트에 해당하는 포스트 메타, 특정 사용자의 포스트와 연관된 텀 메타 데이터 같은 데이터 관계가 없어 사이트나 블로그의 전역 데이터로 사용하는 것이 일반적입니다.

이 글에서는 워드프레스 사용자가 대부분 추가하는 구글의 웹사이트 분석 추적 코드와 구글 사이트 인증 코드를, 최상위 메뉴의 옵션 페이지를 만들어 옵션 필드를 구성한 후, 옵션 데이터로 저장하고, wp_headwp_footer 훅으로 사이트에 추가하는 방법을 간략하게 전달합니다.

먼저 말하면, 워드프레스에서 관리페이지 메뉴 추가와 옵션 페이지 추가는 60초면 가능하지만, 데이터를 저장하는 필드를 하나만 추가하는 데에도, 입문자에게는 3시간이 걸릴 수도 있으며, 이 글을 쓰는 저도 매일 추가 필드를 만들지 않기에 기억을 더듬고 찾느라 빨라야 10분이 걸립니다. 그런데 완료하고 나면 너무 간단합니다.

워드프레스는 사용자가 관리페이지에 필요한 메뉴와 페이지, 필드를 쉽게 구성할 수 있도록 Settings API, Options API 등을 제공하고 있습니다. 그런데 Settings API로 옵션 필드를 구성할 때 Html 마크업으로 세부 필드를 정의하는 것은 상대적으로 간단하고 혼동되지 않지만, 옵션 필드와 옵션 필드의 섹션을 옵션 페이지 내의 최상위 섹션(Section) 그룹과 연결(설정)하는 작업이 처음 경험할 경우 아주 복잡하게 느껴집니다.

그런지 몰라도 다음처럼 옵션 페이지 관련 코드를 생성해주는 서비스도 있습니다. 물론 커스텀 포스트 타입이나 커스텀 분류(taxonomy), 커스텀 필드 메타 박스 등을 코드로 생성해주는 사이트도 있지만, 경험적으로 볼 때 옵션 페이지는 변환 사이트를 통해 코드를 획득하는 것이 더 복잡합니다. 직접 구성하는 게 오히려 시간을 줄입니다.

따라서 워드프레스의 옵션 페이지를 구성한다면 처음에는 제법 혼동할 가능성이 있다는 것을 염두에 두고, 하나의 최상위 섹션 그룹에 하나의 옵션 필드 섹션과 그룹만 구성하면, 추가 옵션 필드 섹션과 그룹은 대체로 빨리 구성할 수 있다는 정도만 기억하세요. Settings API 방식을 사용하지 않고 옵션 페이지를 구성하는 것도 가능하지만, Settings API를 사용하면 얻을 수 있는 이점(보안적, 데이터 저장 등)이 더 많습니다.

글에서는 상세한 설명을 하지 않습니다. 혹 따라 한다면 자신의 사이트에 나오는 결과를 보고 이해하세요. 나중에 관심이 있다면 글의 뒤에 있는 최종 코드를 보고 유추하여 다시 직접 해보세요. 모든 게 마찬가지지만, 워드프레스 옵션 페이지 추가는 직접 경험하지 않고 그림이나 글자만으로 방법을 습득하기 어렵습니다.

플러그인으로

혹시 따라 하는 분들을 위해, 또 따라 하시는 분들의 사용 환경을 알 수 없으므로 활성화 방식의 플러그인으로 설명하고자 합니다. 아래 코드를 추가하여 my-site-options.php 이름의 파일을 만듭니다.

<?php
/**
 * Plugin Name: 옵션 페이지 추가
 * Description: 최상위 메뉴와 페이지 추가, 사이트에 추가로 필요한 옵션 데이터 관리
 * Author: 방문자와 운영자
 */

만든 파일을, 사용하는 워드프레스 wp-content > plugins 디렉터리에 올리고, 플러그인 목록 페이지에서 이 플러그인을 활성화합니다.

최상위 메뉴와 옵션 페이지 추가

영문 버전에서는 ‘Settings’, 한글 버전에서는 ‘설정’ 메뉴가 특정 플러그인을 설치하지 않았다면 관리페이지 메뉴 맨 아래에 있습니다. 다음 코드는 그 메뉴 바로 위에 ‘옵션’이라는 메뉴를 추가합니다.

<?php
/**
 * Plugin Name: 옵션 페이지 추가
 * Description: 최상위 메뉴와 페이지 추가, 사이트에 추가로 필요한 옵션 데이터 관리
 * Author: 방문자와 운영자
 */

// 최상위 메뉴와 관리(옵션) 페이지 추가
add_action( 'admin_menu', 'register_custom_site_options_page' );
function register_custom_site_options_page() {
    add_menu_page(
        '사이트에 사용할 옵션 데이터', // 페이지 제목
        '옵션', // 메뉴 이름
        'manage_options', // capability (권한)
        'myoptions', // slug
        'myoptions_func', // 옵션 페이지 콜백 함수
        'dashicons-image-filter', // 메뉴 아이콘
        79 // 80 (Settings, 설정)
    );
}

우선, 브라우저 주소 필드 맨 끝에 ‘myoptions’를 볼 수 있습니다. 현재 상태에서 ‘옵션’ 메뉴를 클릭하면 빈 페이지가 나옵니다. ‘옵션’ 글자 앞에 3개의 동그라미 아이콘은 관리페이지에서 사용하는 워드프레스 Dashicon이며, 프런트에서도 사용할 수 있습니다. 아이콘은 위의 코드 dashicons-image-filter의 결과입니다.

Developer Resources: Dashicons

‘옵션’ 메뉴가 설정(한글) 또는 Settings(영문) 위에 나오는 것은 위의 코드 ’79’의 결과이며, 메뉴 순서는 아래 링크에서 확인할 수 있습니다.

Default: bottom of menu structure

위의 코드에서 add_menu_page 대신에 add_options_page 함수로 변경하면 한글 버전 기준으로 ‘설정’ 메뉴 하위에 ‘옵션’ 메뉴가 추가되며, add_posts_page 함수로 변경하면 ‘글’ 메뉴 하위에 ‘옵션’ 메뉴가 추가됩니다.

// 다음을 참고.
add_*_page (
    add_menu_page(),
    add_options_page(),
    add_posts_page(),
    add_dashboard_page(),
    add_posts_page(),
    add_media_page(),
    add_links_page(),
    add_pages_page(),
    add_comments_page(),
    add_theme_page(),
    add_plugins_page(),
    add_users_page(),
    add_management_page()
);

다음 코드는 add_menu_page 함수에서 정의한 옵션 페이지 전체의 콜백 함수를 추가한 것으로, 추가한 옵션 페이지 안에 제목과 버튼을 확인할 수 있습니다.

<?php
/**
 * Plugin Name: 옵션 페이지 추가
 * Description: 최상위 메뉴와 페이지 추가, 사이트에 추가로 필요한 옵션 데이터 관리
 * Author: 방문자와 운영자
 */

// 최상위 메뉴와 관리(옵션) 페이지 추가
add_action( 'admin_menu', 'register_custom_site_options_page' );
function register_custom_site_options_page() {
    add_menu_page(
        '사이트에 사용할 옵션 데이터', '옵션', 'manage_options', 'myoptions', 'myoptions_func', 'dashicons-image-filter', 79
    );
}

// 옵션 페이지 콜백 함수, myoptions_func
function myoptions_func() {
    if ( isset( $_GET['settings-updated'] ) ) {
        // 옵션 데이터 저장 후 메시지
        add_settings_error( 'myoptions_messages', 'myoptions_message', '옵션 데이터를 저장함.', 'updated' );
    }
    settings_errors( 'myoptions_messages' );
?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); // 페이지 제목 출력 함수 ?></h1>
        <form method="post" action="options.php">
<?php
            do_settings_sections( 'myoptions' );
            settings_fields( 'myoptions_section' );
            submit_button( '옵션 데이터 저장' ); // 관리페이지에서만 사용 가능
?>
        </form>
    </div>
<?php
}

이후 나오는 코드로 위의 코드를 이해하기 바라며, 여기까지의 코드가 하나의 옵션 페이지를 구성하는 기본 최상위 구조입니다. 이 설정은 다중 섹션 그룹이 가능한 것으로, 이 글에서는 하나의 섹션 그룹만 구성할 것입니다. 또 그 섹션 그룹에 속할 2개의 필드 그룹을 정의하고, 각 필드 그룹을 마크업하며, 각 필드 그룹의 필드 데이터의 유효성 및 포맷(Valtdate & Sanitize)을 조정하는 함수를 만듭니다.

섹션 그룹과 필드 그룹

먼저, 다음 코드처럼 섹션 그룹(First)과 필드 그룹(A)를 정의합니다.

/**
 * myoptions_section_settings
 * 섹션 그룹
 */
add_action( 'admin_init', 'myoptions_section_settings' );
function myoptions_section_settings() {

    /**
     * myoptions_first_section
     */
    // First 섹션
    add_settings_section(
        'myoptions_first_section', // 섹션 그룹명
        '사이트 전역 코드', // 섹션 제목
        'myoptions_first_text_function', // 섹션 설명 콜백 함수
        'myoptions' // 메뉴 페이지 slug
    );

    // 필드 그룹 A
    add_settings_field(
        'myoptions_headfoot', // 필드 ID
        '사이트 위, 아래 코드', // 필드 이름
        'myoptions_headfoot_field_html_func', // 섹션 필드 그룹 A 마크업 콜백 함수
        'myoptions', // 메뉴 페이지 slug
        'myoptions_first_section' // 섹션 그룹명
    );
    register_setting(
        'myoptions_section', // 옵션 페이지 전역 필드 그룹
        'myoptions_headfoot', // 필드 ID
        'myoptions_headfoot_sanitize_func' // Sanitize 콜백 함수
    );

}

다음 코드를 이어서 추가하여 첫 번째 옵션 필드 그룹 설정을 마무리합니다.

/**
 * myoptions_first_section
 * 섹션 설명 콜백 함수
 */
function myoptions_first_text_function() {
    echo '<p>CSS, 구글 분석 코드 등 사이트 위, 아래에 추가할 코드를 입력합니다.</p>';
}

/**
 * myoptions_first_section
 * 섹션 필드 그룹 A 마크업 콜백 함수
 */
function myoptions_headfoot_field_html_func() {
    $headfoot_options = get_option( 'myoptions_headfoot' );
?>
    <div style="margin-bottom: 1.5em;">
        <textarea id="myoptions_headfoot" name="myoptions_headfoot[header_code]" rows="10" cols="80"><?php echo $headfoot_options['header_code']; ?></textarea>
        <p class="description">사이트 위에 추가할 코드를 입력합니다.</p>
    </div>
    <div style="margin-bottom: 1.5em;">
        <textarea id="myoptions_headfoot" name="myoptions_headfoot[footer_code]" rows="10" cols="80"><?php echo $headfoot_options['footer_code']; ?></textarea>
        <p class="description">사이트 아래에 추가할 코드를 입력합니다.</p>
    </div>
<?php
}

위의 코드에서 2개의 옵션 데이터를 시리얼(serial) 데이터로 저장하기 위해 하나의 myoptions_headfoot 필드에 배열로 구성하였습니다. 강제는 아니지만, 최소한 같은 필드 그룹의 데이터는 배열로 정의하는 것이 좋습니다. 하나의 데이터를 하나의 개별 필드 그룹으로 만들면 번거롭고 비효율적입니다.

다음 그림은 현재 과정까지의 결과입니다.

옵션 페이지 필드 그룹 A

그림에서 위의 필드에는 다음의 구글 사이트 인증 코드를, 아래 필드에는 구글 웹사이트 분석 추적 코드를 입력하고 저장하면 다음 그림과 같습니다.

옵션 페이지 필드 그룹 A에 데이터 입력

여기서 데이터베이스에 저장된 데이터를 확인하면 다음 그림처럼 따옴표나 꺽쇠 괄호(<, >) 등이 부호화(HTML Character Entity encodes, escape) 되지 않고 그대로 저장됩니다.

부호하되지 않고 저장된 코드

옵션 데이터 포맷 (Sanitizes, Encodes)

다음의 Sanitize 콜백 함수를 플러그인 파일에 이어서 추가합니다.

/**
 * myoptions_first_section
 * 섹션 필드 그룹 A Valtdate & sanitize 콜백 함수
 */
function myoptions_headfoot_sanitize_func( $input ) {
    $input['header_code'] = trim( esc_textarea( $input['header_code'] ) );
    $input['footer_code'] = trim( esc_textarea( $input['footer_code'] ) );
    return $input;
}

위의 코드에서 사용한 esc_textarea 함수는 textarea 안의 데이터를 부호화 하는데, 워드프레스 Data Validation 및 Sanitizing 관련하여 다음 링크 정도의 정보를 탐색하는 것이 도움이 됩니다.

앞의 코드를 추가하여 저장 후 데이터를 확인하면 부호화한 데이터를 확인할 수 있습니다.

부호화한 코드

만약, 옵션 데이터가 숫자라면 콜백 함수를 분리하여 정의하지 않고, 직접 관련 함수를 넣을 수 있습니다.

register_setting(
    'myoptions_section',
    'myoptions_headfoot',
    'intval' // 또는 'absint'
);

옵션 데이터의 적합성

옵션 데이터 저장까지 완료되었으므로 구글 사이트 인증 코드는 wp_head 훅으로 사이트 head 태그 안에, 추적 코드는 wp_footer 훅으로 body 태그 안에 추가할 수 있습니다. 그런데, 사이트 전역에서 추적 코드나 인증 코드와 같은 html, 스크립트 소스 전체를 데이터로 저장하고, 다시 출력하여 사용하는 것보다는 필요한 값만 저장하여 사용하는 방법이 좋겠습니다. 결국 사이트 소스에 추가하기 위해서 훅을 포함한 관련 코드를 다시 추가해야 하니까요.

이 글에서 다루는 인증 코드나 추적 코드를 보면 ‘키’와 ‘추적 ID’만 사용자마다 다른 값을 가지며, 나머지 소스는 누구에게나 같은 것입니다. 결국 옵션 데이터로 저장할 것은 ‘키’와 ‘추적 ID’, 나머지 코드는 파일에 직접 추가하여 옵션 데이터가 추가되도록 처리하는 것이 좋겠습니다. 틀리거나 올바르지 않다는 것은 아닙니다.

이를 위해 필드 그룹을 추가하여 단일 값만을 옵션 데이터로 저장하도록 구성합니다. 이미 만든 필드 그룹(textarea)은 그대로 두어 CSS 코드 등 필요할 경우 사용하면 됩니다.

별도 과정은 생략하고 지금까지의 플러그인 코드에 새로운 필드 그룹과 Sanitize 콜백 함수, 그리고 사이트에 추가하는 이 글의 전체 코드를 다음으로 정리합니다.

<?php
/**
 * Plugin Name: 옵션 페이지 추가
 * Description: 최상위 메뉴와 페이지 추가, 사이트에 추가로 필요한 옵션 데이터 관리
 * Author: 방문자와 운영자
 */

/**
 * 최상위 메뉴와 관리(옵션) 페이지 추가
 */
add_action( 'admin_menu', 'register_custom_site_options_page' );
function register_custom_site_options_page() {
    add_menu_page(
        '사이트에 사용할 옵션 데이터', '옵션', 'manage_options', 'myoptions', 'myoptions_func', 'dashicons-image-filter', 79
    );
}

/**
 * 옵션 페이지 콜백 함수
 * myoptions_func
 */
function myoptions_func() {
    if ( isset( $_GET['settings-updated'] ) ) {
        // 옵션 데이터 저장 후 메시지
        add_settings_error( 'myoptions_messages', 'myoptions_message', '옵션 데이터를 저장함.', 'updated' );
    }
    settings_errors( 'myoptions_messages' );
?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); // 페이지 제목 출력 함수 ?></h1>
        <form method="post" action="options.php">
<?php
            do_settings_sections( 'myoptions' );
            settings_fields( 'myoptions_section' );
            submit_button( '옵션 데이터 저장' ); // 관리페이지에서만 사용 가능
?>
        </form>
    </div>
<?php
}

/**
 * myoptions_section_settings
 * 섹션 그룹
 */
add_action( 'admin_init', 'myoptions_section_settings' );
function myoptions_section_settings() {

    /**
     * myoptions_first_section
     */
    // First 섹션
    add_settings_section(
        'myoptions_first_section', // 섹션 그룹명
        '사이트 전역 코드', // 섹션 제목
        'myoptions_first_text_function', // 섹션 설명 콜백 함수
        'myoptions' // 메뉴 페이지 slug
    );

    // 필드 그룹 A
    add_settings_field(
        'myoptions_headfoot', // 필드 ID
        '사이트 위, 아래 코드', // 필드 이름
        'myoptions_headfoot_field_html_func', // 섹션 필드 그룹 A 마크업 콜백 함수
        'myoptions', // 메뉴 페이지 slug
        'myoptions_first_section' // 섹션 그룹명
    );
    register_setting(
        'myoptions_section', // 옵션 페이지 전역 필드 그룹
        'myoptions_headfoot', // 필드 ID
        'myoptions_headfoot_sanitize_func' // Sanitize 콜백 함수
    );

    // 필드 그룹 B
    add_settings_field(
        'myoptions_unit_code',
        '개별 코드',
        'myoptions_unit_code_field_html_func',
        'myoptions',
        'myoptions_first_section'
    );
    register_setting(
        'myoptions_section',
        'myoptions_unit_code',
        'myoptions_funit_code_sanitize_func'
    );
}

/**
 * myoptions_first_section
 * 섹션 설명 콜백 함수
 */
function myoptions_first_text_function() {
    echo '<p>CSS, 구글 분석 코드 등 사이트 위, 아래에 추가할 코드를 입력합니다.</p>';
}

/**
 * myoptions_first_section
 * 섹션 필드 그룹 A 마크업 콜백 함수
 */
function myoptions_headfoot_field_html_func() {
    $headfoot_options = get_option( 'myoptions_headfoot' );
?>
    <div style="margin-bottom: 1.5em;">
        <textarea id="myoptions_headfoot" name="myoptions_headfoot[header_code]" rows="10" cols="80"><?php echo $headfoot_options['header_code']; ?></textarea>
        <p class="description">사이트 위에 추가할 코드를 입력합니다.</p>
    </div>
    <div style="margin-bottom: 1.5em;">
        <textarea id="myoptions_headfoot" name="myoptions_headfoot[footer_code]" rows="10" cols="80"><?php echo $headfoot_options['footer_code']; ?></textarea>
        <p class="description">사이트 아래에 추가할 코드를 입력합니다.</p>
    </div>
<?php
}

/**
 * myoptions_first_section
 * 섹션 필드 그룹 B 마크업 콜백 함수
 */
function myoptions_unit_code_field_html_func() {
    $unit_code_options = get_option( 'myoptions_unit_code' );
?>
    <div style="margin-bottom: 1.5em;">
        <label>구글 사이트 인증 코드</label><br>
        <input type="text" id="myoptions_unit_code" name="myoptions_unit_code[gsv]" size="80" value="<?php echo $unit_code_options['gsv']; ?>">
        <p class="description">(예) IQCu6xz34rt45jhjhjie5sr5Kpzpxm7pjQ85QOA4</p>
    </div>
    <div style="margin-bottom: 1.5em;">
        <label>구글 추적 아이디</label><br>
        <input type="text" id="myoptions_unit_code" name="myoptions_unit_code[ga]" size="80" value="<?php echo $unit_code_options['ga']; ?>">
        <p class="description">(예) UA-123456-1</p>
    </div>
<?php
}

/**
 * myoptions_first_section
 * 섹션 필드 그룹 A Valtdate & sanitize 콜백 함수
 */
function myoptions_headfoot_sanitize_func( $input ) {
    $input['header_code'] = trim( esc_textarea( $input['header_code'] ) );
    $input['footer_code'] = trim( esc_textarea( $input['footer_code'] ) );
    return $input;
}
/**
 * myoptions_first_section
 * 섹션 필드 그룹 B Valtdate & sanitize 콜백 함수
 */
function myoptions_funit_code_sanitize_func( $input ) {
    $input['gsv'] = trim( wp_filter_nohtml_kses( $input['gsv'] ) );
    $input['ga'] = trim( wp_filter_nohtml_kses( $input['ga'] ) );
    return $input;
}

/**
 * 코드 삽입
 * 실제 사용한다면 분리
 */

// head
add_action( 'wp_head', 'myoption_head_code' );
function myoption_head_code() {
    $unit_code_options = get_option( 'myoptions_unit_code' );
    if ( $unit_code_options[ 'gsv' ] )
    echo '<meta name="google-site-verification" content="' . $unit_code_options[ 'gsv' ] . '" />';
}

// footer
add_action( 'wp_footer', 'myoption_footer_code' );
function myoption_footer_code() {
    $unit_code_options = get_option( 'myoptions_unit_code' );
    if ( !$unit_code_options[ 'ga' ] )
        return false;
?>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '<?php echo $unit_code_options[ 'ga' ]; ?>', 'auto');
ga('send', 'pageview');
</script>
<?php
}

다음 그림처럼 새로 추가한 필드 구룹 B에 데이터를 입력하고, 위의 최종 코드처럼 사이트에 삽입하면 원하는 결과를 얻을 수 있습니다.

옵션 페이지 구성

사이트 소스를 확인하여 데이터가 제대로 출력되는지도 확인하는 것이 필요하며, 위의 그림을 보고 섹션과 필드 그룹에 대해 조금 더 이해할 수 있기를 바랍니다.

정리

아래 링크로 가면 비슷하지만, 부분적으로 약간 다른 예제를 볼 수 있습니다. 참고하면 좋습니다. 또, 섹션 그룹, 필드 그룹은 공식 용어는 아니며, 설명과 이해를 돕기 위해 사용한 것입니다.

Custom Settings Page

이 글에서 추가한 사이트 통계 추적 코드 등은 대부분의 사이트에 존재하는 것이며, 적용을 위해서는 사이트에 삽입해야 하는 요소입니다. 사이트 제작을 업으로 한다면 (이미 있겠지만) 이와 같은 데이터를 쉽게 입력하고 사이트에 추가하는 옵션 페이지 및 삽입 기능은 테마나 플러그인을 통해 제공하는 것이 효율적일 것입니다. 테마 파일 편집 없이 워드프레스를 사용하는 보통의 사용자도 다르지 않을 것입니다.

댓글 3