์˜ค๋Š˜ HTTP ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ์งˆ๋ฌธ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๋‹ต๋ณ€์„ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•˜๋‹ค๋ณด๋‹ˆ ์ œ๊ฐ€ GET, POST, DELETE, PUT, PATCH ๊ฐ™์€ ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์„œ๋“œ ์™ธ์—๋Š” ์ž˜ ๋ชจ๋ฅธ๋‹ค๋Š”๊ฑธ ์•Œ๊ฒŒ๋์–ด์š”๐Ÿฅฒ ํŠนํžˆ HEAD๋‚˜ OPTIONS ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋“ค์€ ์กด์žฌ์˜ ์œ ๋ฌด๋Š” ์•Œ์•˜์œผ๋‚˜, ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ธ์ง€ํ•˜์ง€๋„ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ์‹ ์ž… ์‹œ์ ˆ์˜ ๊ธฐ์–ต๋„ ๋˜์‚ด๋ฆฌ๊ณ , ๋ถ€์กฑํ–ˆ๋˜ ๋ถ€๋ถ„๋„ ์ฑ„์›Œ๋ณด๊ณ ์ž HTTP ๋ฉ”์„œ๋“œ์™€ ๊ทธ ์‚ฌ์šฉ๋ฒ•์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•ด์š”.

ํŠนํžˆ React์™€ Next.js๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๋Š” ๋ถ„๋“ค์„ ์œ„ํ•ด ์‹ค์ œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

HTTP ๋ฉ”์„œ๋“œ Next.js ๋Š” Pages Router๋ฐฉ์‹์ด ๋” ์ต์ˆ™ํ•˜์‹  ๋ถ„๋“ค์„ ์œ„ํ•ด API Routes handler ๋ฌธ๋ฒ•(pages/api/ ๋””๋ ‰ํ† ๋ฆฌ ์‚ฌ์šฉ)์ธ Next.js 12 ์ด์ „์˜ ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

App Router ๋ฐฉ์‹๋งŒ์„ ์•Œ๊ณ  ๊ณ„์‹  ๋ถ„๋“ค์€, export async function GET() ์ฒ˜๋Ÿผ HTTP ๋ฉ”์„œ๋“œ๋ฅผ ํ•จ์ˆ˜ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ƒˆ๋กœ์šด convention ๋ฐฉ์‹ ๋“ฑ์„ ํฌํ•จํ•œ ์—ฌ๋Ÿฌ ์ฐจ์ด์ ์ด ์žˆ์ง€๋งŒ ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•˜๋ฉฐ ์ง„ํ–‰ํ•˜์‹ ๋‹ค๋ฉด ๋ฌธ์ œ์—†์ด ์ดํ•ดํ•˜์‹ค ์ˆ˜ ์žˆ์„ ๊ฑฐ๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

 

 

HTTP Method

HTTP ๋ฉ”์„œ๋“œ๋ž€?

HTTP ๋ฉ”์„œ๋“œ๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ํ†ต์‹ ์—์„œ ์–ด๋–ค ๋™์ž‘์„ ์ˆ˜ํ–‰ํ• ์ง€ ์ง€์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. REST API์—์„œ ๋งค์šฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•˜๋ฉฐ, ๊ฐ ๋ฉ”์„œ๋“œ๋Š” ํŠน์ •ํ•œ ๋ชฉ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์›น ๊ฐœ๋ฐœ์˜ ๊ธฐ์ดˆ๊ฐ€ ๋˜์ฃ .

 

์ฃผ์š” HTTP ๋ฉ”์„œ๋“œ ์‚ดํŽด๋ณด๊ธฐ

1. GET ๋ฉ”์„œ๋“œ

GET์€ ๋ฆฌ์†Œ์Šค๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

 

React์—์„œ์˜ ์‚ฌ์šฉ ์˜ˆ์‹œ:

// React ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ
const UserProfile = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch('/api/users/1');
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      }
    };

    fetchUser();
  }, []);

  return (
    <div>
      {user && <h1>{user.name}</h1>}
    </div>
  );
};

 

Next.js API Route์—์„œ์˜ ์ฒ˜๋ฆฌ:

// pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'GET') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const { id } = req.query;
  // DB ์กฐํšŒ ๋กœ์ง
  const user = await prisma.user.findUnique({
    where: { id: Number(id) }
  });

  res.status(200).json(user);
}

 

2. POST ๋ฉ”์„œ๋“œ

POST๋Š” ์ƒˆ๋กœ์šด ๋ฆฌ์†Œ์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

 

React์—์„œ์˜ ์‚ฌ์šฉ ์˜ˆ์‹œ:

// ์ƒˆ ์‚ฌ์šฉ์ž ๋“ฑ๋ก ํผ
const RegisterForm = () => {
  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: formData.get('name'),
          email: formData.get('email'),
        }),
      });

      if (!response.ok) throw new Error('Registration failed');
      
      const data = await response.json();
      // ์„ฑ๊ณต ์ฒ˜๋ฆฌ
    } catch (error) {
      // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" type="text" required />
      <input name="email" type="email" required />
      <button type="submit">๋“ฑ๋ก</button>
    </form>
  );
};

 

3. PUT๊ณผ PATCH ๋ฉ”์„œ๋“œ

// pages/api/users/[id].ts
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query;

  if (req.method === 'PUT') {
    // ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ต์ฒด
    const userData = req.body;
    await prisma.user.update({
      where: { id: Number(id) },
      data: userData,
    });
    return res.status(200).json({ message: 'User updated' });
  }

  if (req.method === 'PATCH') {
    // ๋ถ€๋ถ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ •
    const updates = req.body;
    await prisma.user.update({
      where: { id: Number(id) },
      data: updates,
    });
    return res.status(200).json({ message: 'User partially updated' });
  }
}

 

4. DELETE ๋ฉ”์„œ๋“œ

React์—์„œ์˜ ์‚ฌ์šฉ ์˜ˆ์‹œ:

const DeleteUserButton = ({ userId }) => {
  const handleDelete = async () => {
    try {
      const response = await fetch(`/api/users/${userId}`, {
        method: 'DELETE',
      });

      if (!response.ok) throw new Error('Delete failed');
      // ์„ฑ๊ณต ์ฒ˜๋ฆฌ
    } catch (error) {
      // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    }
  };

  return (
    <button onClick={handleDelete}>
      ์‚ฌ์šฉ์ž ์‚ญ์ œ
    </button>
  );
};

 

React์™€ Next.js์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋ฉ”์„œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์™€ ํšจ๊ณผ์ ์œผ๋กœ ํ†ต์‹ ํ•˜๊ณ , RESTful API๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ž์ฃผ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ ์•Œ์•„๋‘๋ฉด ์œ ์šฉํ•œ HTTP ๋ฉ”์„œ๋“œ๋“ค

1. HEAD ๋ฉ”์„œ๋“œ

HEAD ๋ฉ”์„œ๋“œ๋Š” GET๊ณผ ๋™์ผํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€๋งŒ, ์‘๋‹ต์œผ๋กœ ํ—ค๋”๋งŒ ๋ฐ›๊ณ  ๋ณธ๋ฌธ์€ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ํฐ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜๊ธฐ ์ „์— ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ
  • ๋ฆฌ์†Œ์Šค๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์‚ฌ(Last-Modified ํ™•์ธ)
  • ๋ฆฌ์†Œ์Šค ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
// Next.js์—์„œ HEAD ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'HEAD') {
    // ํŒŒ์ผ ์ •๋ณด๋งŒ ํ™•์ธ
    const fileStats = await getFileStats('large-file.pdf');
    
    res.setHeader('Content-Length', fileStats.size);
    res.setHeader('Last-Modified', fileStats.mtime.toUTCString());
    return res.status(200).end();
  }
}

// React์—์„œ HEAD ์š”์ฒญ ์˜ˆ์‹œ
const checkFileDetails = async () => {
  try {
    const response = await fetch('/api/files/large-file.pdf', {
      method: 'HEAD'
    });
    
    const fileSize = response.headers.get('Content-Length');
    const lastModified = response.headers.get('Last-Modified');
    
    console.log(`File size: ${fileSize} bytes`);
    console.log(`Last modified: ${lastModified}`);
  } catch (error) {
    console.error('Error checking file:', error);
  }
};

 

