반응형
WordPress REST API 완전 가이드
커스텀 엔드포인트 생성부터 실전 활용까지
기초부터 고급까지, 실제 코드 예제와 함께 배우는 완전 가이드
WordPress REST API란?
WordPress REST API는 WordPress와 외부 애플리케이션 간의 데이터 교환을 위한 강력한 인터페이스입니다. 이 가이드에서는 커스텀 엔드포인트를 생성하여 원하는 기능을 구현하는 방법을 자세히 알아보겠습니다.

목차
1 기본 개념과 시작하기
WordPress REST API 기본 구조
WordPress REST API는 다음과 같은 구조를 가집니다:
- 네임스페이스: API의 버전과 벤더를 식별 (예: myplugin/v1)
- 라우트: 실제 URL 경로 (예: /author/123)
- 엔드포인트: HTTP 메소드와 콜백 함수의 조합
핵심 개념
모든 REST API 엔드포인트는 rest_api_init
액션 훅에서 등록해야 합니다. 이는 API가 필요할 때만 엔드포인트를 로드하기 위함입니다.
2 간단한 엔드포인트 생성
첫 번째 커스텀 엔드포인트
가장 기본적인 GET 엔드포인트를 만들어보겠습니다:
functions.php
<?php
// 작가의 최신 포스트 제목을 가져오는 함수
function get_author_latest_post( $data ) {
$posts = get_posts( array(
'author' => $data['id'],
'numberposts' => 1,
) );
if ( empty( $posts ) ) {
return new WP_Error( 'no_posts', '해당 작가의 포스트를 찾을 수 없습니다.', array( 'status' => 404 ) );
}
return $posts[0]->post_title;
}
// REST API 엔드포인트 등록
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'get_author_latest_post',
'permission_callback' => '__return_true', // 공개 엔드포인트
) );
} );
접근 URL
https://yoursite.com/wp-json/myplugin/v1/author/123
register_rest_route 함수 상세 분석
매개변수 | 설명 | 예제 |
---|---|---|
namespace | API의 네임스페이스 (vendor/version) | myplugin/v1 |
route | URL 패턴 (정규식 지원) | /author/(?P<id>\d+) |
methods | 허용된 HTTP 메소드 | GET, POST, PUT, DELETE |
callback | 요청을 처리할 함수 | get_author_latest_post |
permission_callback | 권한 확인 함수 | __return_true |
3 고급 기능과 매개변수 처리
매개변수 검증과 사니타이제이션
안전하고 신뢰할 수 있는 API를 만들기 위해서는 입력값 검증이 필수입니다:
고급 엔드포인트 예제
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/posts/search', array(
'methods' => 'GET',
'callback' => 'search_posts_api',
'permission_callback' => '__return_true',
'args' => array(
'keyword' => array(
'required' => true,
'validate_callback' => function($param, $request, $key) {
return is_string($param) && strlen($param) >= 3;
},
'sanitize_callback' => 'sanitize_text_field',
'description' => '검색 키워드 (최소 3글자)',
),
'per_page' => array(
'default' => 10,
'validate_callback' => function($param, $request, $key) {
return is_numeric($param) && $param > 0 && $param <= 100;
},
'sanitize_callback' => 'absint',
'description' => '페이지당 게시물 수 (1-100)',
),
'page' => array(
'default' => 1,
'validate_callback' => function($param, $request, $key) {
return is_numeric($param) && $param > 0;
},
'sanitize_callback' => 'absint',
'description' => '페이지 번호',
),
),
) );
} );
function search_posts_api( WP_REST_Request $request ) {
// 검증된 매개변수 가져오기
$keyword = $request->get_param( 'keyword' );
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
// WP_Query로 검색 실행
$query = new WP_Query( array(
's' => $keyword,
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
) );
if ( !$query->have_posts() ) {
return new WP_Error( 'no_results', '검색 결과가 없습니다.', array( 'status' => 404 ) );
}
$posts = array();
while ( $query->have_posts() ) {
$query->the_post();
$posts[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'date' => get_the_date( 'c' ),
'author' => get_the_author(),
);
}
wp_reset_postdata();
// WP_REST_Response 객체로 응답
$response = new WP_REST_Response( $posts );
$response->set_status( 200 );
$response->header( 'X-Total-Count', $query->found_posts );
return $response;
}
다양한 HTTP 메소드 처리
CRUD 엔드포인트 예제
<?php
add_action( 'rest_api_init', function () {
// 단일 리소스 엔드포인트
register_rest_route( 'myplugin/v1', '/comments/(?P<id>\d+)', array(
// GET - 단일 댓글 조회
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'get_single_comment_api',
'permission_callback' => '__return_true',
),
// PUT - 댓글 업데이트
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => 'update_comment_api',
'permission_callback' => 'check_comment_edit_permission',
'args' => array(
'content' => array(
'required' => true,
'sanitize_callback' => 'wp_filter_nohtml_kses',
'validate_callback' => function($param) {
return !empty(trim($param));
}
),
),
),
// DELETE - 댓글 삭제
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => 'delete_comment_api',
'permission_callback' => 'check_comment_delete_permission',
),
) );
// 컬렉션 엔드포인트
register_rest_route( 'myplugin/v1', '/comments', array(
// GET - 댓글 목록
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'get_comments_list_api',
'permission_callback' => '__return_true',
),
// POST - 새 댓글 생성
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'create_comment_api',
'permission_callback' => 'check_comment_create_permission',
'args' => array(
'post_id' => array(
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param) && get_post($param);
},
'sanitize_callback' => 'absint',
),
'content' => array(
'required' => true,
'sanitize_callback' => 'wp_filter_nohtml_kses',
),
'author_name' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'author_email' => array(
'required' => true,
'validate_callback' => 'is_email',
'sanitize_callback' => 'sanitize_email',
),
),
),
) );
} );
4 Controller 패턴 활용
왜 Controller 패턴을 사용해야 할까요?
- 코드의 구조화와 재사용성 향상
- WordPress 코어 패턴과의 일관성
- 유지보수와 확장의 용이함
- 표준화된 메소드명과 구조

