์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์›Œ๋“œํ”„๋ ˆ์Šค REST API๋กœ ํ—ค๋“œ๋ฆฌ์Šค CMS ๋งŒ๋“ค๊ธฐ

๐Ÿš€ ์›Œ๋“œํ”„๋ ˆ์Šค REST API๋กœ ํ—ค๋“œ๋ฆฌ์Šค CMS ๋งŒ๋“ค๊ธฐ

ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ์˜ ์™„๋ฒฝํ•œ ๋ถ„๋ฆฌ, ๊ทธ ์‹ ์„ธ๊ณ„๋กœ์˜ ์ดˆ๋Œ€ โœจ

๐ŸŽฏ ํ—ค๋“œ๋ฆฌ์Šค CMS, ๋„๋Œ€์ฒด ๋ญ๊ธธ๋ž˜?

์š”์ฆ˜ ์›น ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋ฅผ ๋ณด๋ฉด "ํ—ค๋“œ๋ฆฌ์Šค(Headless)"๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ์ •๋ง ์ž์ฃผ ๋“ฑ์žฅํ•ด. ๋งˆ์น˜ ๋จธ๋ฆฌ ์—†๋Š” ๊ธฐ์‚ฌ์ฒ˜๋Ÿผ ๋“ค๋ฆฌ์ง€๋งŒ, ์‚ฌ์‹ค์€ ์—„์ฒญ๋‚˜๊ฒŒ ๊ฐ•๋ ฅํ•œ ๊ฐœ๋…์ด์•ผ! ๐Ÿ˜„

์ „ํ†ต์ ์ธ CMS๋Š” ๋ฐฑ์—”๋“œ(๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ)์™€ ํ”„๋ก ํŠธ์—”๋“œ(ํ™”๋ฉด ํ‘œ์‹œ)๊ฐ€ ํ•˜๋‚˜๋กœ ๋ฌถ์—ฌ ์žˆ์–ด. ์›Œ๋“œํ”„๋ ˆ์Šค๋กœ ์˜ˆ๋ฅผ ๋“ค๋ฉด, PHP ํ…Œ๋งˆ ํŒŒ์ผ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ฐ”๋กœ HTML์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ด์ง€. ํ•˜์ง€๋งŒ ํ—ค๋“œ๋ฆฌ์Šค CMS๋Š” "๋จธ๋ฆฌ"์— ํ•ด๋‹นํ•˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๋ถ€๋ถ„์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•ด๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์•ผ.

์™œ ์ด๋ ‡๊ฒŒ ํ• ๊นŒ? ๐Ÿค” ์ž์œ ๋„๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋†’์•„์ง€๊ฑฐ๋“ ! React, Vue, Next.js ๊ฐ™์€ ์ตœ์‹  ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋งˆ์Œ๊ป ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ๋ชจ๋ฐ”์ผ ์•ฑ, ์›น, IoT ๊ธฐ๊ธฐ ๋“ฑ ์—ฌ๋Ÿฌ ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•œ ์ฝ˜ํ…์ธ ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

WordPress Backend REST API ๐Ÿ”„ React Web App Vue.js Mobile App Next.js SSR Site โœ… ์ž์œ ๋กœ์šด ๊ธฐ์ˆ  ์„ ํƒ โœ… ๋‹ค์ค‘ ํ”Œ๋žซํผ ์ง€์› โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™” โœ… ๋…๋ฆฝ์  ๊ฐœ๋ฐœ โœ… ํ™•์žฅ์„ฑ ๊ทน๋Œ€ํ™”

๐Ÿ”ง ์›Œ๋“œํ”„๋ ˆ์Šค REST API์˜ ๊ฐ•๋ ฅํ•จ

์›Œ๋“œํ”„๋ ˆ์Šค๋Š” 2016๋…„ ๋ฒ„์ „ 4.7๋ถ€ํ„ฐ REST API๋ฅผ ์ฝ”์–ด์— ํฌํ•จ์‹œ์ผฐ์–ด. ์ด๊ฒŒ ์ •๋ง ๊ฒŒ์ž„ ์ฒด์ธ์ €์˜€์ง€! ์ด์ œ ์›Œ๋“œํ”„๋ ˆ์Šค๋Š” ๋‹จ์ˆœํ•œ ๋ธ”๋กœ๊ทธ ํ”Œ๋žซํผ์ด ์•„๋‹ˆ๋ผ, ๊ฐ•๋ ฅํ•œ ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ ๋ฐฑ์—”๋“œ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฑฐ์•ผ. ๐Ÿ’ช

REST API๋Š” HTTP ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์ด์•ผ. ์›Œ๋“œํ”„๋ ˆ์Šค REST API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒŒ์‹œ๊ธ€, ํŽ˜์ด์ง€, ๋ฏธ๋””์–ด, ์‚ฌ์šฉ์ž, ๋Œ“๊ธ€ ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ“Œ ์›Œ๋“œํ”„๋ ˆ์Šค REST API์˜ ์ฃผ์š” ์—”๋“œํฌ์ธํŠธ /wp-json/wp/v2/posts - ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์ •๋ณด
/wp-json/wp/v2/pages - ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ
/wp-json/wp/v2/media - ๋ฏธ๋””์–ด ํŒŒ์ผ
/wp-json/wp/v2/categories - ์นดํ…Œ๊ณ ๋ฆฌ
/wp-json/wp/v2/tags - ํƒœ๊ทธ
/wp-json/wp/v2/users - ์‚ฌ์šฉ์ž ์ •๋ณด
/wp-json/wp/v2/comments - ๋Œ“๊ธ€

์˜ˆ๋ฅผ ๋“ค์–ด, ์ตœ์‹  ๊ฒŒ์‹œ๊ธ€ 10๊ฐœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด? ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•ด:

fetch('https://yoursite.com/wp-json/wp/v2/posts?per_page=10')
  .then(response => response.json())
  .then(data => console.log(data));

์ด๋ ‡๊ฒŒ ๊ฐ€์ ธ์˜จ JSON ๋ฐ์ดํ„ฐ๋ฅผ React, Vue, Angular ๋“ฑ ์–ด๋–ค ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋“  ์ž์œ ๋กญ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