1. OPTIONS ๋ฉ”์„œ๋“œ์™€ CORS

OPTIONS๋Š” ์„œ๋ฒ„๊ฐ€ ์ง€์›ํ•˜๋Š” ๋ฉ”์„œ๋“œ์™€ ๊ธฐ๋Šฅ์„ ํ™•์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. 

์ฃผ๋กœ CORS์˜ Preflight ์š”์ฒญ์—์„œ ํ•ต์‹ฌ์ ์ธ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

 

์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€:

  • CORS ์‚ฌ์ „ ๊ฒ€์‚ฌ
  • ์„œ๋ฒ„ ์ง€์› ๊ธฐ๋Šฅ ํ™•์ธ
  • API ์—”๋“œํฌ์ธํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ

 

Next.js์—์„œ CORS ์„ค์ • ์˜ˆ์‹œ:

// pages/api/users/[id].ts
import Cors from 'cors';

// CORS ๋ฏธ๋“ค์›จ์–ด ์ดˆ๊ธฐํ™”
const cors = Cors({
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  origin: ['https://allowed-origin.com'],
  credentials: true,
});

// ๋ฏธ๋“ค์›จ์–ด ์‹คํ–‰ ํ•จ์ˆ˜
const runMiddleware = (req: NextApiRequest, res: NextApiResponse, fn: Function) => {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // CORS ๋ฏธ๋“ค์›จ์–ด ์‹คํ–‰
  await runMiddleware(req, res, cors);

  // OPTIONS ์š”์ฒญ ์ฒ˜๋ฆฌ
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }

  // ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ...
}

 

๋ธŒ๋ผ์šฐ์ €์˜ Preflight ์š”์ฒญ ์˜ˆ์‹œ:

// React์—์„œ CORS ๊ด€๋ จ OPTIONS ์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋Š” ์ƒํ™ฉ
const fetchWithCustomHeaders = async () => {
  try {
    const response = await fetch('https://api.example.com/data', {
      method: 'GET',
      headers: {
        'Custom-Header': 'value',  // ์ปค์Šคํ…€ ํ—ค๋” ์ถ”๊ฐ€์‹œ ์ž๋™์œผ๋กœ preflight ์š”์ฒญ ๋ฐœ์ƒ
      },
      credentials: 'include',  // ์ธ์ฆ ์ •๋ณด ํฌํ•จ
    });
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
  }
};

 

CORS์™€ Preflight ์š”์ฒญ์˜ ์ดํ•ด

๋ธŒ๋ผ์šฐ์ €๋Š” ํŠน์ • ์กฐ๊ฑด์—์„œ ์‹ค์ œ ์š”์ฒญ ์ „์— OPTIONS ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ Preflight ์š”์ฒญ์„ ์ž๋™์œผ๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

 

Preflight ์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ:

  1. ์ปค์Šคํ…€ ํ—ค๋”๋ฅผ ํฌํ•จํ•  ๋•Œ (์˜ˆ: Authorization)
  2. ๋‹จ์ˆœํ•˜์ง€ ์•Š์€ Content-Type์„ ์‚ฌ์šฉํ•  ๋•Œ
  3. ๋‹จ์ˆœํ•˜์ง€ ์•Š์€ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ (GET, HEAD, POST ์™ธ)
// Preflight ์š”์ฒญ ์˜ˆ์‹œ (๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ๋ณด์ด๋Š” ํ˜•ํƒœ)
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
Origin: https://your-app.com

// ์„œ๋ฒ„ ์‘๋‹ต ์˜ˆ์‹œ
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://your-app.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400

 

3. CONNECT ๋ฉ”์„œ๋“œ

CONNECT๋Š” ํ”„๋ก์‹œ ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด SSL/TLS ํ„ฐ๋„์„ ๊ตฌ์„ฑํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€:

  • HTTPS ํ”„๋ก์‹œ ์—ฐ๊ฒฐ ์„ค์ •
  • ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์—…๊ทธ๋ ˆ์ด๋“œ
  • ํ”„๋ก์‹œ ์„œ๋ฒ„๋ฅผ ํ†ตํ•œ ์•”ํ˜ธํ™”๋œ ํ†ต์‹ 
