WP AJAX 사용할 때 작은 팁들

요즘은 웹 페이지들이 엄청 인터랙티브하다. 그만큼 비동기 호출, AJAX의 사용이 많을 수 밖에 없는 환경이다. 한편 워드프레스에서는 admin-ajax.php라는 곳에서 거의 모든 AJAX 요청을 처리하게 된다. 이 포스트에서는 워드프레스 플러그인에서 AJAX 요청을 작성할 때 참고하면 좋을 팁을 몇 가지 적어 보도록 한다.

비로그인 사용자를 위한 wp_ajax_nopriv_* 훅이 있다

가장 기초적인 사항이다. AJAX 요청을 서버에서 인식하려면 반드시 add_action()을 통해 미리 등록해야 한다. 이 때 훅 이름은 ‘wp_ajax_’로 시작한다. 여기서 주의하자. 이 훅은 ‘로그인 된 사용자’만을 위한 것이다. 비로그인 사용자도 AJAX를 사용하게 하고 싶다면? ‘wp_ajax_nopriv_’가 있다. 예를 들어 액션 ‘hello’로 AJAX 작업을 등록한다면,

add_action( 'wp_ajax_hello', 'blah_do_ajax_hello' ); 
add_action( 'wp_ajax_nopriv_hello', 'blah_do_ajax_hello' );

function blah_do_ajax_hello() {
    ....
}

이렇게 하고, 자바스크립트에서는 다음처럼 쓰면 될 것이다.

(function($) {
    ....
    $.ajax( ajaxUrl, { 
        ....
        data: {
            action: 'hello',
            ....
        }
    });
})(jQuery);

노파심에 한 마디 더 한다. WP AJAX는 반드시 ‘action’이라는 파라미터를 가지고 있어야 한다. 이 파라미터의 값이 원하는 콜백 함수와 대응되기 때문이다. 위 코드 조각에서는 action 파라미터의 값이 ‘hello’이므로 add_action()을 통해 입력된 ‘wp_ajax_hello’, ‘wp_ajax_nopriv_hello’ 훅의 콜백을 실행할 것이다.

 

GET/POST를 구분하라

이것도 기초 중의 기초다. GET 방식은 URL 뒤에 query string으로 원하는 데이터를 호출한다. POST는 HTTP 메시지 내부에 원하는 데이터를 담아 보낸다. 간단히 말해 GET은 서버의 상태에 변화를 주지 않는 작업, 즉 조회하는 일에 한정하여 사용한다. POST, 혹은 PUT 등등은 서버의 상태에 변화를 줄 때 사용한다. 포스트를 등록 수정하거나, 사용자의 폼을 받아 어떤 작업을 해서 서버에 명시적인 기록을 남기는 것들이다.

물론 GET 방식으로 호출한 데이터를 가지고 DB에 삽입과 수정, 혹은 삭제 작업을 못 하는 건 아니다. 그러나 절대 권장하지 않는다. 만약 회원의 비밀번호를 업데이트 하는데 GET 방식을 쓴다면? 이런 미친, URL 주소에 회원의 비밀번호가 막 노출될 것이다. 그 상황에 누가 로그라도 수집하면?

 

nonce는 꼭 써라

nonce가 뭐냐고? number used once를 nonce라고 한다. 워드프레스의 nonce는 진짜 딱 한 번만 쓰이는 nonce는 아니며 일정 시간마다 변하는 난수이다. 그래도 CSRF 공격 같은 보안을 위한 방책이니 어지간하면 꼭 쓰자. 기본적으로 어지간한 관리자 화면 등지에서 만들어진 폼 정보에는 워드프레스나 기타 플러그인이 기본적으로 넣어둔 nonce 정보가 있다. 그래서 페이지에서 폼 제출(submit)이 일어나면 그나마 좀 낫지만. 그러나 AJAX는 그렇지 않다. 그들이 도와 주지 않는다.