๐Ÿ—๏ธ ํ—ค๋“œ๋ฆฌ์Šค CMS ๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

1๋‹จ๊ณ„: ์›Œ๋“œํ”„๋ ˆ์Šค ํ™˜๊ฒฝ ์ค€๋น„ ๐Ÿ› ๏ธ

๋จผ์ € ์›Œ๋“œํ”„๋ ˆ์Šค๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด. ๋กœ์ปฌ ํ™˜๊ฒฝ์ด๋“  ์„œ๋ฒ„๋“  ์ƒ๊ด€์—†์–ด. ์ค‘์š”ํ•œ ๊ฑด REST API๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฑฐ์•ผ.

์›Œ๋“œํ”„๋ ˆ์Šค ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ ์„ค์ • โ†’ ๊ณ ์œ ์ฃผ์†Œ๋กœ ๊ฐ€์„œ "๊ธฐ๋ณธ" ์ด์™ธ์˜ ๋‹ค๋ฅธ ์˜ต์…˜์„ ์„ ํƒํ•ด์ค˜. REST API๋Š” ๊ณ ์œ ์ฃผ์†Œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด์•ผ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ฑฐ๋“ !

๐Ÿ’ก ํŒ: ๋ธŒ๋ผ์šฐ์ €์—์„œ https://yoursite.com/wp-json/์— ์ ‘์†ํ•ด๋ด. JSON ํ˜•์‹์˜ API ์ •๋ณด๊ฐ€ ๋ณด์ด๋ฉด ์„ฑ๊ณต์ด์•ผ!
2๋‹จ๊ณ„: CORS ์„ค์ •ํ•˜๊ธฐ ๐ŸŒ

ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์— ์žˆ์„ ๊ฒฝ์šฐ, CORS(Cross-Origin Resource Sharing) ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฑธ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์›Œ๋“œํ”„๋ ˆ์Šค์˜ functions.php ํŒŒ์ผ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜:

function add_cors_http_header(){
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
}
add_action('init','add_cors_http_header');

๋ฌผ๋ก  ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” * ๋Œ€์‹  ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๊ฒŒ ๋ณด์•ˆ์ƒ ์ข‹์•„! ๐Ÿ”’

3๋‹จ๊ณ„: ์ปค์Šคํ…€ ์—”๋“œํฌ์ธํŠธ ๋งŒ๋“ค๊ธฐ โš™๏ธ

๊ธฐ๋ณธ REST API๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•  ๋•Œ๊ฐ€ ์žˆ์–ด. ํŠน๋ณ„ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์„ ๋•Œ ๋ง์ด์•ผ. ์ด๋Ÿด ๋•Œ๋Š” ์ปค์Šคํ…€ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด ๋ผ!

add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/featured-posts/', array(
        'methods' => 'GET',
        'callback' => 'get_featured_posts',
        'permission_callback' => '__return_true'
    ));
});

function get_featured_posts() {
    $args = array(
        'post_type' => 'post',
        'posts_per_page' => 5,
        'meta_key' => 'featured',
        'meta_value' => '1'
    );
    
    $posts = get_posts($args);
    
    $data = array();
    foreach($posts as $post) {
        $data[] = array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'excerpt' => $post->post_excerpt,
            'thumbnail' => get_the_post_thumbnail_url($post->ID, 'medium')
        );
    }
    
    return new WP_REST_Response($data, 200);
}

์ด์ œ /wp-json/custom/v1/featured-posts/๋กœ ์ ‘๊ทผํ•˜๋ฉด ํŠน๋ณ„ํžˆ ์„ ๋ณ„๋œ ๊ฒŒ์‹œ๊ธ€๋งŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

4๋‹จ๊ณ„: ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๐Ÿ”

๊ฒŒ์‹œ๊ธ€์„ ์ฝ๋Š” ๊ฑด ๋ˆ„๊ตฌ๋‚˜ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ์ธ์ฆ์ด ํ•„์š”ํ•ด. ์›Œ๋“œํ”„๋ ˆ์Šค REST API๋Š” ์—ฌ๋Ÿฌ ์ธ์ฆ ๋ฐฉ์‹์„ ์ง€์›ํ•ด:

์ธ์ฆ ๋ฐฉ์‹ ํŠน์ง• ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค
์ฟ ํ‚ค ์ธ์ฆ ์›Œ๋“œํ”„๋ ˆ์Šค ๊ธฐ๋ณธ ๋ฐฉ์‹ ๊ฐ™์€ ๋„๋ฉ”์ธ ๋‚ด์—์„œ ์‚ฌ์šฉ
JWT ํ† ํฐ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋จ ์™ธ๋ถ€ ํ”„๋ก ํŠธ์—”๋“œ ์•ฑ
OAuth ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ ์ œ3์ž ์•ฑ ์—ฐ๋™
Application Password WP 5.6+ ๊ธฐ๋ณธ ์ œ๊ณต ๊ฐ„๋‹จํ•œ ์ธ์ฆ์ด ํ•„์š”ํ•  ๋•Œ

JWT(JSON Web Token) ๋ฐฉ์‹์ด ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ์–ด. JWT Authentication for WP REST API ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ•˜๋ฉด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!

// ๋กœ๊ทธ์ธ ์š”์ฒญ
fetch('https://yoursite.com/wp-json/jwt-auth/v1/token', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        username: 'your_username',
        password: 'your_password'
    })
})
.then(response => response.json())
.then(data => {
    // ํ† ํฐ ์ €์žฅ
    localStorage.setItem('token', data.token);
});
5๋‹จ๊ณ„: ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌ์ถ• (React ์˜ˆ์ œ) โš›๏ธ

์ด์ œ ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์ด์•ผ! React๋กœ ์‹ค์ œ ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๋จผ์ € ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด:

npx create-react-app my-headless-site
cd my-headless-site
npm install axios