// Node.js์—์„œ CONNECT ์š”์ฒญ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
const net = require('net');
const server = require('http').createServer();

server.on('connect', (req, clientSocket, head) => {
  // ํ”„๋ก์‹œ ์—ฐ๊ฒฐ ์„ค์ •
  const { port, hostname } = new URL(`http://${req.url}`);
  const serverSocket = net.connect(port || 80, hostname, () => {
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    serverSocket.write(head);
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });
});

 

4. TRACE ๋ฉ”์„œ๋“œ

TRACE๋Š” ์š”์ฒญ ๋ฉ”์‹œ์ง€๊ฐ€ ํ”„๋ก์‹œ๋‚˜ ๋ฐฉํ™”๋ฒฝ์„ ํ†ต๊ณผํ•˜๋ฉด์„œ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝ๋˜๋Š”์ง€ ์ถ”์ ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ๋””๋ฒ„๊น…
  • ์š”์ฒญ ๋ฉ”์‹œ์ง€ ๋ณ€์กฐ ๊ฐ์ง€
  • ํ”„๋ก์‹œ ์ฒด์ธ ์ถ”์ 
// Next.js์—์„œ TRACE ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'TRACE') {
    // ์š”์ฒญ ํ—ค๋”์™€ ๊ฒฝ๋กœ ์ •๋ณด๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
    const responseBody = {
      method: req.method,
      url: req.url,
      headers: req.headers,
      path: req.url
    };
    
    res.setHeader('Content-Type', 'message/http');
    return res.status(200).json(responseBody);
  }
}

// ๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋ฒ„๋Š” TRACE ๋ฉ”์„œ๋“œ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค

 

๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

// Next.js์—์„œ ๋ฉ”์„œ๋“œ ์ œํ•œ ๊ตฌํ˜„ ์˜ˆ์‹œ
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ ์ฐจ๋‹จ
  const allowedMethods = ['GET', 'POST', 'OPTIONS'];
  if (!allowedMethods.includes(req.method)) {
    return res.status(405).json({ 
      error: 'Method Not Allowed',
      allowedMethods 
    });
  }

  // TRACE ๋ฉ”์„œ๋“œ๋Š” ๋ช…์‹œ์ ์œผ๋กœ ์ฐจ๋‹จ
  if (req.method === 'TRACE') {
    return res.status(403).json({ 
      error: 'TRACE method is disabled for security reasons' 
    });
  }
}
 

์ด๋Ÿฌํ•œ HTTP ๋ฉ”์„œ๋“œ๋“ค์€ ํŠน์ˆ˜ํ•œ ์ƒํ™ฉ์—์„œ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ณด์•ˆ์„ ๊ณ ๋ คํ•ด ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํŠนํžˆ TRACE ๋ฉ”์„œ๋“œ๋Š” ๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ฉฑ๋“ฑ์„ฑ

HTTP ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ๋งˆ์ง€๋ง‰ ์„ค๋ช…์œผ๋กœ ์ค‘์š”ํ•œ ๊ฐœ๋…์ธ ๋ฉฑ๋“ฑ์„ฑ์— ๋Œ€ํ•ด ๊ฐ„๋‹จํžˆ ์„ค๋ช…๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ฉฑ๋“ฑ์„ฑ(Idempotency)์ด๋ž€ ๋™์ผํ•œ ์š”์ฒญ์„ ํ•œ ๋ฒˆ ๋ณด๋‚ด๋Š” ๊ฒƒ๊ณผ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ณด๋‚ด๋Š” ๊ฒƒ์ด ์„œ๋ฒ„์˜ ์ƒํƒœ์— ๋™์ผํ•œ ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

์ฃผ์š” HTTP ๋ฉ”์„œ๋“œ์˜ ๋ฉฑ๋“ฑ์„ฑ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

