워드프레스 폼 데이터 처리 방식으로 메일 전송 폼 직접 만들고 Contact Form 7 그만 쓰기, admin-post.php

워드프레스 사용자가 메일 전송 폼으로 사용하는 플러그인 중 대표적인 것으로 Contact Form 7, Jetpack Contact Form 모듈이 있습니다. 더 많지만, 생략합니다. 메일 전송 폼이 필요할 때 Jetpack 사용 중이라면 Contact Form 7 플러그인을 설치할 필요가 보통은 없습니다.

그러나, 보통의 메일 전송 폼은 ‘이름’, ‘보내는 사람 메일 주소’, ‘내용 정도’의 필드 제공으로 해당 정보를 받으며, 내용은 줄 바꿈과 단락 구분 정도의 기능 제공과 제한을 둡니다. 이 정도라면 꼭 플러그인을 설치할 필요가 있을까 생각합니다.

물론, Jetpack Contact Form 모듈은 메일 전송과 함께 커스텀 포스트 타입의 포스트로도 저장이 되며, 목록으로 받을 수 있는 기능을 제공합니다. Contact Form 7 플러그인도 다양한 설정을 포함하고 있으며, 관련 애드온이 많아 원하는 추가 기능을 선택 사용할 수 있습니다.

선택은 자유지만, 공개 플러그인보다 직접 워드프레스 함수와 폼 데이터 처리 방식의 쉬운 흐름으로 직접 메일 전송 폼을 만들어 두면 메일 데이터를 따로 저장하는 등의 나중에 필요한 폼 데이터 활용이 더 편리합니다. 간단하게 만들어 봅니다.

페이지 템플릿과 Shortcode

워드프레스 페이지 템플릿을 만들어 Html 폼과 폼 데이터 처리에 관한 정의를 해당 파일에서 모두 처리해도 됩니다. 그러나, 조금 더 유연한 사용을 위해서 page, post 등 많은 곳에서 쉽게 사용할 수 있는 Shortcode, 필요할 때 테마 템플릿 파일에 직접 함수(템플릿 태그)로 추가할 수 있도록 구성합니다. 어떤 방법이 더 좋다는 뜻은 아닙니다.

Html 폼

먼저, 이 포스트에서는 워드프레스 방식의 폼 데이터 처리 기준으로 메일 전송 폼을 만드는데, 다음처럼 기본적인 Html 폼과 폼 액션 등을 구성합니다.

/**
 * Html Form
 */

add_shortcode( 'mailform', 'custom_form_creation' );
function custom_form_creation(){
    ob_start();
?>
<div style="margin-bottom: 2em">
    <form id="sendmail_form" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="POST">
        <p>모든 입력 필드는 반드시 입력해야 하며, 인용기호는 사용할 수 없습니다. 한 번 보낸 후 다시 보내려면 일정 시간이 지나야 합니다.</p>
        <p>
            <label for="ftitle">문의에 관한 주제를 입력하세요</label>
            <input type="text" id="ftitle" name="ftitle" placeholder="제목" required>
        </p>
        <p>
            <label for="fname">이름 또는 단체명</label>
            <input type="text" id="fname" name="fname" placeholder="이름" required>
        </p>
        <p>
            <label for="femail">이메일 주소를 바르게 입력하세요</label>
            <input type="email" id="femail" name="femail" placeholder="이메일" required>
        </p>
        <p>
            <label for="fcontent">내용을 입력하세요</label>
            <textarea type="textarea" id="fcontent" name="fcontent" maxlength="3000" rows="8" placeholder="줄 바꿈, 단락(빈 줄) 구분만 가능하며, 최대 글자 수 제한이 있습니다. (3000)" required></textarea>
        </p>
        <input type="hidden" name="action" value="sendmail_formdata">
        <?php wp_nonce_field( 'sendmail_formdata', 'sendmail_formdata_field' ); ?>
        <button type="submit" class="submit">보내기</button>
    </form>
</div>
<?php
    return ob_get_clean();
}