๊ฐ„๋‹จํ•œ ๋ธ”๋กœ๊ทธ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function BlogList() {
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        axios.get('https://yoursite.com/wp-json/wp/v2/posts?_embed')
            .then(response => {
                setPosts(response.data);
                setLoading(false);
            })
            .catch(error => {
                console.error('Error fetching posts:', error);
                setLoading(false);
            });
    }, []);

    if (loading) return <div>๋กœ๋”ฉ ์ค‘...</div>;

    return (
        <div className="blog-list">
            {posts.map(post => (
                <article key={post.id} className="post-card">
                    {post._embedded?.['wp:featuredmedia'] && (
                        <img 
                            src={post._embedded['wp:featuredmedia'][0].source_url} 
                            alt={post.title.rendered}
                        />
                    )}
                    <h2>{post.title.rendered}</h2>
                    <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
                    <a href={`/post/${post.slug}`}>๋” ์ฝ๊ธฐ</a>
                </article>
            ))}
        </div>
    );
}

export default BlogList;

?_embed ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ธ๋„ค์ผ ์ด๋ฏธ์ง€, ์ž‘์„ฑ์ž ์ •๋ณด ๋“ฑ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ์—„์ฒญ ํŽธ๋ฆฌํ•˜์ง€? ๐Ÿ˜Š

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

์บ์‹ฑ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ’จ

๋งค๋ฒˆ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์„œ๋ฒ„์— ๋ถ€๋‹ด์ด ๊ฐ€๊ณ  ์†๋„๋„ ๋А๋ ค์ ธ. ์บ์‹ฑ์„ ์ ๊ทน ํ™œ์šฉํ•ด์•ผ ํ•ด!

์บ์‹ฑ ๋ ˆ์ด์–ด ๊ตฌ์„ฑ: 1. ๋ธŒ๋ผ์šฐ์ € ์บ์‹ฑ - localStorage๋‚˜ sessionStorage ํ™œ์šฉ
2. CDN ์บ์‹ฑ - Cloudflare, AWS CloudFront ๋“ฑ
3. ์„œ๋ฒ„ ์บ์‹ฑ - Redis, Memcached
4. ์›Œ๋“œํ”„๋ ˆ์Šค ์บ์‹ฑ - WP Super Cache, W3 Total Cache

React์—์„œ๋Š” SWR์ด๋‚˜ React Query ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์บ์‹ฑ๊ณผ ์žฌ๊ฒ€์ฆ์„ ์ฒ˜๋ฆฌํ•ด์ค˜:

import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());

function Posts() {
    const { data, error } = useSWR(
        'https://yoursite.com/wp-json/wp/v2/posts',
        fetcher,
        { refreshInterval: 60000 } // 1๋ถ„๋งˆ๋‹ค ์žฌ๊ฒ€์ฆ
    );

    if (error) return <div>๋กœ๋”ฉ ์‹คํŒจ</div>;
    if (!data) return <div>๋กœ๋”ฉ ์ค‘...</div>;

    return (
        <div>
            {data.map(post => (
                <div key={post.id}>{post.title.rendered}</div>
            ))}
        </div>
    );
}
์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๐Ÿ–ผ๏ธ

์›Œ๋“œํ”„๋ ˆ์Šค๋Š” ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€์˜ ์—ฌ๋Ÿฌ ํฌ๊ธฐ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด. REST API์—์„œ๋„ ์ด๊ฑธ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

// ๋ฏธ๋””์–ด ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
const media = post._embedded['wp:featuredmedia'][0];

// ๋‹ค์–‘ํ•œ ํฌ๊ธฐ ํ™œ์šฉ
const thumbnail = media.media_details.sizes.thumbnail.source_url;
const medium = media.media_details.sizes.medium.source_url;
const large = media.media_details.sizes.large.source_url;

๋ฐ˜์‘ํ˜• ์ด๋ฏธ์ง€๋ฅผ ์œ„ํ•ด srcset์„ ํ™œ์šฉํ•˜๋ฉด ๋”์šฑ ์ข‹์•„:

<img 
    src={medium}
    srcSet={`${thumbnail} 300w, ${medium} 768w, ${large} 1024w`}
    sizes="(max-width: 768px) 100vw, 768px"
    alt={post.title.rendered}
/>
ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ๋ฌดํ•œ ์Šคํฌ๋กค ๐Ÿ“œ

ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€์„ ๊ฐ€์ ธ์˜ค๋ฉด ์„ฑ๋Šฅ์ด ๋–จ์–ด์ ธ. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

function PaginatedPosts() {
    const [posts, setPosts] = useState([]);
    const [page, setPage] = useState(1);
    const [totalPages, setTotalPages] = useState(0);

    useEffect(() => {
        axios.get(`https://yoursite.com/wp-json/wp/v2/posts?page=${page}&per_page=10`)
            .then(response => {
                setPosts(response.data);
                setTotalPages(parseInt(response.headers['x-wp-totalpages']));
            });
    }, [page]);

    return (
        <div>
            {posts.map(post => (
                <div key={post.id}>{post.title.rendered}</div>
            ))}
            
            <div className="pagination">
                <button 
                    onClick={() => setPage(page - 1)} 
                    disabled={page === 1}
                >
                    ์ด์ „
                </button>
                <span>{page} / {totalPages}</span>
                <button 
                    onClick={() => setPage(page + 1)} 
                    disabled={page === totalPages}
                >
                    ๋‹ค์Œ
                </button>
            </div>
        </div>
    );
}

์›Œ๋“œํ”„๋ ˆ์Šค REST API๋Š” ์‘๋‹ต ํ—ค๋”์— ์ด ํŽ˜์ด์ง€ ์ˆ˜(X-WP-TotalPages)์™€ ์ด ๊ฒŒ์‹œ๊ธ€ ์ˆ˜(X-WP-Total)๋ฅผ ํฌํ•จํ•ด์ค˜. ์ด๊ฑธ ํ™œ์šฉํ•˜๋ฉด ํŽ˜์ด์ง€๋„ค์ด์…˜ UI๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿ‘

๐ŸŽจ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๐Ÿ”

์›Œ๋“œํ”„๋ ˆ์Šค REST API๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