완전한 Controller 클래스 예제
<?php
class My_Custom_Posts_Controller extends WP_REST_Controller {
protected $namespace;
protected $rest_base;
public function __construct() {
$this->namespace = 'myplugin/v1';
$this->rest_base = 'custom-posts';
}
/**
* 라우트 등록
*/
public function register_routes() {
// 컬렉션 라우트
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( true ),
),
) );
// 단일 아이템 라우트
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( false ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
) );
}
/**
* 아이템 목록 조회
*/
public function get_items( $request ) {
$args = array(
'post_type' => 'custom_post_type',
'posts_per_page' => $request['per_page'],
'paged' => $request['page'],
'post_status' => 'publish',
);
if ( !empty( $request['search'] ) ) {
$args['s'] = $request['search'];
}
$query = new WP_Query( $args );
$posts = array();
foreach ( $query->posts as $post ) {
$data = $this->prepare_item_for_response( $post, $request );
$posts[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $posts );
$response->header( 'X-WP-Total', (int) $query->found_posts );
$response->header( 'X-WP-TotalPages', (int) $query->max_num_pages );
return $response;
}
/**
* 단일 아이템 조회
*/
public function get_item( $request ) {
$post = get_post( $request['id'] );
if ( !$post || $post->post_type !== 'custom_post_type' ) {
return new WP_Error( 'rest_post_invalid_id', '잘못된 포스트 ID입니다.', array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $data );
}
/**
* 새 아이템 생성
*/
public function create_item( $request ) {
$prepared_post = $this->prepare_item_for_database( $request );
$post_id = wp_insert_post( $prepared_post, true );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
// 메타 데이터 저장
if ( !empty( $request['meta'] ) ) {
foreach ( $request['meta'] as $key => $value ) {
update_post_meta( $post_id, $key, $value );
}
}
$post = get_post( $post_id );
$response = $this->prepare_item_for_response( $post, $request );
$response->set_status( 201 );
return $response;
}
/**
* 응답용 데이터 준비
*/
public function prepare_item_for_response( $post, $request ) {
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'status' => $post->post_status,
'date' => $post->post_date,
'author' => (int) $post->post_author,
'featured_image' => get_post_thumbnail_id( $post->ID ),
'meta' => get_post_meta( $post->ID ),
);
$context = !empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $post ) );
return $response;
}
/**
* 데이터베이스용 데이터 준비
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = array(
'post_type' => 'custom_post_type',
'post_title' => $request['title'],
'post_content' => $request['content'],
'post_excerpt' => $request['excerpt'],
'post_status' => $request['status'] ?: 'draft',
);
if ( !empty( $request['author'] ) ) {
$prepared_post['post_author'] = $request['author'];
}
return $prepared_post;
}
/**
* 권한 확인 메소드들
*/
public function get_items_permissions_check( $request ) {
return current_user_can( 'read' );
}
public function create_item_permissions_check( $request ) {
return current_user_can( 'publish_posts' );
}
public function update_item_permissions_check( $request ) {
$post = get_post( $request['id'] );
return current_user_can( 'edit_post', $post->ID );
}
public function delete_item_permissions_check( $request ) {
$post = get_post( $request['id'] );
return current_user_can( 'delete_post', $post->ID );
}
}
// Controller 인스턴스화 및 등록
add_action( 'rest_api_init', function() {
$controller = new My_Custom_Posts_Controller();
$controller->register_routes();
} );
5 인증과 권한 관리
WordPress 기본 인증 방법
쿠키 인증
WordPress 프런트엔드에서 사용하기 적합
- • 자동 세션 관리
- • Nonce 검증 필요
- • CSRF 보호
Application Password
외부 애플리케이션에서 사용
- • HTTP 기본 인증
- • 사용자별 비밀번호
- • 개별 권한 관리
고급 권한 확인 예제
<?php
/**
* 고급 권한 확인 함수
*/
function advanced_permission_check( WP_REST_Request $request ) {
// 현재 사용자가 로그인되어 있는지 확인
if ( !is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', '로그인이 필요합니다.', array( 'status' => 401 ) );
}
// 관리자는 모든 작업 허용
if ( current_user_can( 'manage_options' ) ) {
return true;
}
// GET 요청은 읽기 권한만 확인
if ( $request->get_method() === 'GET' ) {
return current_user_can( 'read' );
}
// POST/PUT 요청의 경우 작성자 확인
if ( in_array( $request->get_method(), array( 'POST', 'PUT', 'PATCH' ) ) ) {
$post_id = $request->get_param( 'id' );
if ( $post_id ) {
$post = get_post( $post_id );
if ( $post && $post->post_author == get_current_user_id() ) {
return true;
}
}
return current_user_can( 'edit_others_posts' );
}
// DELETE 요청의 경우 삭제 권한 확인
if ( $request->get_method() === 'DELETE' ) {
$post_id = $request->get_param( 'id' );
if ( $post_id ) {
return current_user_can( 'delete_post', $post_id );
}
}
return false;
}
/**
* 사용자 역할별 권한 확인
*/
function check_user_role_permission( WP_REST_Request $request ) {
$required_roles = array( 'editor', 'administrator' );
if ( !is_user_logged_in() ) {
return false;
}
$user = wp_get_current_user();
$user_roles = $user->roles;
return !empty( array_intersect( $required_roles, $user_roles ) );
}
/**
* API 키 기반 인증 (커스텀)
*/
function api_key_authentication( WP_REST_Request $request ) {
$api_key = $request->get_header( 'X-API-Key' );
if ( empty( $api_key ) ) {
return new WP_Error( 'missing_api_key', 'API 키가 필요합니다.', array( 'status' => 401 ) );
}
// 저장된 API 키와 비교
$valid_keys = get_option( 'my_plugin_api_keys', array() );
if ( !in_array( $api_key, $valid_keys ) ) {
return new WP_Error( 'invalid_api_key', '잘못된 API 키입니다.', array( 'status' => 403 ) );
}
return true;
}
/**
* 시간 기반 액세스 제어
*/
function time_based_access_control( WP_REST_Request $request ) {
$current_hour = (int) date( 'H' );
$business_hours_start = 9; // 오전 9시
$business_hours_end = 18; // 오후 6시
if ( $current_hour < $business_hours_start || $current_hour >= $business_hours_end ) {
return new WP_Error( 'outside_business_hours', '업무시간 외에는 접근할 수 없습니다.', array( 'status' => 403 ) );
}
return current_user_can( 'read' );
}
// Nonce 검증을 포함한 안전한 엔드포인트
add_action( 'rest_api_init', function() {
register_rest_route( 'secure/v1', '/data', array(
'methods' => 'POST',
'callback' => 'secure_data_handler',
'permission_callback' => function( WP_REST_Request $request ) {
// Nonce 검증 (프런트엔드에서 온 요청의 경우)
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( $nonce && !wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error( 'rest_cookie_invalid_nonce', 'Nonce 검증 실패', array( 'status' => 403 ) );
}
return current_user_can( 'edit_posts' );
},
) );
} );
보안 주의사항
- • 모든 엔드포인트에 적절한 permission_callback 설정
- • 입력 데이터의 검증과 사니타이제이션 필수
- • 민감한 정보는 응답에 포함하지 않기
- • Rate limiting으로 무차별 대입 공격 방지
6 실전 예제
전자상거래 제품 API
WooCommerce 스타일 제품 API
<?php
class Product_API_Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = 'shop/v1';
$this->rest_base = 'products';
}
public function register_routes() {
// 제품 목록 및 생성
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_products' ),
'permission_callback' => '__return_true',
'args' => array(
'category' => array(
'description' => '제품 카테고리 ID',
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'price_min' => array(
'description' => '최소 가격',
'type' => 'number',
'minimum' => 0,
),
'price_max' => array(
'description' => '최대 가격',
'type' => 'number',
'minimum' => 0,
),
'on_sale' => array(
'description' => '할인 제품만 조회',
'type' => 'boolean',
),
'featured' => array(
'description' => '추천 제품만 조회',
'type' => 'boolean',
),
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_product' ),
'permission_callback' => array( $this, 'create_product_permissions_check' ),
),
) );
// 단일 제품
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_product' ),
'permission_callback' => '__return_true',
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_product' ),
'permission_callback' => array( $this, 'update_product_permissions_check' ),
),
) );
// 제품 리뷰
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/reviews', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_product_reviews' ),
'permission_callback' => '__return_true',
) );
// 재고 업데이트
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/stock', array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_stock' ),
'permission_callback' => array( $this, 'manage_stock_permissions_check' ),
'args' => array(
'quantity' => array(
'required' => true,
'type' => 'integer',
'minimum' => 0,
),
'operation' => array(
'required' => true,
'enum' => array( 'set', 'increase', 'decrease' ),
),
),
) );
}
/**
* 제품 목록 조회
*/
public function get_products( WP_REST_Request $request ) {
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $request->get_param( 'per_page' ) ?: 10,
'paged' => $request->get_param( 'page' ) ?: 1,
'meta_query' => array(),
);
// 카테고리 필터
if ( $request->get_param( 'category' ) ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $request->get_param( 'category' ),
),
);
}
// 가격 범위 필터
if ( $request->get_param( 'price_min' ) || $request->get_param( 'price_max' ) ) {
$price_query = array(
'key' => '_price',
'type' => 'NUMERIC',
'compare' => 'BETWEEN',
);
$price_range = array(
$request->get_param( 'price_min' ) ?: 0,
$request->get_param( 'price_max' ) ?: 999999999,
);
$price_query['value'] = $price_range;
$args['meta_query'][] = $price_query;
}
// 할인 제품 필터
if ( $request->get_param( 'on_sale' ) ) {
$args['meta_query'][] = array(
'key' => '_sale_price',
'value' => '',
'compare' => '!=',
);
}
// 추천 제품 필터
if ( $request->get_param( 'featured' ) ) {
$args['meta_query'][] = array(
'key' => '_featured',
'value' => 'yes',
'compare' => '=',
);
}
$query = new WP_Query( $args );
$products = array();
foreach ( $query->posts as $post ) {
$products[] = $this->prepare_product_data( $post );
}
$response = rest_ensure_response( $products );
$response->header( 'X-WP-Total', (int) $query->found_posts );
return $response;
}
/**
* 제품 데이터 준비
*/
private function prepare_product_data( $post ) {
return array(
'id' => $post->ID,
'name' => $post->post_title,
'description' => $post->post_content,
'short_description' => $post->post_excerpt,
'sku' => get_post_meta( $post->ID, '_sku', true ),
'price' => array(
'regular' => get_post_meta( $post->ID, '_regular_price', true ),
'sale' => get_post_meta( $post->ID, '_sale_price', true ),
'current' => get_post_meta( $post->ID, '_price', true ),
),
'stock' => array(
'quantity' => get_post_meta( $post->ID, '_stock', true ),
'status' => get_post_meta( $post->ID, '_stock_status', true ),
'manage_stock' => get_post_meta( $post->ID, '_manage_stock', true ) === 'yes',
),
'categories' => wp_get_post_terms( $post->ID, 'product_cat', array( 'fields' => 'names' ) ),
'tags' => wp_get_post_terms( $post->ID, 'product_tag', array( 'fields' => 'names' ) ),
'images' => $this->get_product_images( $post->ID ),
'attributes' => $this->get_product_attributes( $post->ID ),
'featured' => get_post_meta( $post->ID, '_featured', true ) === 'yes',
'permalink' => get_permalink( $post->ID ),
'date_created' => $post->post_date,
'date_modified' => $post->post_modified,
);
}
/**
* 제품 이미지 가져오기
*/
private function get_product_images( $product_id ) {
$images = array();
// 대표 이미지
$thumbnail_id = get_post_thumbnail_id( $product_id );
if ( $thumbnail_id ) {
$images[] = array(
'id' => $thumbnail_id,
'src' => wp_get_attachment_image_url( $thumbnail_id, 'full' ),
'thumbnail' => wp_get_attachment_image_url( $thumbnail_id, 'thumbnail' ),
'alt' => get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true ),
);
}
// 갤러리 이미지
$gallery_ids = get_post_meta( $product_id, '_product_image_gallery', true );
if ( $gallery_ids ) {
$gallery_ids = explode( ',', $gallery_ids );
foreach ( $gallery_ids as $image_id ) {
$images[] = array(
'id' => $image_id,
'src' => wp_get_attachment_image_url( $image_id, 'full' ),
'thumbnail' => wp_get_attachment_image_url( $image_id, 'thumbnail' ),
'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
);
}
}
return $images;
}
/**
* 재고 업데이트
*/
public function update_stock( WP_REST_Request $request ) {
$product_id = $request['id'];
$quantity = $request['quantity'];
$operation = $request['operation'];
$product = get_post( $product_id );
if ( !$product || $product->post_type !== 'product' ) {
return new WP_Error( 'invalid_product', '제품을 찾을 수 없습니다.', array( 'status' => 404 ) );
}
$current_stock = (int) get_post_meta( $product_id, '_stock', true );
switch ( $operation ) {
case 'set':
$new_stock = $quantity;
break;
case 'increase':
$new_stock = $current_stock + $quantity;
break;
case 'decrease':
$new_stock = max( 0, $current_stock - $quantity );
break;
default:
return new WP_Error( 'invalid_operation', '잘못된 작업입니다.', array( 'status' => 400 ) );
}
update_post_meta( $product_id, '_stock', $new_stock );
update_post_meta( $product_id, '_stock_status', $new_stock > 0 ? 'instock' : 'outofstock' );
return rest_ensure_response( array(
'product_id' => $product_id,
'previous_stock' => $current_stock,
'new_stock' => $new_stock,
'operation' => $operation,
) );
}
public function create_product_permissions_check( $request ) {
return current_user_can( 'manage_woocommerce' );
}
public function manage_stock_permissions_check( $request ) {
return current_user_can( 'manage_woocommerce' );
}
}
// API 등록
add_action( 'rest_api_init', function() {
$controller = new Product_API_Controller();
$controller->register_routes();
} );
대시보드 통계 API
통계 및 분석 API
<?php
add_action( 'rest_api_init', function() {
// 사이트 통계 API
register_rest_route( 'analytics/v1', '/dashboard', array(
'methods' => 'GET',
'callback' => 'get_dashboard_stats',
'permission_callback' => function() {
return current_user_can( 'read' );
},
'args' => array(
'period' => array(
'default' => '30days',
'enum' => array( '7days', '30days', '90days', '1year' ),
),
),
) );
// 사용자 활동 API
register_rest_route( 'analytics/v1', '/user-activity', array(
'methods' => 'GET',
'callback' => 'get_user_activity_stats',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
) );
// 콘텐츠 성능 API
register_rest_route( 'analytics/v1', '/content-performance', array(
'methods' => 'GET',
'callback' => 'get_content_performance',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => array(
'post_type' => array(
'default' => 'post',
'sanitize_callback' => 'sanitize_key',
),
'limit' => array(
'default' => 10,
'sanitize_callback' => 'absint',
),
),
) );
} );
function get_dashboard_stats( WP_REST_Request $request ) {
$period = $request['period'];
$cache_key = 'dashboard_stats_' . $period;
// 캐시 확인
$cached_stats = wp_cache_get( $cache_key );
if ( $cached_stats !== false ) {
return $cached_stats;
}
// 기간별 날짜 계산
$date_query = get_date_query_for_period( $period );
$stats = array(
'posts' => array(
'total' => wp_count_posts( 'post' )->publish,
'recent' => count_posts_in_period( 'post', $date_query ),
),
'pages' => array(
'total' => wp_count_posts( 'page' )->publish,
'recent' => count_posts_in_period( 'page', $date_query ),
),
'comments' => array(
'total' => wp_count_comments()->approved,
'recent' => count_comments_in_period( $date_query ),
'pending' => wp_count_comments()->moderated,
),
'users' => array(
'total' => count_users()['total_users'],
'new' => count_new_users_in_period( $date_query ),
),
'media' => array(
'total' => wp_count_posts( 'attachment' )->inherit,
'recent' => count_posts_in_period( 'attachment', $date_query ),
),
'popular_posts' => get_popular_posts( 10 ),
'recent_activity' => get_recent_activity( 20 ),
'period' => $period,
'generated_at' => current_time( 'mysql' ),
);
// 1시간 캐시
wp_cache_set( $cache_key, $stats, '', 3600 );
return rest_ensure_response( $stats );
}
function get_content_performance( WP_REST_Request $request ) {
global $wpdb;
$post_type = $request['post_type'];
$limit = $request['limit'];
// 조회수가 많은 포스트 (조회수 플러그인 사용 가정)
$popular_posts_query = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_date, pm.meta_value as views
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'post_views_count'
WHERE p.post_type = %s AND p.post_status = 'publish'
ORDER BY CAST(pm.meta_value AS UNSIGNED) DESC
LIMIT %d",
$post_type,
$limit
);
$popular_posts = $wpdb->get_results( $popular_posts_query );
// 댓글이 많은 포스트
$commented_posts_query = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_date, COUNT(c.comment_ID) as comment_count
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->comments} c ON p.ID = c.comment_post_ID AND c.comment_approved = '1'
WHERE p.post_type = %s AND p.post_status = 'publish'
GROUP BY p.ID
ORDER BY comment_count DESC
LIMIT %d",
$post_type,
$limit
);
$commented_posts = $wpdb->get_results( $commented_posts_query );
// 최근 포스트 성능
$recent_posts_query = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_date,
COUNT(c.comment_ID) as comments,
IFNULL(pm.meta_value, 0) as views
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->comments} c ON p.ID = c.comment_post_ID AND c.comment_approved = '1'
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'post_views_count'
WHERE p.post_type = %s AND p.post_status = 'publish'
AND p.post_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY p.ID
ORDER BY p.post_date DESC
LIMIT %d",
$post_type,
$limit
);
$recent_posts = $wpdb->get_results( $recent_posts_query );
return rest_ensure_response( array(
'popular_posts' => $popular_posts,
'most_commented' => $commented_posts,
'recent_performance' => $recent_posts,
'post_type' => $post_type,
'generated_at' => current_time( 'mysql' ),
) );
}
// 헬퍼 함수들
function get_date_query_for_period( $period ) {
switch ( $period ) {
case '7days':
return array( 'after' => '7 days ago' );
case '30days':
return array( 'after' => '30 days ago' );
case '90days':
return array( 'after' => '90 days ago' );
case '1year':
return array( 'after' => '1 year ago' );
default:
return array( 'after' => '30 days ago' );
}
}
function count_posts_in_period( $post_type, $date_query ) {
$query = new WP_Query( array(
'post_type' => $post_type,
'post_status' => 'publish',
'date_query' => $date_query,
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
return $query->found_posts;
}
7 모범 사례와 팁
해야 할 것들
- 항상 적절한 HTTP 상태 코드 반환
- 입력 데이터 검증과 사니타이제이션
- 일관성 있는 네이밍 컨벤션 사용
- 적절한 캐싱 전략 구현
- 상세한 API 문서 작성
- 버전 관리를 통한 하위 호환성
하지 말아야 할 것들
- 권한 확인 없이 민감한 데이터 노출
- 하드코딩된 값이나 설정 사용
- SQL 인젝션에 취약한 쿼리 작성
- 과도한 데이터를 한 번에 반환
- 에러 메시지에서 시스템 정보 누출
- Rate limiting 없이 API 공개
성능 최적화 팁
성능 최적화 예제
<?php
// 1. 캐싱 전략
function cached_api_response( $cache_key, $callback, $expiration = 3600 ) {
$cached = wp_cache_get( $cache_key );
if ( $cached !== false ) {
return $cached;
}
$result = call_user_func( $callback );
wp_cache_set( $cache_key, $result, '', $expiration );
return $result;
}
// 2. 데이터베이스 쿼리 최적화
function optimized_post_query( $args ) {
// 불필요한 메타 데이터 로딩 방지
$defaults = array(
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'no_found_rows' => true, // 페이징이 필요없다면
);
$args = wp_parse_args( $args, $defaults );
return new WP_Query( $args );
}
// 3. 페이지네이션 구현
function paginated_api_response( WP_REST_Request $request, $query ) {
$page = $request->get_param( 'page' ) ?: 1;
$per_page = min( $request->get_param( 'per_page' ) ?: 10, 100 ); // 최대 100개 제한
$response = rest_ensure_response( $data );
// 페이지네이션 헤더 추가
$response->header( 'X-WP-Total', $query->found_posts );
$response->header( 'X-WP-TotalPages', $query->max_num_pages );
// 링크 헤더 추가 (RESTful 방식)
$base_url = rest_url( 'myplugin/v1/posts' );
$links = array();
if ( $page > 1 ) {
$links[] = '<' . add_query_arg( 'page', $page - 1, $base_url ) . '>; rel="prev"';
}
if ( $page < $query->max_num_pages ) {
$links[] = '<' . add_query_arg( 'page', $page + 1, $base_url ) . '>; rel="next"';
}
if ( !empty( $links ) ) {
$response->header( 'Link', implode( ', ', $links ) );
}
return $response;
}
// 4. Rate Limiting 구현
function implement_rate_limiting( WP_REST_Request $request ) {
$user_id = get_current_user_id();
$ip_address = $_SERVER['REMOTE_ADDR'];
$identifier = $user_id ?: $ip_address;
$rate_limit_key = 'api_rate_limit_' . md5( $identifier );
$requests = wp_cache_get( $rate_limit_key );
$limit = 100; // 시간당 100회 제한
$window = 3600; // 1시간
if ( $requests === false ) {
wp_cache_set( $rate_limit_key, 1, '', $window );
} else {
if ( $requests >= $limit ) {
return new WP_Error( 'rate_limit_exceeded',
'요청 한도를 초과했습니다. 잠시 후 다시 시도하세요.',
array( 'status' => 429 )
);
}
wp_cache_set( $rate_limit_key, $requests + 1, '', $window );
}
return true;
}
// 5. 조건부 응답 (ETag 활용)
function conditional_response( $data, $request ) {
$etag = md5( serialize( $data ) );
$client_etag = $request->get_header( 'If-None-Match' );
if ( $client_etag === $etag ) {
return new WP_REST_Response( null, 304 );
}
$response = rest_ensure_response( $data );
$response->header( 'ETag', $etag );
$response->header( 'Cache-Control', 'public, max-age=3600' );
return $response;
}
디버깅과 로깅
디버깅 도구
<?php
// API 로깅 시스템
class API_Logger {
public static function log_request( WP_REST_Request $request, $response = null ) {
if ( !WP_DEBUG || !WP_DEBUG_LOG ) {
return;
}
$log_data = array(
'timestamp' => current_time( 'mysql' ),
'method' => $request->get_method(),
'route' => $request->get_route(),
'params' => $request->get_params(),
'user_id' => get_current_user_id(),
'ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
);
if ( $response ) {
$log_data['status_code'] = $response->get_status();
}
error_log( 'API Request: ' . wp_json_encode( $log_data ) );
}
public static function log_error( $error, $context = array() ) {
$log_data = array(
'timestamp' => current_time( 'mysql' ),
'error' => $error,
'context' => $context,
'backtrace' => wp_debug_backtrace_summary(),
);
error_log( 'API Error: ' . wp_json_encode( $log_data ) );
}
}
// API 응답 시간 측정
add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) {
$GLOBALS['api_start_time'] = microtime( true );
return $result;
}, 10, 3 );
add_filter( 'rest_post_dispatch', function( $response, $server, $request ) {
if ( isset( $GLOBALS['api_start_time'] ) ) {
$execution_time = microtime( true ) - $GLOBALS['api_start_time'];
$response->header( 'X-Execution-Time', number_format( $execution_time, 4 ) . 's' );
}
// 요청 로깅
API_Logger::log_request( $request, $response );
return $response;
}, 10, 3 );
// 개발 환경에서만 활성화되는 디버그 엔드포인트
if ( WP_DEBUG ) {
add_action( 'rest_api_init', function() {
register_rest_route( 'debug/v1', '/info', array(
'methods' => 'GET',
'callback' => function() {
return array(
'wp_version' => get_bloginfo( 'version' ),
'php_version' => PHP_VERSION,
'memory_usage' => memory_get_usage( true ),
'memory_limit' => ini_get( 'memory_limit' ),
'time_limit' => ini_get( 'max_execution_time' ),
'plugins' => get_option( 'active_plugins' ),
);
},
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
) );
} );
}
추가 학습 리소스
- WordPress REST API Handbook: developer.wordpress.org/rest-api/
- Postman으로 API 테스트: getpostman.com
- REST API 클라이언트 라이브러리: wp-api/node-wpapi (JavaScript)
- 인증 플러그인: JWT Authentication, Application Passwords
축하합니다! 🎉
WordPress REST API 커스텀 엔드포인트 개발의 모든 과정을 완주하셨습니다.
이제 여러분은 강력하고 안전한 WordPress API를 구축할 수 있는 모든 지식을 갖추었습니다!
반응형