5번 줄에서 속성 정의 없는 아주 간단한 Shortcode를 정의하였고, 10번 줄의 폼 액션 경로가 워드프레스 방식의 폼 데이터 처리를 위한 기본 설정입니다. 그냥 액션 URL을 코드와 같이 사용하면 워드프레스 방식입니다. 다른 생각하지 않고, 그대로 사용하면 됩니다.

이때, 워드프레스 방식의 폼 데이터 처리 핵심은 28번 줄 action 이름(name)과 sendmail_formdata 값(value)으로 이름(name)은 action으로, 값(value)은 원하는 것으로 지정하면 됩니다.

29번 줄 wp_nonce_field 함수를 사용하여 최소한의 보안에 관한 필드를 추가하는 것도 기본으로 생각하면 됩니다. 해당 함수는 다른 설정이 없으면 2개의 필드를 자동으로 생성하는데, 이 포스트에서는 다음 2번 줄처럼 액션 이름과 nonce 필드 이름을 따로 지정하였습니다. $referer, $echo 파라미터는 기본 그대로 두는 게 일반적입니다.

wp_nonce_field( $action, $name, $referer, $echo );
wp_nonce_field( 'sendmail_formdata', 'sendmail_formdata_field' );

Html 폼 소스코드 전체를 functions.php 파일에 저장하고, 관리페이지에서 page 또는 post 타입 편집 화면에서 Shortcode를 추가하여 사이트에서 확인하면 폼이 출력됩니다.

메일 폼 Shortcode

Custom Html 위젯에 Shortcode 사용하려면 아래 코드를 functions.php 파일 아무 곳에 추가하면 됩니다.

// Custom Html 위젯에 Shortcode 사용 가능
add_filter( 'widget_text', 'do_shortcode' );

admin-post.php

먼저, 사이트 폼 출력 페이지에서 F12 (크롬 브라우저 기준) 누른 후 다음 그림처럼 적당히 입력 후 보내기 버튼을 클릭합니다.

메일 전송 폼

페이지는 /wp-admin/admin-post.php, 폼에서 요청한 데이터는 다음 그림처럼 나옵니다.

폼 전송 데이터

폼 데이터를 admin-post.php 파일로 보냈을 때 데이터에 action이 있지만, 해당 action을 정의하지 않았으므로 admin-post.php 페이지에 멈춘 상태입니다.

폼 액션

이제는 폼 액션과 폼 데이터를 검증하고 메일을 보냅니다. 워드프레스 폼 데이터 처리 액션과 함수 정의 패턴은 다른 생각하지 않고 다음의 기준을 그대로 사용하면 되고, 이 포스트 기준 다음 코드는 앞에서 추가한 Html 폼 함수 다음에 추가하면 됩니다.

add_action( 'admin_post_nopriv_sendmail_formdata', 'func_sendmail_formdata' );
add_action( 'admin_post_sendmail_formdata', 'func_sendmail_formdata' );
function func_sendmail_formdata() {

    //

}

액션 훅은 admin_post_{action필드의값}, admin_post_nopriv_{action필드의값} 패턴을 가진다는 것을 쉽게 파악할 수 있으며, 이 포스트에서는 action 필드의 값이 sendmail_formdata이므로 위의 코드로 정의할 수 있습니다. func_sendmail_formdata 함수명은 원하는 것으로 만들면 됩니다.

admin_post_, admin_post_nopriv_ 차이는 인증 상태인가에 따라 구분하는데, 보통은 로그인 전과 후를 뜻하는 것으로 생각하면 됩니다. 폼을 로그인 전에만 출력한다면 admin_post_nopriv_ 액션만 추가하면 되지만, 로그인 전과 후 모두 출력한다면 2가지 액션을 모두 추가해야 하며, 일반적으로 모두 추가합니다.

나중에 폼 구성이 끝나면 로그인 전과 후 상황을 만들어 둘 중에 하나를 지우고 폼에서 데이터를 입력한 후 결과를 확인하는 과정을 반복하면 바로 이해할 수 있습니다.