function SearchPosts() {
    const [searchTerm, setSearchTerm] = useState('');
    const [results, setResults] = useState([]);

    const handleSearch = async () => {
        const response = await axios.get(
            `https://yoursite.com/wp-json/wp/v2/posts?search=${searchTerm}`
        );
        setResults(response.data);
    };

    return (
        <div>
            <input 
                type="text" 
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                placeholder="๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
            />
            <button onClick={handleSearch}>๊ฒ€์ƒ‰</button>
            
            <div className="search-results">
                {results.map(post => (
                    <div key={post.id}>
                        <h3>{post.title.rendered}</h3>
                        <p dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
                    </div>
                ))}
            </div>
        </div>
    );
}
์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ํƒœ๊ทธ ํ•„ํ„ฐ๋ง ๐Ÿท๏ธ

ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ๋‚˜ ํƒœ๊ทธ์˜ ๊ฒŒ์‹œ๊ธ€๋งŒ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์„ ๋•Œ:

// ์นดํ…Œ๊ณ ๋ฆฌ ID๋กœ ํ•„ํ„ฐ๋ง
axios.get('https://yoursite.com/wp-json/wp/v2/posts?categories=5')

// ํƒœ๊ทธ ID๋กœ ํ•„ํ„ฐ๋ง
axios.get('https://yoursite.com/wp-json/wp/v2/posts?tags=12')

// ์—ฌ๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ (OR ์กฐ๊ฑด)
axios.get('https://yoursite.com/wp-json/wp/v2/posts?categories=5,8,12')

// ์นดํ…Œ๊ณ ๋ฆฌ ์ œ์™ธ
axios.get('https://yoursite.com/wp-json/wp/v2/posts?categories_exclude=3')

์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ ๋จผ์ € ๊ฐ€์ ธ์™€์„œ ํ•„ํ„ฐ UI๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด:

function CategoryFilter() {
    const [categories, setCategories] = useState([]);
    const [selectedCategory, setSelectedCategory] = useState(null);
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        // ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
        axios.get('https://yoursite.com/wp-json/wp/v2/categories')
            .then(response => setCategories(response.data));
    }, []);

    useEffect(() => {
        // ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๊ฒŒ์‹œ๊ธ€ ๊ฐ€์ ธ์˜ค๊ธฐ
        const url = selectedCategory 
            ? `https://yoursite.com/wp-json/wp/v2/posts?categories=${selectedCategory}`
            : 'https://yoursite.com/wp-json/wp/v2/posts';
        
        axios.get(url).then(response => setPosts(response.data));
    }, [selectedCategory]);

    return (
        <div>
            <div className="category-filter">
                <button onClick={() => setSelectedCategory(null)}>์ „์ฒด</button>
                {categories.map(cat => (
                    <button 
                        key={cat.id}
                        onClick={() => setSelectedCategory(cat.id)}
                        className={selectedCategory === cat.id ? 'active' : ''}
                    >
                        {cat.name}
                    </button>
                ))}
            </div>
            
            <div className="posts">
                {posts.map(post => (
                    <div key={post.id}>{post.title.rendered}</div>
                ))}
            </div>
        </div>
    );
}
์ปค์Šคํ…€ ํ•„๋“œ (ACF) ํ™œ์šฉ ๐ŸŽ›๏ธ

Advanced Custom Fields(ACF) ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒŒ์‹œ๊ธ€์— ๋‹ค์–‘ํ•œ ์ปค์Šคํ…€ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. REST API์—์„œ๋„ ์ด๊ฑธ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด!

ACF Pro ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ๋ฌด๋ฃŒ ๋ฒ„์ „์—์„œ๋Š” ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜:

// functions.php์— ์ถ”๊ฐ€
add_action('rest_api_init', function() {
    register_rest_field('post', 'custom_fields', array(
        'get_callback' => function($post) {
            return get_fields($post['id']);
        }
    ));
});

์ด์ œ API ์‘๋‹ต์— custom_fields ํ•ญ๋ชฉ์ด ํฌํ•จ๋ผ:

{
    "id": 123,
    "title": {
        "rendered": "๋ฉ‹์ง„ ๊ฒŒ์‹œ๊ธ€"
    },
    "custom_fields": {
        "subtitle": "๋ถ€์ œ๋ชฉ์ž…๋‹ˆ๋‹ค",
        "author_info": "์ž‘์„ฑ์ž ์ •๋ณด",
        "rating": 4.5,
        "featured_video": "https://youtube.com/..."
    }
}
๋Œ“๊ธ€ ์‹œ์Šคํ…œ ๊ตฌํ˜„ ๐Ÿ’ฌ

๋Œ“๊ธ€๋„ REST API๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด:

// ํŠน์ • ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€ ๊ฐ€์ ธ์˜ค๊ธฐ
axios.get('https://yoursite.com/wp-json/wp/v2/comments?post=123')

// ๋Œ“๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ
axios.post('https://yoursite.com/wp-json/wp/v2/comments', {
    post: 123,
    author_name: 'ํ™๊ธธ๋™',
    author_email: 'hong@example.com',
    content: '์ข‹์€ ๊ธ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!'
}, {
    headers: {
        'Authorization': `Bearer ${token}`
    }
})

React ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๋ฉด:

function Comments({ postId }) {
    const [comments, setComments] = useState([]);
    const [newComment, setNewComment] = useState('');

    useEffect(() => {
        axios.get(`https://yoursite.com/wp-json/wp/v2/comments?post=${postId}`)
            .then(response => setComments(response.data));
    }, [postId]);

    const handleSubmit = async (e) => {
        e.preventDefault();
        
        try {
            const response = await axios.post(
                'https://yoursite.com/wp-json/wp/v2/comments',
                {
                    post: postId,
                    content: newComment
                },
                {
                    headers: {
                        'Authorization': `Bearer ${localStorage.getItem('token')}`
                    }
                }
            );
            
            setComments([...comments, response.data]);
            setNewComment('');
        } catch (error) {
            console.error('๋Œ“๊ธ€ ์ž‘์„ฑ ์‹คํŒจ:', error);
        }
    };

    return (
        <div className="comments-section">
            <h3>๋Œ“๊ธ€ {comments.length}๊ฐœ</h3>
            
            {comments.map(comment => (
                <div key={comment.id} className="comment">
                    <strong>{comment.author_name}</strong>
                    <p dangerouslySetInnerHTML={{ __html: comment.content.rendered }} />
                    <small>{new Date(comment.date).toLocaleString()}</small>
                </div>
            ))}
            
            <form onSubmit={handleSubmit}>
                <textarea 
                    value={newComment}
                    onChange={(e) => setNewComment(e.target.value)}
                    placeholder="๋Œ“๊ธ€์„ ์ž…๋ ฅํ•˜์„ธ์š”"
                />
                <button type="submit">๋Œ“๊ธ€ ์ž‘์„ฑ</button>
            </form>
        </div>
    );
}

๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”ํ•˜๊ธฐ

API ์ ‘๊ทผ ์ œํ•œ ๐Ÿ›ก๏ธ

REST API๋ฅผ ๊ณต๊ฐœํ•˜๋ฉด ๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด. ํ•„์š”์— ๋”ฐ๋ผ ์ ‘๊ทผ์„ ์ œํ•œํ•ด์•ผ ํ•ด:

// ํŠน์ • IP๋งŒ ํ—ˆ์šฉ
add_filter('rest_authentication_errors', function($result) {
    $allowed_ips = ['123.456.789.0', '111.222.333.444'];
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    if (!in_array($user_ip, $allowed_ips)) {
        return new WP_Error(
            'rest_forbidden',
            '์ ‘๊ทผ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
            array('status' => 403)
        );
    }
    
    return $result;
});
Rate Limiting ๊ตฌํ˜„ โฑ๏ธ

API ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด:

function check_rate_limit() {
    $ip = $_SERVER['REMOTE_ADDR'];
    $transient_key = 'api_rate_limit_' . md5($ip);
    
    $requests = get_transient($transient_key);
    
    if ($requests === false) {
        set_transient($transient_key, 1, MINUTE_IN_SECONDS);
    } else if ($requests >= 60) { // ๋ถ„๋‹น 60ํšŒ ์ œํ•œ
        return new WP_Error(
            'rate_limit_exceeded',
            '์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.',
            array('status' => 429)
        );
    } else {
        set_transient($transient_key, $requests + 1, MINUTE_IN_SECONDS);
    }
    
    return true;
}

add_filter('rest_pre_dispatch', function($result, $server, $request) {
    $rate_check = check_rate_limit();
    if (is_wp_error($rate_check)) {
        return $rate_check;
    }
    return $result;
}, 10, 3);
๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ์ˆจ๊ธฐ๊ธฐ ๐Ÿ™ˆ

๊ธฐ๋ณธ์ ์œผ๋กœ REST API๋Š” ๋งŽ์€ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•ด. ํ•„์š” ์—†๋Š” ์ •๋ณด๋Š” ์ˆจ๊ธฐ๋Š” ๊ฒŒ ์ข‹์•„:

// ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ ์ˆจ๊ธฐ๊ธฐ
add_filter('rest_prepare_user', function($response, $user, $request) {
    if (!current_user_can('edit_users')) {
        unset($response->data['email']);
    }
    return $response;
}, 10, 3);

// ํŠน์ • ๊ฒŒ์‹œ๊ธ€ ํƒ€์ž… ์ˆจ๊ธฐ๊ธฐ
add_filter('rest_prepare_post', function($response, $post, $request) {
    if ($post->post_status === 'draft' && !current_user_can('edit_posts')) {
        return new WP_Error(
            'rest_forbidden',
            '์ด ๊ฒŒ์‹œ๊ธ€์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.',
            array('status' => 403)
        );
    }
    return $response;
}, 10, 3);

๐ŸŒŸ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์˜ˆ์ œ

Next.js๋กœ SSR ๋ธ”๋กœ๊ทธ ๋งŒ๋“ค๊ธฐ ๐Ÿš€

Next.js๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR)๊ณผ ์ •์  ์‚ฌ์ดํŠธ ์ƒ์„ฑ(SSG)์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. SEO์—๋„ ์—„์ฒญ ์œ ๋ฆฌํ•˜์ง€!

// pages/index.js
export async function getStaticProps() {
    const res = await fetch('https://yoursite.com/wp-json/wp/v2/posts?_embed');
    const posts = await res.json();

    return {
        props: {
            posts,
        },
        revalidate: 60, // 60์ดˆ๋งˆ๋‹ค ์žฌ์ƒ์„ฑ
    };
}

export default function Home({ posts }) {
    return (
        <div>
            <h1>๋‚ด ๋ธ”๋กœ๊ทธ</h1>
            {posts.map(post => (
                <article key={post.id}>
                    <h2>{post.title.rendered}</h2>
                    <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
                    <Link href={`/posts/${post.slug}`}>
                        <a>๋” ์ฝ๊ธฐ</a>
                    </Link>
                </article>
            ))}
        </div>
    );
}

๊ฐœ๋ณ„ ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€๋„ ๋งŒ๋“ค์–ด๋ณด์ž:

// pages/posts/[slug].js
export async function getStaticPaths() {
    const res = await fetch('https://yoursite.com/wp-json/wp/v2/posts');
    const posts = await res.json();

    const paths = posts.map(post => ({
        params: { slug: post.slug },
    }));

    return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
    const res = await fetch(
        `https://yoursite.com/wp-json/wp/v2/posts?slug=${params.slug}&_embed`
    );
    const posts = await res.json();
    const post = posts[0];

    return {
        props: {
            post,
        },
        revalidate: 60,
    };
}

export default function Post({ post }) {
    return (
        <article>
            <h1>{post.title.rendered}</h1>
            {post._embedded?.['wp:featuredmedia'] && (
                <img 
                    src={post._embedded['wp:featuredmedia'][0].source_url}
                    alt={post.title.rendered}
                />
            )}
            <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
        </article>
    );
}
Vue.js๋กœ SPA ๋งŒ๋“ค๊ธฐ ๐Ÿ’š

Vue.js๋„ ์›Œ๋“œํ”„๋ ˆ์Šค REST API์™€ ํ™˜์ƒ์˜ ๊ถํ•ฉ์ด์•ผ:

