์ค๋ 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 ๋ฉ์๋๋?
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 ์์ฒญ์ด ๋ฐ์ํ๋ ๊ฒฝ์ฐ:
- ์ปค์คํ ํค๋๋ฅผ ํฌํจํ ๋ (์: Authorization)
- ๋จ์ํ์ง ์์ Content-Type์ ์ฌ์ฉํ ๋
- ๋จ์ํ์ง ์์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๋ (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 ๋ฉ์๋์ ๋ฉฑ๋ฑ์ฑ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
๋ฉฑ๋ฑ์ฑ์ ๊ฐ์ง ๋ฉ์๋:
- GET: ๋ฆฌ์์ค๋ฅผ ์กฐํ๋ง ํ๋ฏ๋ก ๋ฉฑ๋ฑ
- HEAD: GET๊ณผ ๋์ผํ๊ฒ ๋ฆฌ์์ค ์ ๋ณด๋ง ์กฐํํ๋ฏ๋ก ๋ฉฑ๋ฑ
- PUT: ๋ฆฌ์์ค๋ฅผ ๋์ฒด(๋ฎ์ด์ฐ๊ธฐ)ํ๋ฏ๋ก ๋ฉฑ๋ฑ
- DELETE: ๋ฆฌ์์ค๋ฅผ ์ญ์ ํ๋ฏ๋ก ๋ฉฑ๋ฑ (์ด๋ฏธ ์ญ์ ๋ ๋ฆฌ์์ค๋ฅผ ๋ค์ ์ญ์ ํด๋ ์ํ๋ ๋์ผ)
- OPTIONS: ์๋ฒ ๋ฆฌ์์ค๊ฐ ์ง์ํ๋ ๋ฉ์๋ ์ ๋ณด๋ฅผ ์กฐํํ๋ฏ๋ก ๋ฉฑ๋ฑ
- TRACE: ์์ฒญ ๋ฉ์์ง๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๋ฏ๋ก ๋ฉฑ๋ฑ
๋ฉฑ๋ฑ์ฑ์ด ์๋ ๋ฉ์๋:
- POST: ์๋ก์ด ๋ฆฌ์์ค ์์ฑ ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์์ฒญํ๋ฏ๋ก ๋ฉฑ๋ฑํ์ง ์์
- ์: ๊ฐ์ ์ฃผ๋ฌธ์ ์ฌ๋ฌ ๋ฒ POSTํ๋ฉด ์ฌ๋ฌ ๊ฐ์ ์ฃผ๋ฌธ์ด ์์ฑ๋จ
- PATCH: ๋ฆฌ์์ค์ ๋ถ๋ถ ์์ ์ด๋ฏ๋ก ๋ฉฑ๋ฑํ์ง ์์ ์ ์์
- ์: ์ซ์๋ฅผ 1์ฉ ์ฆ๊ฐ์ํค๋ PATCH ์์ฒญ์ ๋ฉฑ๋ฑํ์ง ์์
๋ฉฑ๋ฑ์ฑ์ด ์ค์ํ ์ด์ :
- ๋คํธ์ํฌ ์ค๋ฅ ์ ์ฌ์๋ ์ ๋ต ์๋ฆฝ ๊ฐ๋ฅ
- ์๋ฒ์ ์์ ์ฑ๊ณผ ์์ธก ๊ฐ๋ฅ์ฑ ๋ณด์ฅ
- ์บ์ฑ ์ ๋ต ์๋ฆฝ์ ๋์
API๋ฅผ ์ค๊ณํ ๋๋ ๊ฐ ๋ฉ์๋์ ๋ฉฑ๋ฑ์ฑ ํน์ฑ์ ๊ณ ๋ คํ์ฌ ์ ์ ํ ๋ฉ์๋๋ฅผ ์ ํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
๋ฒ์ธ: 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๋ก ์ฒ๋ฆฌํ๊ณ ์์ต๋๋ค.