폼 데이터 처리 및 메일 전송

다음은 폼 데이터 검증 및 메일 발송, 오류 처리 방식에 관한 전체 코드입니다. 폼은 구성자의 생각에 따라 사용하는 함수, 처리 과정 등이 다양할 수 있으므로 기준이 없습니다. 자유 의지로 구성하면 됩니다.

/**
 * 폼 데이터 전송
 */
add_action( 'admin_post_nopriv_sendmail_formdata', 'func_sendmail_formdata' );
add_action( 'admin_post_sendmail_formdata', 'func_sendmail_formdata' );
function func_sendmail_formdata() {

    $user_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
    $delay = 5; // 재전송 가능 시간
    $transient = $user_ip . '_form_ticket'; // transient name
    if ( get_transient( $transient ) ) {
        wp_die(
            '<span style="margin-bottom:1.5em;font-size: 1.5em; display: block">한 번 보내면 ' . $delay . '분이 지나야 다시 보낼 수 있습니다.</span><a href="' . esc_url( home_url( '/' ) ) . '">홈 페이지로 이동</a>',
            '아직 시간이 남았습니다.',
            403
        );

    }

    if ( 
        isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] ) && 
        isset( $_POST['action'] ) && 'sendmail_formdata' == $_POST['action'] && 
        isset( $_POST['sendmail_formdata_field'] ) && wp_verify_nonce( sanitize_key( $_POST['sendmail_formdata_field'] ), 'sendmail_formdata' ) && 
        isset( $_POST['ftitle'] ) && $_POST['ftitle'] && 
        isset( $_POST['fname'] ) && $_POST['fname'] && 
        isset( $_POST['femail'] ) && $_POST['femail'] && 
        isset( $_POST['fcontent'] ) && $_POST['fcontent']
    ) {
        
        // 폼 데이터, stripslash, sanitize
        $formdata = str_replace( array("'", "\""), "", wp_unslash( $_POST ) ); // stripslashes_deep( $_POST );
        $subject = sanitize_text_field( $formdata['ftitle'] );
        $name = sanitize_text_field( $formdata['fname'] );
        $email = sanitize_email( $formdata['femail'] );
        $content = sanitize_textarea_field( $formdata['fcontent'] ); //sanitize_textarea_field 함수는 라인을 유지하지만, 폼에 다시 출력할 때 필드에서 적용. 따라서 wpautop를 사용해서 메일을 보내면 줄과 단락 구분의 메일을 받을 수 있음

        // header
        $to = sanitize_option( 'admin_email', get_option( 'admin_email' ) ); // 방문자가 보낸 내용을 받는 사람의 메일 (관리자 메일)
        $site_name = sanitize_option( 'blogname', get_option( 'blogname' ) ); // 사이트 이름
        $time = current_time ( 'mysql', 0 );
        
        $headers[] = "Content-Type: text/html; charset=UTF-8";
        $headers[] = "From:{$site_name}에서 {$name}님이 보냄 <{$to}>"; // 사용하지 않으면 WordPress wordpress@사이트주소
        $headers[] = "Reply-To: {$name} <{$email}>"; // 답장 주소

        // body
        $sender_info = array( $name, $email, $user_ip, $time, wptexturize('---') );
        $body = implode( "<br />", $sender_info );
        $body .= wpautop( $content ); //$body .= nl2br( $content );
        
        // send mail
        $success_req = wp_mail( $to, $subject, $body, $headers );

        if ( $success_req ) { // true, 메일 전송 요청의 오류가 없다는 뜻이며, 메일을 잘 받았다는 것을 뜻하지 않음.
            // 5분 짜리 transient
            if ( false === ( $form_ticket = get_transient( $transient ) ) ) {
                $form_ticket = wp_generate_password( 24, false, false );
                set_transient( $transient, $form_ticket, $delay * MINUTE_IN_SECONDS );

            }

            wp_die(
                '<span style="margin-bottom:1.5em;font-size: 1.5em; display: block">메일 잘 보냈습니다. 고맙습니다.</span><a href="' . esc_url( home_url( '/' ) ) . '">홈 페이지로 이동</a>',
                '메일 전송 오류 없음',
                201
            );

        } else {
            
            wp_die(
                '<span style="margin-bottom:1.5em;font-size: 1.5em; display: block">메일 전달에 문제가 있는 것 같습니다.</span>',
                '메일 전송 오류 생김',
                array(
                    'response' => 403,
                    'back_link' => true
                )
            );

        }
        
    } else {

        wp_die(
            '<span style="margin-bottom:1.5em;font-size: 1.5em; display: block">뭔가 이상합니다. 빠진 항목이 있는지 확인하고, 바른 정보를 입력하면 좋겠습니다.</span>',
            '어떤 정보가 빠졌거나 올바르지 않은 데이터가 있습니다.',
            array(
                'response' => 400,
                'back_link' => true
            )
        );

    }

}