// PostList.vue
<template>
  <div class="post-list">
    <div v-if="loading">๋กœ๋”ฉ ์ค‘...</div>
    <div v-else>
      <article v-for="post in posts" :key="post.id" class="post-card">
        <h2 v-html="post.title.rendered"></h2>
        <div v-html="post.excerpt.rendered"></div>
        <router-link :to="`/post/${post.slug}`">๋” ์ฝ๊ธฐ</router-link>
      </article>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'PostList',
  data() {
    return {
      posts: [],
      loading: true
    };
  },
  async mounted() {
    try {
      const response = await axios.get(
        'https://yoursite.com/wp-json/wp/v2/posts?_embed'
      );
      this.posts = response.data;
    } catch (error) {
      console.error('๊ฒŒ์‹œ๊ธ€ ๋กœ๋”ฉ ์‹คํŒจ:', error);
    } finally {
      this.loading = false;
    }
  }
};
</script>

<style scoped>
.post-card {
  border: 1px solid #e0e0e0;
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 8px;
}
</style>
๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™ ๐Ÿ“ฑ

React Native๋‚˜ Flutter๋กœ ๋ชจ๋ฐ”์ผ ์•ฑ์„ ๋งŒ๋“ค ๋•Œ๋„ ๋™์ผํ•œ API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฒŒ ํ—ค๋“œ๋ฆฌ์Šค CMS์˜ ์ง„์งœ ๊ฐ•์ ์ด์•ผ!

// React Native ์˜ˆ์ œ
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, Image, StyleSheet } from 'react-native';
import axios from 'axios';

const BlogApp = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    axios.get('https://yoursite.com/wp-json/wp/v2/posts?_embed')
      .then(response => setPosts(response.data));
  }, []);

  const renderPost = ({ item }) => (
    <View style={styles.postCard}>
      {item._embedded?.['wp:featuredmedia'] && (
        <Image 
          source={{ uri: item._embedded['wp:featuredmedia'][0].source_url }}
          style={styles.thumbnail}
        />
      )}
      <Text style={styles.title}>{item.title.rendered}</Text>
    </View>
  );

  return (
    <FlatList
      data={posts}
      renderItem={renderPost}
      keyExtractor={item => item.id.toString()}
    />
  );
};

const styles = StyleSheet.create({
  postCard: {
    padding: 15,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  thumbnail: {
    width: '100%',
    height: 200,
    borderRadius: 8,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginTop: 10,
  },
});

export default BlogApp;

๐ŸŽ“ ์žฌ๋Šฅ๋„ท์—์„œ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๊ธฐ

ํ—ค๋“œ๋ฆฌ์Šค CMS ๊ตฌํ˜„์ด ์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ๊ฑฑ์ • ๋งˆ! ์žฌ๋Šฅ๋„ท์—๋Š” ์›น ๊ฐœ๋ฐœ, API ์—ฐ๋™, ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์˜ ์ „๋ฌธ๊ฐ€๋“ค์ด ์žˆ์–ด. ๐ŸŽฏ

๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›๊ฑฐ๋‚˜, ํ”„๋กœ์ ํŠธ ์™ธ์ฃผ๋ฅผ ๋งก๊ธธ ์ˆ˜๋„ ์žˆ์–ด. ์‹ค์ œ๋กœ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์žฌ๋Šฅ๋„ท์„ ํ†ตํ•ด ์‹ค๋ฌด ๊ฒฝํ—˜์„ ์Œ“๊ณ , ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ค๊ณ  ์žˆ๊ฑฐ๋“ !

ํŠนํžˆ ์›Œ๋“œํ”„๋ ˆ์Šค ์ปค์Šคํ„ฐ๋งˆ์ด์ง•, REST API ๊ฐœ๋ฐœ, React/Vue.js ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌ์ถ• ๊ฐ™์€ ์„œ๋น„์Šค๋“ค์ด ์ธ๊ธฐ๊ฐ€ ๋งŽ์•„. ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ , ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ’ช

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๐Ÿ”ง ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค: 1. CORS ์—๋Ÿฌ
์ฆ์ƒ: ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์— "Access-Control-Allow-Origin" ์—๋Ÿฌ
ํ•ด๊ฒฐ: ์•ž์„œ ์„ค๋ช…ํ•œ CORS ํ—ค๋” ์ถ”๊ฐ€ ๋˜๋Š” ํ”„๋ก์‹œ ์„œ๋ฒ„ ์‚ฌ์šฉ

2. 404 ์—๋Ÿฌ
์ฆ์ƒ: REST API ์—”๋“œํฌ์ธํŠธ์— ์ ‘๊ทผ ๋ถˆ๊ฐ€
ํ•ด๊ฒฐ: ๊ณ ์œ ์ฃผ์†Œ ์„ค์ • ํ™•์ธ, .htaccess ํŒŒ์ผ ๊ถŒํ•œ ํ™•์ธ

3. ์ธ์ฆ ์‹คํŒจ
์ฆ์ƒ: 401 Unauthorized ์—๋Ÿฌ
ํ•ด๊ฒฐ: JWT ํ† ํฐ ๋งŒ๋ฃŒ ํ™•์ธ, ํ—ค๋”์— ํ† ํฐ ์ œ๋Œ€๋กœ ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

4. ๋А๋ฆฐ ์‘๋‹ต ์†๋„
์ฆ์ƒ: API ํ˜ธ์ถœ์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ
ํ•ด๊ฒฐ: ์บ์‹ฑ ๊ตฌํ˜„, ์ฟผ๋ฆฌ ์ตœ์ ํ™”, CDN ์‚ฌ์šฉ

5. ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ๋ฌธ์ œ
์ฆ์ƒ: ์ธ๋„ค์ผ์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ
ํ•ด๊ฒฐ: _embed ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ, ๋ฏธ๋””์–ด ๊ถŒํ•œ ํ™•์ธ
๐Ÿ’ก ๋””๋ฒ„๊น… ํŒ: ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Network ํƒญ์„ ์—ด์–ด๋‘๋ฉด API ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด. ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ์—ฌ๊ธฐ์„œ ๋จผ์ € ํ™•์ธํ•ด๋ด!

๐Ÿ“Š ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ถ„์„

API ์‘๋‹ต ์‹œ๊ฐ„ ์ธก์ • โฑ๏ธ

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ API ํ˜ธ์ถœ ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๋ฉด ์„ฑ๋Šฅ ๊ฐœ์„  ํฌ์ธํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด:

async function fetchWithTiming(url) {
    const startTime = performance.now();
    
    try {
        const response = await fetch(url);
        const data = await response.json();
        
        const endTime = performance.now();
        const duration = endTime - startTime;
        
        console.log(`API ํ˜ธ์ถœ ์‹œ๊ฐ„: ${duration.toFixed(2)}ms`);
        
        // ๋ถ„์„ ๋„๊ตฌ๋กœ ์ „์†ก
        if (window.gtag) {
            gtag('event', 'api_timing', {
                'event_category': 'API',
                'event_label': url,
                'value': Math.round(duration)
            });
        }
        
        return data;
    } catch (error) {
        console.error('API ํ˜ธ์ถœ ์‹คํŒจ:', error);
        throw error;
    }
}
์—๋Ÿฌ ๋กœ๊น… ๐Ÿ“

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์—๋Ÿฌ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๋กœ๊น…ํ•ด์•ผ ํ•ด:

// ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์œ ํ‹ธ๋ฆฌํ‹ฐ
class APIErrorHandler {
    static log(error, context) {
        const errorData = {
            message: error.message,
            stack: error.stack,
            context: context,
            timestamp: new Date().toISOString(),
            userAgent: navigator.userAgent
        };
        
        // Sentry, LogRocket ๋“ฑ์œผ๋กœ ์ „์†ก
        console.error('API Error:', errorData);
        
        // ์„œ๋ฒ„๋กœ ์—๋Ÿฌ ๋ฆฌํฌํŠธ ์ „์†ก
        fetch('https://yoursite.com/wp-json/custom/v1/error-log', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(errorData)
        });
    }
}