nonce를 삽입할 때는 간단하게 두 가지 방법이 있다.

  1. form 내부에 hidden value로 저장
  2. 스크립트 로컬라이즈 시 별도로 nonce 값을 JSON으로 전달

첫번째의 예는 이렇다.

<form>
    ....
    <?php wp_nonce_field( 'my-nonce', 'nonce' ); ?>
    ...
</form>

두 번째 방법 사용의 예이다.

wp_localize_script(
    'my-script-handle',
    'myScript',
    array(
        'ajaxUrl'          => admin_url( 'admin-ajax.php'),
        'nonce'            => wp_create_nonce('my-nonce'),
        '_wp_http_referer' => esc_attr( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
        ....
    )
);

검증 시에는 이런 방식으로 처리한다.

function my_ajax_handler() {
    if( !wp_doing_ajax() ) {
        wp_die( -1, 403 );
    } 
   
    if( !wp_verify_nonce( $_REQUEST['nonce'], 'my-nonce' ) {
        die('nonce error');
    }
}

두 방법 다 ‘_wp_http_referer’ 필드를 가지고 있으므로 레퍼러를 검증하는 것도 (귀찮기는 하지만) 좋은 대응이 될 수 있다.

2018년 01월 내용 덧붙임: nonce를 언제나 꼭 써야 할까? 그렇지는 않다. 서버에 정보를 보내 서버에 변화를 주는 데이터면 nonce를 쓰는 것이 맞다. 그러나 단순히 서버 내용을 AJAX로 조회하는 것 뿐이라면 굳이 nonce를 쓸 필요는 없다. 왜냐면 사이트 캐싱 때문이다. 속도를 위해 대부분의 사이트는 wp super cache 같은 캐시 플러그인을 사용하는데, nonce와 캐시 플러그인은 동시 공존하기 어려운 존재이다.

자바스크립트는 어지간하면 분리하여 작성하자

어떤 콜백들은 HTML 문서를 생성해야 할 때가 있다. 좋은 예가 admin_menu() 함수의 콜백들이다. 이 함수에서 HTML 문서를 작성하는데, 귀찮다는 이유로 자바스크립트까지 다 섞어버리지는 말자. 앞을 보고 프로그래밍하자. 만일 스크립트에서 버그가 나면, 이런 스크립트는 디버깅하기 어려워진다.

워드프레스의 최종 HTML 문서는 이미 코어에서 틀을 잡고, 플러그인은 그 일부를 조정하는 형태로 작성된다. 이런 구조에서는 당신이 짠 스크립트가 도대체 어디에서 나오는지 감을 잡기조차 어려울 때가 잦다. 이렇게 뒤섞여버린 스크립트에선 에러가 나면 찾기도 힘들 뿐더러, 웹브라우저 콘솔에서 그나마 알려주는 에러가 발생하는 라인 힌트도 당신이 작성한 스크립트의 줄과도 맞지 않는다. 어지간히 간단한 스크립트가 아닌 이상은 별도의 파일로 분리하자 그리고  wp_register_script(), wp_localize_script(), wp_enqueue_script() 세 함수의 단계를 밟아 스크립트를 삽입하도록 하자.

이것도 간단하게 예를 들어 보자.

wp_register_script

1단계는 wp_register_script()로 스크립트의 존재를 코어에게 알리는 일이다. 그냥 ‘알리는’ 것이므로 이 함수를 쓴다고 해서 바로 HTML 코드에 <script> … </script>가 생성되지는 않는다. 이 함수는 admin_enqueue_scripts(), wp_enqueue_scripts() 훅과 같이 사용하면 된다.

wp_register_script(
  'my-handle',
  plugin_dir_url( __FILE__ ) . 'path/to/script.js',
  array(
    'jquery'
  )
  '1.0.0',
  TRUE
);

여기서 버전(4번째 인자)을 넣기도 하는데, 플러그인의 버전을 살뜰히 챙겨 넣어 주는 것이 좋다. 나중에 CDN이나 캐시 서버를 활용할 때 버전 문자열이 도움이 된다.

wp_localize_script

2단계는 wp_localize_script()이다. 원래는 이 함수를 이용해 스크립트에 출력될 문자열의 번역을 자바스크립트로도 보내는 것이지만, 꼭 문자열의 번역일 필요는 없다. 스크립트에서 유용히 사용될 정보는 어떤 것이든 삽입할 수 있다. 여기서 집어 넣을 정보들을 집어 넣자. 다음 항목들은 어지간하면 꼭 명시적으로 삽입하도록 하자. 어설프게 <?php echo … ?> 따위로 하드코딩 처리하지 말고.

  • ajax url: 많은 플러그인들이 이 정보를 문서 곳곳에 집어 넣었을 것이다. 그러나 절대 다른 플러그인이 출력했다고 해서 내 플러그인은 출력을 생략하는 일이 없도록 하자. 우리가 작성한 정보만이 유일하게 믿을 수 있는 정보이다.
  • nonce: nonce 값 또한 여기서 밀어 넣어 주는 것이 좋다.
  • action: 당연히도.

자바스크립트와 PHP, 두 이질적인 플랫폼이 맞물리는 부분이다. 여기서 섣불리 하드코딩된 정보를 남발하면 나중이 힘들다.

wp_localize_script(
  'my-handle',
  'myObj',
  array(
    'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    ....
  )
);

두번째 인자 myObj는 자바스크립트의 전역 변수 이름이 되고, 세번째 인자는 그대로 JSON encode 되어 변수에 대입된다. 그래서 자바스크립트 코드에서 ‘myObj.ajaxUrl’ 처럼 참조가 가능하다.

wp_enqueue_script

3단계는 wp_enqueue_script() 이다. 로컬라이즈 할 부분이 없다면 wp_register_script() 없이 바로 쓸 수는 있지만, 위에서 말한 이유 때문에 AJAX를 위해서는 추천하지 않는다. 1~2단계를 거쳤다면 3단계는 그냥 핸들 이름만 주면 된다.

wp_enqueue_script( 'my-handle' );

wp_enqueue_script() 함수를 통해 자바스크립트를 <head> 태그 사이에 넣으려면 wp_enqueue_scripts, admin_enqueue_scripts 훅의 콜백에서 바로 사용하는 것을 추천한다. 다른 훅에서는 이미 head 태그의 출력이 끝나고 body 부분을 열심히 작성하고 있을 수 있기 때문이다. 만일 head 출력이 끝난 경우라면 워드프레스는 footer 부분에 스크립트를 위치시킨다. 자바스크립트를 헤더, 푸터 위치에 덜 민감하게 짜는 것도 요령이다.

wp_add_inline_script

여기서 버전 4.5이상의 워드프레스라면 나머지 한 단계를 더욱 사용할 수 있다. wp_add_inline_script()가 그것이다. 이 함수를 쓰면 위에서 삽입한 스크립트 바로 앞이나 뒤에 추가로 인라인 코드를 삽입 가능하다. 완성도 높은 스크립트는 수정 없이 재사용할 수 있다. 그리고 그런 코드들은 단지 초기 입력값만 별도로 잘 정의하면 된다. 이 방법을 통해 그런 초기값들만 잘 정리해 주면 더욱 유지 보수하기 좋은 코드가 된다.

wp_add_inline_script(
  'my-handle',
  array(
   ....
  )
);

 

자바스크립트는 PHP처럼 타입 저글링이 일어나지 않음에 유의하라

AJAX 요청을 하면 그 요청이 성공인지 실패인지를 알려주는 코드를 수신하게 된다. 이 코드는 보통 HTTP Response Code, 그러니까 200, 404, 403 등으로 대체하기도 하지만 보다 상세한 상태를 서술하기 위해서 별도의 코드를 사용하기도 한다. 그리고 AJAX 수신을 성공적으로 한 후 이 코드에 따라 동작이 분기되게 되는데, 여기서 주의해서 처리해야 할 부분이 하나 있다. 바로 자바스크립트는 PHP처럼 그렇게 유연하게(혹은 마구잡이로) 타입이 변하지는 않는다는 점이다.

자, 자바스크립트에서 다음처럼 success 콜백을 디자인했다고 하자.

$.ajax({
  data: {....},
  success: function(response) {
    if(response.success) {
      switch(response.data.code) {
        case '4300':
           ....
           break;
        case '4400':
           ....
           break;
       case '4500':
           ....
           break;
       ....
      }
    }
  }
});

별 문제가 없는 것 같지만, 나로서는 뭔가 조금 아쉽다. 왜냐면 status code의 4300, 4400, … 등이 모두 정수형임에도 불구하고 switch~case 문에서는 문자열로 표기되었기 때문이다. PHP 코드를 계속 짜다가 보면 무의식적으로 헷갈릴 수 있다. 자바스크립트는 PHP처럼 타입 저글링이 일어나지는 않는다. 만일 response.data.code 가 정수형으로 날아왔다면 아래 코드는 무용지물이 된다. 그리고 이 AJAX를 처리하는 백엔드 쪽이 PHP임을 주의하고 주의하자. PHP에서 json_encode() 함수를 사용할 때 주어진 배열 내의 값의 타입에 따라 encode 된 값이 달라질 수 있다. 가령

json_encode( array( 'a' => '1' ) );


json_encode( array( 'a' => 1 ) );

첫번째 인코드 결과는 {“a”:”1″}이고, 두번째 결과는 {“a”:1}이다. 자바스크립트의 ‘1’과 1은 다르다.

 

wp_send_json_*() 함수를 써서 응답하라

플러그인 코드의 강건함과 직결되는 무지막지하게 중요한 부분이다. 나도 WP AJAX 처음 쓸 때 콜백에서 자주 이렇게 응답을 보내곤 했다.

function foo() {
 ...
 echo 'your-data';
 die();

 // 또는
 die('your-response');
}

절대, 이렇게 쓰지 마라. 그리고 인터넷에서 자주 nonce fail 같은 에러도 이렇게 작성하곤 하는데,

wp_die( 'nonce verification error' );
// 또는
die('nonce verification error');

비추천하는 에러 고지 방식임을 이야기하고 싶다.

이유는 이렇다. 우선 저렇게 단촐하게 응답을 보내는 것은 ‘성공’인지 ‘실패’인지 명확하지 않다. 사람이야 “nonce verification error”라는 메시지를 보고 에러라고 인지할 수는 있으나, 이 코드를 다루는 쪽은 인간이 아닌 기계이다. 상태는 명확해야 한다. REST API에서 결과는 단순하다. 실패, 혹은 성공. 그 두가지가 딱 떨어져야 한다. 그런데 저런 메시지는 심히 문맥에 의존하므로 깔끔하게 코드로 표현하기 어렵다.

그리고 더더욱 중요한 한 가지 이유. 저 메시지는 어떠한 메시지 컨테이너에도 담겨 있지 않다! 즉 그 자체로 본문(payload)이기 때문에 노이즈에 매우 취약하다는 단점이 있다. 이게 무슨 전파 통신도 아닌데 노이즈는 왠 노이즈인가? 하고 반문할 수 있겠다. 그러나 그게 그렇지가 않다. admin-ajax.php 동작을 조금 살펴보면 이 문제는 중요하다.

<?php 태그, 함부로 열고 닫는 거 아니다.

PHP는 인터넷에서 그다지 좋은 평가를 받는 언어는 아니다. 태생적인 한계가 참 많은 언어인데, 지금 말하고자 하는 것도 그런 태생적인 한계인 부분인 것 같다. 보통 php 스크립트는 <?php 태그를 열음으로써 시작한다. 그리고 스크립트를 끝낼 때는 HTML이나 XML처럼 태그를 닫기도 하는데, 아니다. 그렇지 않다. 만일 PHP 스크립트로 파일이 끝나게 된다면, 닫지 마라. 농담하는 것 아니다. 진짜 닫지 마라. XML 처럼 수미쌍관이 아니어도 된다. <?php 태그로만 시작해서 그냥 끝나도 아무런 문제가 없다.

보통 php 함수에서 html 태그를 출력하려면 이런 식으로 쓰기도 한다.

function foo() { ?>
 <div> blah blah .... </div>
<?php }

문법상 문제는 없는 코드이긴 하다. 그런데 여기서 중요한 점을 지적하고 싶다. “php의 종료 태그가 선언되고 난 다음, php 시작 태그가 다시 선언되기 전까지의 영역은 어떻게 되는가?”이다. 정답은 “그냥 출력된다”이다.

admin-ajax.php로 AJAX를 보내면 워드프레스 코어는 그 어떤 출력도 보내지 않다가 가장 마지막 순간에 do_action( ‘wp_ajax_’ . $_REQUEST[‘action’] ); 를 실행하고, die(0)으로 끝난다. 보통 사용자의 콜백 안에서 코어의 die(0)가 나오기 전에 미리 죽어버리니 제대로 된 AJAX 콜백에서는 저 ‘0’을 볼 일이 없는 것이다. 이 때까지 원칙적으로 코어는 그 어떤 출력도 내보내지 않게 설계되어 있다.

그런데 문제는 php 태그 열고 닫음의 원칙을 모르는 어설픈 플러그인 개발자가 소스를 짤 때 일어난다. admin-ajax.php도 정상적으로 동작하기 위해선 플러그인 파일들을 인클루드해야 한다. 이 과정에서 소스를 읽어들이는데… 아까 ?> <?php 사이의 내용은 어떻게 된다고 하였나? 그냥 출력된다고 말했다. 그렇다. 어떤 내용도 출력되어서는 안되는 AJAX 루틴에서도 저 공백이 출력되는 일이 일어나게 되는 것이다.

이런 일은 보통 소스 중간에서 의미 없이 ?>로 닫고, <?php를 열었을 때, 그리고 파일 끝에 ?>를 잘못 삽입할 때 일어난다.

##### file parent.php ######
<?php

include( 'child-a.php' );
include( 'chlid-b.php');

?> // end of parent.php
############################


##### file child-a.php #####
<?php
....
?>
   // 여기 공백 있음
// end of child-a.php
############################


##### file child-b.php #####
<?php
....
?> // end of child-a.php
############################

위 예는 3개의 소스 파일이 있다고 가정했을 때를 그린 것이다. parent.php는 child-a, child-b를 include한다. 이 때 a의 마지막에서 공백이 포함되었다. parent.php가 잘 감싼다면 문제가 없겠지만, 간혹 이런 게 문제가 될 가능성이 있다. 그리고 아래처럼 한 파일 안에서 아무 의미 없이 태그를 열고 닫으면 확실하게 문제가 된다.

<?php
....
?> // 여기에 공백 있음
<?php
....
?>

이게 보통처럼 HTML문서를 출력하는 상황이면 큰 문제가 없다. HTML문서는 원체 공백 투성이니까. 그러나 die( ‘…’ ) 바로 출력한 심플한 AJAX 응답은? 내가 말한 노이즈의 정체가 바로 이것이다.

워드프레스는 여러 플러그인들이 설치되어 돌아가게 마련이다. 언제 누가 저런 실수를 저질렀을지 모른다. 이때 우리의 AJAX 호출이 JSON 형태, wp_send_json_success(), wp_send_json_error()를 사용한 응답이라면 저런 실수에도 흔들림이 없을 것이다. JSON 문자열은 파싱할 때도 공백 문자는 허용하기 때문이다. 그러나 아까처럼 심플한 문자열 출력은 반드시 어디선가 에러를 내게 될 것이다.

그러면 이렇게 하면 되지 않겠느냐고 반문할 수도 있겠다.

die( json_encode( $output ) );

물론 그렇다. 나쁘지 않다. 그러나 이 코드는 응답 헤더가 적절하지 않다. JSON으로 응답을 보냈다면 Content-Type: application/json이 설정되어 있어야 한다. 그러므로,

header('Content-Type: application/json');
status_header( 200 );

...

die(json_encode( $output ) );

이 적절하다. 이렇게 하면 똑똑한 jQuery는 콜백 인자로 들어오는 data를 알아서 JSON 객체로 입력해 준다. 그러니 아래처럼 매번 이런 자바스크립트에서 삽질을 할 필요가 없다.

....
, success: function(data) {
   var obj = JSON.parse(data);
   ....
}

wp_send_json()의 응답은 success, data 두 속성을 가지고, data에는 내가 전달한 결과가, success는 이 API호출이 최종적으로 성공/실패인지를 명시한다. 편하고도 디테일하다. 안 쓸 이유가 없다. 프로그램의 안정성을 위해서 애용하도록 하자.

2017년 10월 28일 추가 : wp_send_json_error() 함수의 인자로 WP_Error 객체를 넣을 수 있다. 에러를 전송할 때는 WP_Error가 더욱 훌륭한 대응이다. 단순히 에러를 던지고 끝나버리면, 이 에러를 처리하는 자바스크립트에서 각 에러의 종류마다 정밀하게 대응하기 어렵게 된다. WP_Error는 에러 메시지 뿐만 아니라 에러 코드를 넘기도록 되어 있다. 에러 메시지는 사람에게 읽기 편하고 에러 코드는 코드에서 예외 처리를 할 때 편하다. AJAX는 항상 성공을 넘기는 것이 아니다. 에러일 때도 왜 에러가 났는지 유저에게 친절하게 알려주려면, 코드에서 그 에러를 인지하기 편리해야 한다.

마치며

AJAX를 많이 사용하면서 느낀 점을 정리하는 느낌으로 적어 보았다. 내가 전하는 방법이 100% 옳고 완벽한 방법은 아니다. 환경은 변하고 내가 작성하는 코드들도 진화한다. 그냥 좋은 팁으로 기록되기도 하고, 이 두서없이 쓰는 포스팅이 도움이 될리 있겠냐만…. 그렇지만 누군가에게는 작은 도움이라도 되었으면 좋겠다.

 

 

댓글 4개

  1. 안녕하세요?
    워드프레스 위젯(html코드로 삽입)로 db테이블의 내용을 출력하는 작업을 하고있는데요.
    여러위젯에 여러테이블을 넣으려합니다.
    그런데 html코드로 만든 각각의 위젯에서 동시에 ajax 를 쓰고있어서
    한쪽테이블이뜨면한쪽테이블이안뜨는것같은데
    이런경우에는 어떻게해결해야하나요?
    답변 해주시면 정말 감사하겠습니다!

    1. 구현의 문제 같습니다. 한 전 점검해 보세요.
      Q. 위젯이 프론트에서 반드시 AJAX 콜을 해야 하는가?
      A. 구현 자체는 서버 사이드에서 처리하는 것이 더 심플할 수 있습니다. AJAX 콜을 왜 해야 하는지 고려해 보세요.

      Q. 여러 위젯이 동적으로 다른 데이터를 조회하는가?
      A. 이 경우 위젯마다 설정된 값을 통해 AJAX 콜을 해야 합니다. 그러나 그렇지 않다면 여러 위젯이 동시에 AJAX 콜을 할 이유가 없습니다.

      그 외에 AJAX 콜을 여럿 한다고 해서 응답이 누락되는 경우는 있기 어렵습니다. 해결 방법은 간단히 설명드리자면, 각 AJAX 호출마다 요청 파라미터와 응답 파라미터를 철저히 검증하십시오. 그냥 ‘안 나온다가’ 아니라 어떻게 호출을 했고 응답으로 무엇을 받았는가 구체적으로 따져 들어가시다 보면 뭔가 잘못된 곳을 마주칠 수 있을 것입니다.

댓글 남기기