๋ฉฑ๋“ฑ์„ฑ์„ ๊ฐ€์ง„ ๋ฉ”์„œ๋“œ:

  1. GET: ๋ฆฌ์†Œ์Šค๋ฅผ ์กฐํšŒ๋งŒ ํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ
  2. HEAD: GET๊ณผ ๋™์ผํ•˜๊ฒŒ ๋ฆฌ์†Œ์Šค ์ •๋ณด๋งŒ ์กฐํšŒํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ
  3. PUT: ๋ฆฌ์†Œ์Šค๋ฅผ ๋Œ€์ฒด(๋ฎ์–ด์“ฐ๊ธฐ)ํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ
  4. DELETE: ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ญ์ œํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ (์ด๋ฏธ ์‚ญ์ œ๋œ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‹ค์‹œ ์‚ญ์ œํ•ด๋„ ์ƒํƒœ๋Š” ๋™์ผ)
  5. OPTIONS: ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค๊ฐ€ ์ง€์›ํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ
  6. TRACE: ์š”์ฒญ ๋ฉ”์‹œ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑ

๋ฉฑ๋“ฑ์„ฑ์ด ์—†๋Š” ๋ฉ”์„œ๋“œ:

  1. POST: ์ƒˆ๋กœ์šด ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ ๋˜๋Š” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์š”์ฒญํ•˜๋ฏ€๋กœ ๋ฉฑ๋“ฑํ•˜์ง€ ์•Š์Œ
    • ์˜ˆ: ๊ฐ™์€ ์ฃผ๋ฌธ์„ ์—ฌ๋Ÿฌ ๋ฒˆ POSTํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ฃผ๋ฌธ์ด ์ƒ์„ฑ๋จ
  2. PATCH: ๋ฆฌ์†Œ์Šค์˜ ๋ถ€๋ถ„ ์ˆ˜์ •์ด๋ฏ€๋กœ ๋ฉฑ๋“ฑํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
    • ์˜ˆ: ์ˆซ์ž๋ฅผ 1์”ฉ ์ฆ๊ฐ€์‹œํ‚ค๋Š” PATCH ์š”์ฒญ์€ ๋ฉฑ๋“ฑํ•˜์ง€ ์•Š์Œ

๋ฉฑ๋“ฑ์„ฑ์ด ์ค‘์š”ํ•œ ์ด์œ :

  1. ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ ์žฌ์‹œ๋„ ์ „๋žต ์ˆ˜๋ฆฝ ๊ฐ€๋Šฅ
  2. ์„œ๋ฒ„์˜ ์•ˆ์ •์„ฑ๊ณผ ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ ๋ณด์žฅ
  3. ์บ์‹ฑ ์ „๋žต ์ˆ˜๋ฆฝ์— ๋„์›€

API๋ฅผ ์„ค๊ณ„ํ•  ๋•Œ๋Š” ๊ฐ ๋ฉ”์„œ๋“œ์˜ ๋ฉฑ๋“ฑ์„ฑ ํŠน์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ์ ์ ˆํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

 

 

GraphQL

๋ฒˆ์™ธ: REST API vs GraphQL 

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

 

GraphQL์ด๋ž€?

GraphQL์€ Facebook์—์„œ ๊ฐœ๋ฐœํ•œ API๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ ์–ธ์–ด์ž…๋‹ˆ๋‹ค. REST API์™€๋Š” ๋‹ฌ๋ฆฌ, GraphQL์€ ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ์—์„œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ •ํ™•ํžˆ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

REST API vs GraphQL ๋น„๊ต

// REST API ๋ฐฉ์‹
// GET /api/users/1
// GET /api/users/1/posts
// GET /api/users/1/followers

// GraphQL ๋ฐฉ์‹
const query = `
  query {
    user(id: 1) {
      name
      email
      posts {
        title
        content
      }
      followers {
        name
      }
    }
  }
`;

 

Next.js์—์„œ GraphQL ์‚ฌ์šฉ ์˜ˆ์‹œ

// pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
});

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  await apolloServer.start();
  await apolloServer.createHandler({
    path: '/api/graphql',
  })(req, res);
}

 

React์—์„œ GraphQL ์‚ฌ์šฉ ์˜ˆ์‹œ (Apollo Client)

import { useQuery } from '@apollo/client';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      posts {
        title
      }
    }
  }