이 포스트의 폼은 Html 폼에서 전체 폼 데이터를 필수 항목으로 설정하였는데, 정상적인 방법으로 폼을 사용하는 방문자라면 문제가 없지만, Html 폼은 브라우저에서 방문자가 임시 수정이 가능하므로 폼 데이터 처리 과정에서 따로 제어해야 합니다. 코드에 관한 상세 설명은 생략하며, 일부 요소만 확인해봅니다.

 요청 방식 및 폼 필드 확인

코드 21번 줄에서 27번 줄은 다음과 같은데, POST 요청(대문자), nonce, 폼 필드 및 데이터에 관한 조건입니다. 모두 참일 때만 이후 단계를 처리합니다.

isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] ) && 
isset( $_POST['action'] ) && 'sendmail_formdata' == $_POST['action'] && 
isset( $_POST['sendmail_formdata_field'] ) && wp_verify_nonce( sanitize_key( $_POST['sendmail_formdata_field'] ), 'sendmail_formdata' ) && 
isset( $_POST['ftitle'] ) && $_POST['ftitle'] && 
isset( $_POST['fname'] ) && $_POST['fname'] && 
isset( $_POST['femail'] ) && $_POST['femail'] && 
isset( $_POST['fcontent'] ) && $_POST['fcontent']

sanitize, remove backslash & quote(s), wp_unslash