// ์‚ฌ์šฉ ์˜ˆ
try {
    const data = await fetchPosts();
} catch (error) {
    APIErrorHandler.log(error, { action: 'fetchPosts', page: 'home' });
}

๐ŸŒˆ ๋ฏธ๋ž˜ ์ง€ํ–ฅ์  ๊ธฐ๋Šฅ๋“ค

GraphQL ํ†ตํ•ฉ ๐Ÿ”ฎ

REST API๋„ ์ข‹์ง€๋งŒ, GraphQL์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์œ ์—ฐํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. WPGraphQL ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ•˜๋ฉด ์›Œ๋“œํ”„๋ ˆ์Šค์—์„œ GraphQL์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

// GraphQL ์ฟผ๋ฆฌ ์˜ˆ์ œ
query GetPosts {
  posts(first: 10) {
    nodes {
      id
      title
      excerpt
      featuredImage {
        node {
          sourceUrl
        }
      }
      author {
        node {
          name
        }
      }
    }
  }
}

GraphQL์˜ ์žฅ์ ์€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ •ํ™•ํžˆ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ. Over-fetching์ด๋‚˜ Under-fetching ๋ฌธ์ œ๊ฐ€ ์—†์–ด! ๐ŸŽฏ

์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (WebSocket) ๐Ÿ”„

๋Œ“๊ธ€์ด๋‚˜ ์ข‹์•„์š” ๊ฐ™์€ ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด WebSocket์„ ๊ณ ๋ คํ•ด๋ด:

// Socket.io ์˜ˆ์ œ
import io from 'socket.io-client';

const socket = io('https://yoursite.com');

socket.on('new_comment', (comment) => {
    // ์ƒˆ ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆฌ๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ
    setComments(prevComments => [...prevComments, comment]);
});

socket.on('post_updated', (postId) => {
    // ๊ฒŒ์‹œ๊ธ€์ด ์ˆ˜์ •๋˜๋ฉด ๋‹ค์‹œ ๋กœ๋“œ
    fetchPost(postId);
});
PWA (Progressive Web App) ๊ตฌํ˜„ ๐Ÿ“ฒ

ํ—ค๋“œ๋ฆฌ์Šค CMS๋กœ ๋งŒ๋“  ์‚ฌ์ดํŠธ๋ฅผ PWA๋กœ ๋งŒ๋“ค๋ฉด ์˜คํ”„๋ผ์ธ์—์„œ๋„ ์ž‘๋™ํ•˜๊ณ , ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด:

// service-worker.js
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // ์บ์‹œ์— ์žˆ์œผ๋ฉด ์บ์‹œ์—์„œ, ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ์—์„œ
        return response || fetch(event.request);
      })
  );
});

๐Ÿ’ก ์‹ค๋ฌด ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

โœจ ํ”„๋กœ์ฒ˜๋Ÿผ ๊ฐœ๋ฐœํ•˜๊ธฐ: 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
API URL์„ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•ด. ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ์‰ฝ๊ฒŒ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด.

2. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ๋„์ž…
API ์‘๋‹ต ํƒ€์ž…์„ ์ •์˜ํ•˜๋ฉด ๋ฒ„๊ทธ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ํฐ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ•„์ˆ˜์•ผ!

3. ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ ๊ตฌํ˜„
React์—์„œ๋Š” Error Boundary๋ฅผ ์‚ฌ์šฉํ•ด์„œ API ์—๋Ÿฌ๊ฐ€ ์ „์ฒด ์•ฑ์„ ๋ง๊ฐ€๋œจ๋ฆฌ์ง€ ์•Š๋„๋ก ํ•ด.

4. ๋กœ๋”ฉ ์ƒํƒœ ๊ด€๋ฆฌ
์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด ์Šค์ผˆ๋ ˆํ†ค ์Šคํฌ๋ฆฐ์ด๋‚˜ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๊ผญ ๊ตฌํ˜„ํ•ด.

5. SEO ์ตœ์ ํ™”
๋ฉ”ํƒ€ ํƒœ๊ทธ, Open Graph, ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žŠ์ง€ ๋ง๊ณ  ์ถ”๊ฐ€ํ•ด. SSR์ด๋‚˜ SSG๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ข‹์•„!