`;

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
      <h2>Posts</h2>
      <ul>
        {data.user.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

 

REST API vs GraphQL: ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ 

REST API

๐Ÿ‘ ์žฅ์ :

  • HTTP ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ™œ์šฉ
  • ์ง๊ด€์ ์ธ ๋ฆฌ์†Œ์Šค ๊ตฌ์กฐ
  • ๋” ๋„๋ฆฌ ์‚ฌ์šฉ๋˜์–ด ํ•™์Šต ์ž๋ฃŒ๊ฐ€ ํ’๋ถ€
  • ๋‹จ์ˆœํ•œ API์—์„œ๋Š” ๊ตฌํ˜„์ด ๋” ๊ฐ„๋‹จ

๐Ÿ‘Ž ๋‹จ์ :

  • Over-fetching/Under-fetching ๋ฌธ์ œ
  • ์—ฌ๋Ÿฌ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์š”์ฒญ ํ•„์š”
  • ์—”๋“œํฌ์ธํŠธ ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์Œ

 

GraphQL

๐Ÿ‘ ์žฅ์ :

  • ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ •ํ™•ํžˆ ์š”์ฒญ ๊ฐ€๋Šฅ
  • ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฆฌ์†Œ์Šค ์กฐํšŒ ๊ฐ€๋Šฅ
  • ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ
  • ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ์ง€์› (Subscriptions)

๐Ÿ‘Ž ๋‹จ์ :

  • ํ•™์Šต ๊ณก์„ ์ด ์ƒ๋Œ€์ ์œผ๋กœ ๊ฐ€ํŒŒ๋ฆ„
  • ์บ์‹ฑ ๊ตฌํ˜„์ด ๋” ๋ณต์žก
  • ๋‹จ์ˆœํ•œ API์—์„œ๋Š” ์˜ค๋ฒ„ ์—”์ง€๋‹ˆ์–ด๋ง์ด ๋  ์ˆ˜ ์žˆ์Œ
  • ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ๊ฐ€ ๋ณต์žก

 

์‹ค์ œ ์‚ฌ์šฉ ํŒ

// GraphQL๊ณผ REST API๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ
// pages/api/hybrid/[...path].ts
export default async function handler(req, res) {
  // ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” REST API๋กœ ์ฒ˜๋ฆฌ
  if (req.url.startsWith('/api/upload')) {
    return handleFileUpload(req, res);
  }

  // ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ๋Š” GraphQL๋กœ ์ฒ˜๋ฆฌ
  if (req.url.startsWith('/api/graphql')) {
    return handleGraphQLRequest(req, res);
  }

  // ๋‹จ์ˆœํ•œ CRUD๋Š” REST API๋กœ ์ฒ˜๋ฆฌ
  return handleRESTRequest(req, res);
}

 

์–ธ์ œ ๋ฌด์—‡์„ ์„ ํƒํ• ๊นŒ?

 

  • REST API๊ฐ€ ์ข‹์€ ๊ฒฝ์šฐ:
    • ๋‹จ์ˆœํ•œ CRUD ์ž‘์—…์ด ์ฃผ๋ฅผ ์ด๋ฃจ๋Š” ๊ฒฝ์šฐ
    • ์บ์‹ฑ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
    • ํŒŒ์ผ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ
    • ํŒ€์ด REST์— ๋” ์ต์ˆ™ํ•œ ๊ฒฝ์šฐ
  • GraphQL์ด ์ข‹์€ ๊ฒฝ์šฐ:
    • ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒฝ์šฐ
    • ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
    • ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
    • API ๋ณ€๊ฒฝ์ด ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ

 

์‹ค์ œ๋กœ ๊ธฐ์—…๋“ค์—์„œ ๋‘ ๋ฐฉ์‹์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ์„ ํƒํ•˜๊ณ  ์žˆ๋Š”๊ฑธ๋กœ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. GitHub์ด ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์‹œ์ธ๋ฐ์š”, ์š”๊ตฌ ์‚ฌํ•ญ์— ๊ฐ€์žฅ ์ž˜ ๋ถ€ํ•ฉํ•˜๊ณ  ๊ฐ€์žฅ ํŽธ์•ˆํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” API๋ฅผ ์‚ฌ์šฉํ•˜๋ผ ๊ถŒ๊ณ ํ•˜๊ณ  ์žˆ๊ณ , ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฐ™์€ ๊ธฐ๋Šฅ์€ REST API๋กœ, ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ๋Š” GraphQL๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

+ Recent posts