30번 줄에서 35번 줄은 다음과 같은데, 다음 2번처럼 $_POST 변수의 배열 데이터에서 \를 제거하기 위해 wp_unslash 함수를 사용하였고,  single quote('), double quotes(") 기호도 빈 문자로 대체하여 제거하였습니다. 폼의 각 필드는 필드 유형에 맞는 워드프레스 sanitize 함수를 사용하여 폼 데이터 안정을 위한 최소한의 기본 방식을 사용하였습니다.

// 폼 데이터, stripslash, sanitize
$formdata = str_replace( array("'", "\""), "", wp_unslash( $_POST ) );
$subject = sanitize_text_field( $formdata['ftitle'] );
$name = sanitize_text_field( $formdata['fname'] );
$email = sanitize_email( $formdata['femail'] );
$content = sanitize_textarea_field( $formdata['fcontent'] );

특정 기호를 문자로 처리하기 위한 PHP Magic Quotes, 이 기능은 특정 PHP 버전 이후부터 자동으로 적용되지 않지만, 워드프레스는 하위 버전 호환을 위해 사이트의 환경에 상관 없이 강제로 \를 추가하여 부호화합니다. 이 포스트 작성일 기준일 까지도.

워드프레스 기본의 폼 데이터가 아니라 사용자가 따로 구성한 PHP 프로그램에서 single quote('), double quotes(") 같은 기호가 $_POST, $_GET, $_REQUEST, $_COOKIE 등의 전역 변수를 통해 데이터로 전달될 때 강제로 \를 추가합니다.

Magic Quotes, backslash, quote(s) 정보에 관한 상세 내용은 생략하고, 현재 상태에서 폼 제목에 quote 기호를 포함하여 '폼메일' 제목입니다.라고 입력한 후 메일을 확인하면 \ 기호가 없는 메일 제목을 볼 수 있습니다. (9번 줄 숫자 5를 1로 변경하여 시험하세요.)

코드에서 제목 부분만 다음처럼 변경한 후 제목을 직전과 똑같이 인용 기호를 포함하여 폼 데이터를 작성하여 수신 메일을 확인해보세요. 메일 제목에 \가 함께 출력될 것입니다. 확인 후 코드는 다시 원래로 변경합니다.

// '폼메일' 제목입니다.
$subject = sanitize_text_field( $_POST['ftitle'] );

수신 메일 줄 및 단락

수신 메일에서 내용의 줄 바꿈(br) 상태와 단락(p)을 유지하기 위해 추가한 다음 코드만 살펴봅니다.

$content = sanitize_textarea_field( $formdata['fcontent'] ); // 35번
$body = implode( "<br />", $sender_info ); // 48번
$body .= wpautop( $content ); // 49번

sanitize_textarea_field 함수는 Html을 허용하지 않습니다. 그에 따라 사용한 것이며, 다른 함수를 사용해도 됩니다. 만약, 사이트의 폼 textarea 영역에 작성한 내용을 다시 보여준다면 이 함수를 Html 폼에서 다시 사용하면 됩니다. 결국, 이 함수는 줄 구분과 단락을 위한 출력 필터의 뜻으로 생각하면 됩니다. 관련한 워드프레스 sanitize 함수도 비슷한 개념입니다.

sanitize_textarea_field 함수는 Html 태그를 허용하지 않고, 출력 시 줄과 단락을 구분하지만, 수신 메일에서는 적용되지 않으므로 wpautop 함수를 사용하여 메일을 보낼 때 줄과 단락을 다시 구분하도록 정의한 것입니다.

보내는 사람의 세부 정보도 수신 메일에서 줄을 나누기 위해 PHP implode 함수를 사용하였습니다.

폼 데이터 오류에 관한 친절은 오히려 불편

보통 폼 메일에서 폼 데이터 검증에 자바스크립트 또는 PHP 함수로 폼 필드 하나마다 오류 처리에 관하여 정의하여 친절함을 더하는 때가 많습니다. 앞에서 추가한 코드를 보면 wp_die 함수를 사용하여 폼 데이터 오류나 메일 전송에 관한 결과를 일괄 처리했습니다.

만드는 사람 마음이며, 흔한 메일 전송 폼에서 특정 필드 누락 시 친절하게 일일이 알려주는 방식은 중요한 데이터를 전달할 때 외에는 방문자가 불편할 때가 많습니다. Html 폼에서 데이터 형식에 맞는 필드 유형 지정 정도만으로도 올바른 데이터를 입력하는 방문자에게는 충분합니다.

폼 데이터 전송 주기 제어

다음 그림은 메일 전송 성공 후 페이지입니다. 메일 전송 요청의 오류가 없다는 뜻이며, 지정한 메일 주소로 메일이 전달된 것을 증명하는 것은 아닙니다.

메일 전송 성공 메시지

위의 그림 페이지에서 브라우저의 이전 버튼을 클릭하면, 보낸 메일 내용이 그대로 나오는 폼을 볼 수 있습니다. 이때 8번 줄에서 18번 줄의 코드가 없다면, 보내기 버튼을 클릭하면 메일을 다시 보낼 수 있습니다. 재미로 반복할 가능성도 있습니다.

8번 줄에서 18번 줄의 코드는 다음과 같은데, 워드프레스 Transient API를 사용하여 한 번 보내면 해당 아이피는 5분이 지나면 다시 보낼 수 있도록 정의한 것입니다. 다음 코드는 아이피_form_ticket 이름의 옵션 데이터(transient name)가 있다면 wp_die 함수를 사용하여 특정 메시지를 출력하도록 정의한 것입니다.

$user_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
$delay = 5; // 재전송 가능 시간
$transient = $user_ip . '_form_ticket'; // transient name
if ( get_transient( $transient ) ) {
    wp_die(
        '<span style="margin-bottom:1.5em;font-size: 1.5em; display: block">한 번 보내면 ' . $delay . '분이 지나야 다시 보낼 수 있습니다.</span><a href="' . esc_url( home_url( '/' ) ) . '">홈 페이지로 이동</a>',
        '아직 시간이 남았습니다.',
        403
    );

}

숫자 5를 원하는 것으로 변경할 수 있습니다. 위의 코드에서 숫자 5는 5분이 아니라 그냥 5입니다. 5분은 코드 56번 줄에서 61번 줄 코드인 다음 코드의 MINUTE_IN_SECONDS 상수(Using Time Constants)에 의해 단위가 결정됩니다.

// 5분 짜리 transient
if ( false === ( $form_ticket = get_transient( $transient ) ) ) {
    $form_ticket = wp_generate_password( 24, false, false );
    set_transient( $transient, $form_ticket, $delay * MINUTE_IN_SECONDS );

}

위의 코드는 아이피_form_ticket($transient 변수) 이름의 transient 옵션 데이터가 없다면 해당 이름의 transient 데이터를 생성하는 것으로 5분($delay 변수)간 존재하도록 시간을 정한 것이며, $form_ticket 변수의 값(wp_generate_password 함수)은 이 포스트의 폼에서 무의미한 데이터로 다른 데이터를 추가해도 됩니다. 포스트의 폼에서는 자동으로 특정 시간이 지나면 만료되는 데이터가 필요한 것입니다.

메일 전송 주기 안내

워드프레스 Transient API 정보는 코덱스 또는 워드프레스 Transient API 포스트를 참고하세요.

이 포스트의 전체 코드를 다음 링크의 파일에서 간단한 플러그인 형식으로 구성하였습니다. 혹시 이 포스트를 따라 했다면 추가한 코드를 모두 지우고, 파일을 받아 업로드 방식으로 플러그인을 추가 후 page 하나를 만들고 Shortcode를 사용하여 메일 전송 폼을 시험해보세요.

정리

메일 전송 폼은 관련 플러그인이 많아 쉽게 얻을 수 있습니다. 그러나, 보통 필드 정도의 메일 전송 폼이라면 직접 만들어 사용하는 것이 때로 좋습니다. 예를 들면, 폼으로 얻은 데이터를 활용할 때 결국, 워드프레스 함수를 호출하는 플러그인 제공 함수를 따로 학습하는 것보다 직접 구성하여 응용하는 것이 유리합니다.

폼을 구성하여 방문자의 데이터를 얻을 때 가장 염려하는 것은 보안입니다. 워드프레스 기본의 시스템이 아닌 사용자가 따로 만들어 워드프레스에 사용할 때 더욱 주의해야 합니다. 완벽한 보안은 존재할 수 없고, 워드프레스가 최소한의 폼 데이터 검증 등의 함수를 제공하므로 지나치게 염려하지 않아도 됩니다.

참고

공유 웹호스팅에서 워드프레스를 사용한다면 메일 전송에 문제가 있을 수 있으며, 그렇지 않은 때도 있습니다. 그렇지 않은 때 메일 전송 폼을 통해 받는 수신 메일에 경고와 같은 메시지가 나올 수 있습니다. 보통 Gmail은 늘 나옵니다.

공유 웹호스팅, 서버 환경 등 구분하지 않고, 워드프레스 사용할 때는 phpmailer_init 훅을 사용하여 직접 보내는 메일을 설정하는 것이 유리한 때가 많습니다. 워드프레스 메일, phpmailer_init, Works Mobile, sSMTP 포스트를 참고해보세요. 이때는 메일 전송 폼을 통해 받은 메일에 경고 메시지는 나오지 않습니다. 물론, 모든 환경에 적용된다고 장담할 수 없습니다.