6. ๋ฒ„์ „ ๊ด€๋ฆฌ
API ๋ฒ„์ „์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ด. ๋‚˜์ค‘์— API๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด๋„ ๊ธฐ์กด ์•ฑ์ด ๊ณ„์† ์ž‘๋™ํ•˜๋„๋ก!

7. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
Jest, React Testing Library ๋“ฑ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ์™€ API ํ˜ธ์ถœ์„ ํ…Œ์ŠคํŠธํ•ด. ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ ์ž์‹ ๊ฐ์ด ์ƒ๊ฒจ!
๐ŸŽจ ๋””์ž์ธ ํŒ: ์›Œ๋“œํ”„๋ ˆ์Šค ๊ด€๋ฆฌ์ž ํ™”๋ฉด์€ ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ์‹ค์ œ ์‚ฌ์šฉ์ž๋Š” ๋ฉ‹์ง„ ํ”„๋ก ํŠธ์—”๋“œ๋งŒ ๋ณด๊ฒŒ ๋ผ. ๋””์ž์ธ์— ์ œ์•ฝ์ด ์—†์œผ๋‹ˆ ์ฐฝ์˜๋ ฅ์„ ๋งˆ์Œ๊ป ๋ฐœํœ˜ํ•ด๋ด!

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€์šฐ, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐ŸŽ‰ ์›Œ๋“œํ”„๋ ˆ์Šค REST API๋ฅผ ํ™œ์šฉํ•œ ํ—ค๋“œ๋ฆฌ์Šค CMS ๊ตฌํ˜„์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ์•Œ์•„๋ดค์–ด.

ํ—ค๋“œ๋ฆฌ์Šค CMS๋Š” ๋‹จ์ˆœํžˆ ํŠธ๋ Œ๋””ํ•œ ๊ธฐ์ˆ ์ด ์•„๋‹ˆ์•ผ. ์‹ค์ œ๋กœ ์œ ์—ฐ์„ฑ, ์„ฑ๋Šฅ, ํ™•์žฅ์„ฑ ๋ฉด์—์„œ ์—„์ฒญ๋‚œ ์žฅ์ ์„ ์ œ๊ณตํ•ด. ํŠนํžˆ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์— ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์›น ์„œ๋น„์Šค์—๋Š” ๊ฑฐ์˜ ํ•„์ˆ˜์ ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด.

๋ฌผ๋ก  ์ฒ˜์Œ์—๋Š” ๋Ÿฌ๋‹ ์ปค๋ธŒ๊ฐ€ ์žˆ์–ด. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ชจ๋‘ ๋‹ค๋ค„์•ผ ํ•˜๊ณ , API ํ†ต์‹ , ์ธ์ฆ, ์บ์‹ฑ ๋“ฑ ์‹ ๊ฒฝ ์จ์•ผ ํ•  ๊ฒŒ ๋งŽ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ ํ•œ ๋ฒˆ ์ต์ˆ™ํ•ด์ง€๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ! ๐Ÿ’ช

ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ์ •๋ฆฌํ•˜๋ฉด:

โœ… ์›Œ๋“œํ”„๋ ˆ์Šค๋Š” ํ›Œ๋ฅญํ•œ ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ ๋ฐฑ์—”๋“œ๋กœ ํ™œ์šฉ ๊ฐ€๋Šฅ
โœ… REST API๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
โœ… React, Vue, Next.js ๋“ฑ ์ตœ์‹  ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ์ˆ  ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉ
โœ… ๋ชจ๋ฐ”์ผ ์•ฑ, ์›น, IoT ๋“ฑ ๋‹ค์ค‘ ํ”Œ๋žซํผ ์ง€์›
โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๋ณด์•ˆ์— ์‹ ๊ฒฝ ์จ์•ผ ํ•จ
โœ… ์บ์‹ฑ, ์ธ์ฆ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ํ•„์ˆ˜

์ด์ œ ์ง์ ‘ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ด๋ด! ์ž‘์€ ๋ธ”๋กœ๊ทธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๊ฐ€๋ฉด์„œ ๊ฒฝํ—˜์„ ์Œ“๋Š” ๊ฒŒ ์ตœ๊ณ ์˜ ํ•™์Šต ๋ฐฉ๋ฒ•์ด์•ผ. ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ํ•„์š”ํ•˜๋ฉด ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„๋ด.

ํ—ค๋“œ๋ฆฌ์Šค CMS์˜ ์„ธ๊ณ„๋Š” ์ •๋ง ๋„“๊ณ  ์žฌ๋ฏธ์žˆ์–ด. ๊ณ„์† ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๊ณผ ํŒจํ„ด๋“ค์ด ๋‚˜์˜ค๊ณ  ์žˆ์œผ๋‹ˆ, ํ•ญ์ƒ ๋ฐฐ์šฐ๋Š” ์ž์„ธ๋กœ ์ž„ํ•˜๋ฉด ์ข‹์„ ๊ฑฐ์•ผ! ๐Ÿš€

์—ฌ๋Ÿฌ๋ถ„์˜ ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ์‘์›ํ• ๊ฒŒ! ํ™”์ดํŒ…! ๐Ÿ’–

๐ŸŽฏ Success! ๐Ÿ“ฑ Mobile ๐Ÿ’ป Web โšก Fast ๐Ÿ”’ Secure

๐ŸŒŸ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ

ํ—ค๋“œ๋ฆฌ์Šค CMS ๊ฐœ๋ฐœ์ด ์ฒ˜์Œ์ด๋ผ๋ฉด ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋งˆ์„ธ์š”!
์ „๋ฌธ๊ฐ€์˜ ๋ฉ˜ํ† ๋ง์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ํ”„๋กœ์ ํŠธ ๋„์›€์ด ํ•„์š”ํ•˜๋‹ค๋ฉด
์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋“ค์„ ๋งŒ๋‚˜๋ณด์„ธ์š”. ๐Ÿ’ผ

์—ฌ๋Ÿฌ๋ถ„์˜ ์„ฑ๊ณต์ ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ์‘์›ํ•ฉ๋‹ˆ๋‹ค! ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.