티스토리 뷰

목차


    반응형

     

     

     

    WordPress REST API 완전 가이드

    커스텀 엔드포인트 생성부터 실전 활용까지

    기초부터 고급까지, 실제 코드 예제와 함께 배우는 완전 가이드

    WordPress REST API란?

    WordPress REST API는 WordPress와 외부 애플리케이션 간의 데이터 교환을 위한 강력한 인터페이스입니다. 이 가이드에서는 커스텀 엔드포인트를 생성하여 원하는 기능을 구현하는 방법을 자세히 알아보겠습니다.

    WordPress REST API 개요

    목차

    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 코어 패턴과의 일관성
    • 유지보수와 확장의 용이함
    • 표준화된 메소드명과 구조
    WordPress API 개발
    완전한 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를 구축할 수 있는 모든 지식을 갖추었습니다!

    © 2024 WordPress REST API 완전 가이드

    기초부터 고급까지, 실전 예제로 배우는 커스텀 엔드포인트 개발

    반응형