Ver3.0 ๐ JavaScript ํ๋ก์ ์๋ฒ๋ก CORS ์ด์ ์๋ฒฝ ํด๊ฒฐํ๊ธฐ

๐ JavaScript ํ๋ก์ ์๋ฒ๋ก CORS ์ด์ ์๋ฒฝ ํด๊ฒฐํ๊ธฐ
์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ CORS ๋ฌธ์ ์ ํ๋ก์ ์๋ฒ ํ์ฉ๋ฒ
๐ฏ ์ CORS๋ ์ฐ๋ฆฌ๋ฅผ ๊ดด๋กญํ๊น?
์๋
! ํน์ ์น ๊ฐ๋ฐํ๋ค๊ฐ ์ฝ์์ฐฝ์ ๋นจ๊ฐ ๊ธ์จ๋ก "Access to fetch at... has been blocked by CORS policy"๋ผ๋ ๋ฌด์๋ฌด์ํ ์๋ฌ ๋ฉ์์ง ๋ณธ ์ ์์ง? ๐ฑ ์ฒ์ ๋ณด๋ ์๊ฐ ์ ๋ง ๋นํฉ์ค๋ฝ์ง. ๋ถ๋ช
์ฝ๋๋ ์ ๋๋ก ์งฐ๋๋ฐ, API ํธ์ถ๋ง ํ๋ฉด ๋งํ๋ ์ด ์ํฉ!
์ฌ์ค CORS(Cross-Origin Resource Sharing) ๋ฌธ์ ๋ ์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋๊ตฌ๋ ํ ๋ฒ์ฏค ๋ง์ฃผ์น๋ ํต๊ณผ์๋ก ๊ฐ์ ๊ฑฐ์ผ. ํนํ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ์ธ๋ถ API๋ฅผ ํธ์ถํ ์ผ์ด ์ ๋ง ๋ง์๋ฐ, ์ด๋๋ง๋ค CORS ์๋ฌ๊ฐ ํ์ด๋์์ ๊ฐ๋ฐ ์งํ์ ๋ง์๋ฒ๋ฆฌ๊ฑฐ๋ . ๐ค
์ค๋์ ์ด ๊ณจ์น ์ํ CORS ๋ฌธ์ ๋ฅผ JavaScript ํ๋ก์ ์๋ฒ๋ก ๊น๋ํ๊ฒ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์๋ ค์ค๊ฒ! ์ฌ๋ฅ๋ท์์ ์น ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ๋ฐฐ์ฐ๋ ค๋ ๋ถ๋ค์๊ฒ๋ ์ ๋ง ์ ์ฉํ ๋ด์ฉ์ด ๋ ๊ฑฐ์ผ. ๐ช
๐ค CORS๊ฐ ์ ํํ ๋ญ๊น?
CORS๋ฅผ ์ดํดํ๋ ค๋ฉด ๋จผ์ ๋์ผ ์ถ์ฒ ์ ์ฑ
(Same-Origin Policy)์ ์์์ผ ํด. ์ด๊ฑด ์น ๋ธ๋ผ์ฐ์ ์ ๋ณด์ ๋ฉ์ปค๋์ฆ์ธ๋ฐ, ์ฝ๊ฒ ๋งํ๋ฉด "๋ค๋ฅธ ์ถ์ฒ์ ๋ฆฌ์์ค๋ ํจ๋ถ๋ก ๊ฐ์ ธ์ค์ง ๋ง!"๋ผ๋ ๊ท์น์ด์ผ. ๐ก๏ธ
์ฌ๊ธฐ์ '์ถ์ฒ(Origin)'๋ ํ๋กํ ์ฝ, ๋๋ฉ์ธ, ํฌํธ ๋ฒํธ์ ์กฐํฉ์ ๋งํด. ์๋ฅผ ๋ค์ด๋ณผ๊ฒ:
https://www.example.com/page1
https://www.example.com/page2
โ ๋ค๋ฅธ ์ถ์ฒ:
http://www.example.com (ํ๋กํ ์ฝ ๋ค๋ฆ)
https://api.example.com (์๋ธ๋๋ฉ์ธ ๋ค๋ฆ)
https://www.example.com:8080 (ํฌํธ ๋ค๋ฆ)
๋ธ๋ผ์ฐ์ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค๋ฅธ ์ถ์ฒ๋ก์ ์์ฒญ์ ์ฐจ๋จํด. ์๋ํ๋ฉด ์
์์ ์ธ ์น์ฌ์ดํธ๊ฐ ์ฌ์ฉ์ ๋ชฐ๋ ๋ค๋ฅธ ์ฌ์ดํธ์ ๋ฐ์ดํฐ๋ฅผ ํ์ณ๊ฐ ์ ์๊ฑฐ๋ . ์ด๊ฒ ๋ฐ๋ก ๋ณด์์ ์ํ ์กฐ์น์ผ! ๐
ํ์ง๋ง ํ๋ ์น ๊ฐ๋ฐ์์๋ ๋ค๋ฅธ ์ถ์ฒ์ API๋ฅผ ํธ์ถํ๋ ๊ฒ ๋๋ฌด๋ ๋น์ฐํ ์ผ์ด์์? ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ CORS๋ผ๋ ๋ฉ์ปค๋์ฆ์ด์ผ. ์๋ฒ๊ฐ "์ด ์ถ์ฒ์์ ์ค๋ ์์ฒญ์ ๊ด์ฐฎ์!"๋ผ๊ณ ๋ช
์์ ์ผ๋ก ํ๋ฝํด์ฃผ๋ ๋ฐฉ์์ด์ง.
๐ก CORS์ ํต์ฌ ๊ฐ๋
์๋ฒ๊ฐ ์๋ต ํค๋์ ํน์ ์ ๋ณด๋ฅผ ํฌํจ์์ผ์ ๋ธ๋ผ์ฐ์ ์๊ฒ "์ด ์์ฒญ์ ํ์ฉํ ๊ฒ"๋ผ๊ณ ์๋ ค์ฃผ๋ ๊ฑฐ์ผ. ๋ํ์ ์ธ ํค๋๊ฐ ๋ฐ๋ก:
Access-Control-Allow-Origin: ์ด๋ค ์ถ์ฒ์ ์์ฒญ์ ํ์ฉํ ์ง
Access-Control-Allow-Methods: ์ด๋ค HTTP ๋ฉ์๋๋ฅผ ํ์ฉํ ์ง
Access-Control-Allow-Headers: ์ด๋ค ํค๋๋ฅผ ํ์ฉํ ์ง
๐ซ CORS ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์ค์ ์ํฉ๋ค
์ด๋ก ์ ๊ทธ๋ ๋ค ์น๊ณ , ์ค์ ๋ก ์ด๋ค ์ํฉ์์ CORS ์๋ฌ๊ฐ ํฐ์ง๋์ง ๋ณผ๊น? ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ ๋ง ์์ฃผ ๋ง์ฃผ์น๋ ์ผ์ด์ค๋ค์ด์ผ. ๐
1๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ ์ธ๋ถ API ํธ์ถ
๊ฐ์ฅ ํํ ์ผ์ด์ค์ผ! ๋์ React ์ฑ์ด localhost:3000์์ ๋์๊ฐ๋๋ฐ, ์ธ๋ถ API (์: https://api.weather.com)๋ฅผ ํธ์ถํ๋ ค๊ณ ํ๋ฉด?
// ์ด๋ ๊ฒ ์ฝ๋๋ฅผ ์งฐ๋๋ฐ...
fetch('https://api.weather.com/data')
.then(response => response.json())
.then(data => console.log(data));
// ์ฝ์์๋ ์ด๋ฐ ์๋ฌ๊ฐ!
// โ Access to fetch at 'https://api.weather.com/data'
// from origin 'http://localhost:3000' has been blocked by CORS policy
2ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ํฌํธ๊ฐ ๋ค๋ฅผ ๋
ํ๋ก ํธ๋ 3000๋ฒ ํฌํธ, ๋ฐฑ์๋๋ 8080๋ฒ ํฌํธ์์ ๋๋ฆฌ๋ ๊ฒฝ์ฐ. ๊ฐ์ localhost์ฌ๋ ํฌํธ๊ฐ ๋ค๋ฅด๋ฉด ๋ค๋ฅธ ์ถ์ฒ๋ก ์ธ์๋ผ!
// ํ๋ก ํธ์๋ (localhost:3000)
fetch('http://localhost:8080/api/users')
.then(response => response.json());
// ์ด๊ฒ๋ CORS ์๋ฌ! ํฌํธ๊ฐ ๋ค๋ฅด๊ฑฐ๋ ๐ญ
3์๋ํํฐ API๊ฐ CORS๋ฅผ ์ง์ํ์ง ์์ ๋
์ผ๋ถ ์ค๋๋ API๋ CORS ์ค์ ์ด ์ ๋ API๋ ์์ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ํธ์ถ์ด ๋ถ๊ฐ๋ฅํด. ํนํ ๊ณต๊ณต ๋ฐ์ดํฐ API ์ค์ ์ด๋ฐ ๊ฒฝ์ฐ๊ฐ ๋ง์.
โ ๏ธ ์ฃผ์: CORS๋ ๋ธ๋ผ์ฐ์ ์ ์ ์ฑ ์ด์ผ!
์ค์ํ ํฌ์ธํธ! CORS ์๋ฌ๋ ๋ธ๋ผ์ฐ์ ์์๋ง ๋ฐ์ํด. Postman์ด๋ ์๋ฒ ๊ฐ ํต์ ์์๋ ๋ฌธ์ ์์ด ์๋ํ๋๋ฐ ๋ธ๋ผ์ฐ์ ์์๋ง ๋งํ๋ ์ด์ ๊ฐ ๋ฐ๋ก ์ด๊ฑฐ์ผ. ๋ธ๋ผ์ฐ์ ๊ฐ ์ฌ์ฉ์ ๋ณด์์ ์ํด ์ถ๊ฐ๋ก ๊ฒ์ฌํ๋ ๊ฑฐ์ง! ๐
๐ ๏ธ CORS ํด๊ฒฐ ๋ฐฉ๋ฒ๋ค ์ด์ ๋ฆฌ
CORS ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ด. ์ํฉ์ ๋ฐ๋ผ ์ ์ ํ ๋ฐฉ๋ฒ์ ์ ํํ๋ฉด ๋ผ! ๊ฐ๊ฐ์ ์ฅ๋จ์ ์ ์ดํด๋ณด์. ๐ฏ
| ํด๊ฒฐ ๋ฐฉ๋ฒ | ๋์ด๋ | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| ์๋ฒ์์ CORS ํค๋ ์ค์ | โญโญ | ๊ทผ๋ณธ์ ํด๊ฒฐ, ์์ ์ | ์๋ฒ ์ ๊ทผ ๊ถํ ํ์ |
| ํ๋ก์ ์๋ฒ ์ฌ์ฉ | โญโญโญ | ์๋ฒ ์์ ๋ถํ์, ์ ์ฐํจ | ์ถ๊ฐ ์๋ฒ ํ์ |
| ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ | โญ | ์ฆ์ ์ฌ์ฉ ๊ฐ๋ฅ | ๊ฐ๋ฐ์ฉ์ผ๋ก๋ง, ๋ณด์ ์ํ |
| JSONP ๋ฐฉ์ | โญโญ | ์ค๋๋ ๋ธ๋ผ์ฐ์ ์ง์ | GET๋ง ๊ฐ๋ฅ, ๊ตฌ์ ๋ฐฉ๋ฒ |
์ด ์ค์์ ์ค๋ ์ฐ๋ฆฌ๊ฐ ์ง์คํ ๊ฑด ๋ฐ๋ก ํ๋ก์ ์๋ฒ ๋ฐฉ์์ด์ผ! ์๋ํ๋ฉด:
โ ์๋ฒ ์์ ๋ถํ์
์ธ๋ถ API ์๋ฒ๋ฅผ ๊ฑด๋๋ฆด ํ์๊ฐ ์์ด. ํนํ ์๋ํํฐ API ์ฌ์ฉ ์ ์๋ฒฝ!
โ ์ค์ ๋ฐฐํฌ ํ๊ฒฝ์ ์ ํฉ
๊ฐ๋ฐ์ฉ ์์๋ฐฉํธ์ด ์๋๋ผ ํ๋ก๋์ ์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ ์ ์ ๋ฐฉ๋ฒ์ด์ผ.
โ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ ๊ฐ๋ฅ
์บ์ฑ, ๋ก๊น , ์ธ์ฆ ์ฒ๋ฆฌ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ํ๋ก์์ ์ถ๊ฐํ ์ ์์ด.
๐ ํ๋ก์ ์๋ฒ๋ ๋ฌด์์ธ๊ฐ?
ํ๋ก์(Proxy)๋ '๋๋ฆฌ'๋ผ๋ ๋ป์ด์ผ. ํ๋ก์ ์๋ฒ๋ ๋ง ๊ทธ๋๋ก ํด๋ผ์ด์ธํธ์ ์๋ฒ ์ฌ์ด์์ ์ค๊ฐ์ ์ญํ ์ ํ๋ ์๋ฒ์ง! ๐ค
์๊ฐํด๋ด. ๋๊ฐ ํด์ธ ์ง๊ตฌ๋ฅผ ํ ๋ ๋ฐฐ์ก๋ํ์ง๋ฅผ ์ฐ๋ ๊ฒ์ฒ๋ผ, ํ๋ก์ ์๋ฒ๋ ๋น์ทํ ์ญํ ์ ํด. ๋ธ๋ผ์ฐ์ ๊ฐ ์ง์ ์ธ๋ถ API์ ์ ๊ทผํ๋ ๊ฒ ์๋๋ผ, ํ๋ก์ ์๋ฒ์๊ฒ "์ด๊ฑฐ ์ข ๋์ ๊ฐ์ ธ๋ค์ค!"๋ผ๊ณ ๋ถํํ๋ ๊ฑฐ์ผ. ๐ฆ
๐ฏ ํ๋ก์ ์๋ฒ์ ์๋ ๋ฐฉ์
Step 1: ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ก์ ์๋ฒ์ ์์ฒญ (๊ฐ์ ์ถ์ฒ์ด๋ฏ๋ก CORS ๋ฌธ์ ์์)
Step 2: ํ๋ก์ ์๋ฒ๊ฐ ์ธ๋ถ API์ ์์ฒญ (์๋ฒ ๊ฐ ํต์ ์ด๋ฏ๋ก CORS ๋ฌด๊ด)
Step 3: ์ธ๋ถ API๊ฐ ํ๋ก์ ์๋ฒ์ ์๋ต
Step 4: ํ๋ก์ ์๋ฒ๊ฐ ๋ธ๋ผ์ฐ์ ์ ์๋ต ์ ๋ฌ
์ด๋ ๊ฒ ํ๋ฉด ๋ธ๋ผ์ฐ์ ์
์ฅ์์๋ ๊ฐ์ ์ถ์ฒ์ ์๋ฒ์๋ง ํต์ ํ๋ ๊ฑฐ๋๊น CORS ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์! ๐
ํ๋ก์ ์๋ฒ์ ์ฅ์ ์ ๋จ์ํ CORS ํด๊ฒฐ์๋ง ๊ทธ์น์ง ์์:
๐ ๋ณด์ ๊ฐํ
API ํค๋ฅผ ํด๋ผ์ด์ธํธ์ ๋ ธ์ถํ์ง ์๊ณ ์๋ฒ์์ ๊ด๋ฆฌํ ์ ์์ด.
โก ์ฑ๋ฅ ์ต์ ํ
์์ฃผ ์์ฒญํ๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํด์ ์๋ต ์๋๋ฅผ ๋์ผ ์ ์์ง.
๐ ๋ก๊น & ๋ชจ๋ํฐ๋ง
๋ชจ๋ ์์ฒญ์ ์ค์์์ ๊ด๋ฆฌํ๊ณ ๊ธฐ๋กํ ์ ์์ด.
๐ ์์ฒญ/์๋ต ๋ณํ
๋ฐ์ดํฐ ํ์์ ๋ณํํ๊ฑฐ๋ ์ถ๊ฐ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์์ด.
๐ป Node.js๋ก ๊ฐ๋จํ ํ๋ก์ ์๋ฒ ๋ง๋ค๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ๋ก์ ์๋ฒ๋ฅผ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ๋ถํฐ ์์ํด์ ์ ์ ๊ณ ๊ธ ๊ธฐ๋ฅ๊น์ง ์์๋ณด์. ๐
๋ฐฉ๋ฒ 1: http-proxy-middleware ์ฌ์ฉํ๊ธฐ
๊ฐ์ฅ ์ฝ๊ณ ๋ง์ด ์ฐ์ด๋ ๋ฐฉ๋ฒ์ด์ผ. Express์ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ ๋ง ๊ฐ๋จํด! ๋จผ์ ํ์ํ ํจํค์ง๋ฅผ ์ค์นํ์:
npm install express http-proxy-middleware cors
๊ทธ๋ฆฌ๊ณ ์ด๋ ๊ฒ ํ๋ก์ ์๋ฒ๋ฅผ ๋ง๋ค์ด:
// proxy-server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');
const app = express();
const PORT = 5000;
// CORS ์ค์ (๋ชจ๋ ์ถ์ฒ ํ์ฉ)
app.use(cors());
// ํ๋ก์ ๋ฏธ๋ค์จ์ด ์ค์
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com', // ์ค์ API ์๋ฒ ์ฃผ์
changeOrigin: true, // ํธ์คํธ ํค๋๋ฅผ ํ๊ฒ URL๋ก ๋ณ๊ฒฝ
pathRewrite: {
'^/api': '', // /api ๊ฒฝ๋ก๋ฅผ ์ ๊ฑฐํ๊ณ ์ ๋ฌ
},
onProxyReq: (proxyReq, req, res) => {
// ์์ฒญ ์ ๋ก๊น
console.log(`[ํ๋ก์ ์์ฒญ] ${req.method} ${req.path}`);
},
onProxyRes: (proxyRes, req, res) => {
// ์๋ต ํ ๋ก๊น
console.log(`[ํ๋ก์ ์๋ต] ${proxyRes.statusCode}`);
},
onError: (err, req, res) => {
// ์๋ฌ ์ฒ๋ฆฌ
console.error('[ํ๋ก์ ์๋ฌ]', err);
res.status(500).json({ error: 'ํ๋ก์ ์๋ฒ ์๋ฌ ๋ฐ์' });
}
}));
// ์๋ฒ ์์
app.listen(PORT, () => {
console.log(`๐ ํ๋ก์ ์๋ฒ๊ฐ ํฌํธ ${PORT}์์ ์คํ ์ค์
๋๋ค!`);
});
์ด์ ํด๋ผ์ด์ธํธ์์๋ ์ด๋ ๊ฒ ์ฌ์ฉํ๋ฉด ๋ผ:
// ํด๋ผ์ด์ธํธ ์ฝ๋ (React, Vue ๋ฑ)
// ๊ธฐ์กด: fetch('https://api.example.com/users') โ CORS ์๋ฌ!
// ๋ณ๊ฒฝ: fetch('http://localhost:5000/api/users') โ
์ ์ ์๋!
fetch('http://localhost:5000/api/users')
.then(response => response.json())
.then(data => {
console.log('๋ฐ์ดํฐ ๋ฐ๊ธฐ ์ฑ๊ณต!', data);
})
.catch(error => {
console.error('์๋ฌ ๋ฐ์:', error);
});
๐ ์ฝ๋ ์ค๋ช
target: ์ค์ ๋ก ์์ฒญ์ ๋ณด๋ผ API ์๋ฒ ์ฃผ์
changeOrigin: true๋ก ์ค์ ํ๋ฉด ํธ์คํธ ํค๋๋ฅผ ํ๊ฒ URL๋ก ๋ณ๊ฒฝํด์ค. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ํ์ํด!
pathRewrite: ๊ฒฝ๋ก๋ฅผ ๋ณํํด์ค. ์๋ฅผ ๋ค์ด /api๋ฅผ ์ ๊ฑฐํ๊ณ ์ถ์ ๋ ์ฌ์ฉ
onProxyReq/onProxyRes: ์์ฒญ/์๋ต ์ ์ถ๊ฐ ์์
์ ํ ์ ์๋ ํ
(hook) ํจ์๋ค
๋ฐฉ๋ฒ 2: ์ง์ fetch๋ก ํ๋ก์ ๊ตฌํํ๊ธฐ
http-proxy-middleware ์์ด ์ง์ ๊ตฌํํ ์๋ ์์ด. ๋ ์ธ๋ฐํ ์ ์ด๊ฐ ํ์ํ ๋ ์ ์ฉํ์ง! ๐จ
// simple-proxy.js
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch'); // Node.js 18 ๋ฏธ๋ง์์๋ ์ค์น ํ์
const app = express();
const PORT = 5000;
app.use(cors());
app.use(express.json()); // JSON ํ์ฑ
// GET ์์ฒญ ํ๋ก์
app.get('/api/*', async (req, res) => {
try {
// ์๋ณธ ๊ฒฝ๋ก์์ /api ์ ๊ฑฐ
const targetPath = req.path.replace('/api', '');
const targetUrl = `https://api.example.com${targetPath}`;
// ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํฌํจ
const queryString = new URLSearchParams(req.query).toString();
const fullUrl = queryString ? `${targetUrl}?${queryString}` : targetUrl;
console.log(`๐ก ์์ฒญ ์ ๋ฌ: ${fullUrl}`);
// ์ธ๋ถ API ํธ์ถ
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// ํ์ํ ๊ฒฝ์ฐ ์ธ์ฆ ํค๋ ์ถ๊ฐ
// 'Authorization': 'Bearer YOUR_API_KEY'
}
});
const data = await response.json();
// ํด๋ผ์ด์ธํธ์ ์๋ต
res.status(response.status).json(data);
} catch (error) {
console.error('โ ํ๋ก์ ์๋ฌ:', error);
res.status(500).json({
error: '์๋ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค',
message: error.message
});
}
});
// POST ์์ฒญ ํ๋ก์
app.post('/api/*', async (req, res) => {
try {
const targetPath = req.path.replace('/api', '');
const targetUrl = `https://api.example.com${targetPath}`;
console.log(`๐ค POST ์์ฒญ ์ ๋ฌ: ${targetUrl}`);
console.log('๐ฆ ์์ฒญ ๋ณธ๋ฌธ:', req.body);
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
console.error('โ ํ๋ก์ ์๋ฌ:', error);
res.status(500).json({
error: '์๋ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค',
message: error.message
});
}
});
app.listen(PORT, () => {
console.log(`๐ ์ปค์คํ
ํ๋ก์ ์๋ฒ ์คํ ์ค: http://localhost:${PORT}`);
});
์ด ๋ฐฉ๋ฒ์ ์ฅ์ ์ ์์ฒญ/์๋ต์ ์์ ํ ์ ์ดํ ์ ์๋ค๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด:
โจ ์๋ต ๋ฐ์ดํฐ ๊ฐ๊ณตํ๊ธฐ
const data = await response.json();
// ๋ฐ์ดํฐ ๊ฐ๊ณต ์์
const processedData = {
...data,
timestamp: new Date().toISOString(),
source: 'proxy-server'
};
res.json(processedData);
๐ API ํค ์จ๊ธฐ๊ธฐ
// ํ๊ฒฝ ๋ณ์์์ API ํค ๊ฐ์ ธ์ค๊ธฐ
const API_KEY = process.env.API_KEY;
const response = await fetch(targetUrl, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
// ํด๋ผ์ด์ธํธ๋ API ํค๋ฅผ ๋ชจ๋ฅด๋ ์ํ๋ก ์ฌ์ฉ ๊ฐ๋ฅ!
๋ฐฉ๋ฒ 3: Create React App์ ํ๋ก์ ์ค์
React ๊ฐ๋ฐ์๋ผ๋ฉด ์ด ๋ฐฉ๋ฒ์ด ๊ฐ์ฅ ๊ฐ๋จํ ์๋ ์์ด! Create React App์๋ ํ๋ก์ ๊ธฐ๋ฅ์ด ๋ด์ฅ๋์ด ์๊ฑฐ๋ . ๐
package.json์ ํ ์ค๋ง ์ถ๊ฐ:
{
"name": "my-app",
"version": "0.1.0",
"proxy": "http://localhost:8080", // ๐ ์ด๊ฒ๋ง ์ถ๊ฐ!
"dependencies": {
"react": "^18.0.0",
...
}
}
์ด์ ์๋ ๊ฒฝ๋ก๋ก API๋ฅผ ํธ์ถํ๋ฉด ์๋์ผ๋ก ํ๋ก์๋ผ:
// ์ด๋ ๊ฒ๋ง ํ๋ฉด ์๋์ผ๋ก http://localhost:8080/api/users๋ก ์์ฒญ๋จ!
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data));
๋ ๋ณต์กํ ์ค์ ์ด ํ์ํ๋ฉด src/setupProxy.js ํ์ผ์ ๋ง๋ค์ด:
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
// ์ฌ๋ฌ API ์๋ํฌ์ธํธ ์ค์ ๊ฐ๋ฅ
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
})
);
app.use(
'/auth',
createProxyMiddleware({
target: 'http://localhost:9000',
changeOrigin: true,
})
);
// ์ธ๋ถ API๋ ๊ฐ๋ฅ
app.use(
'/external',
createProxyMiddleware({
target: 'https://api.external.com',
changeOrigin: true,
pathRewrite: {
'^/external': '', // /external ์ ๊ฑฐ
},
})
);
};
๐ก CRA ํ๋ก์ ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
โ
๊ฐ๋ฐ ์๋ฒ(npm start)์์๋ง ์๋ํด
โ
ํ๋ก๋์
๋น๋์๋ ์ ์ฉ๋์ง ์์
โ
๋ฐฐํฌ ์์๋ ๋ณ๋์ ํ๋ก์ ์๋ฒ๋ ์๋ฒ ์ค์ ํ์
โ
setupProxy.js๋ ์ฌ์์ ์์ด ์๋ ์ ์ฉ๋ผ
๐จ ์ค์ ํ๋ก์ ์๋ฒ ๊ณ ๊ธ ๊ธฐ๋ฅ
๊ธฐ๋ณธ์ ์ธ ํ๋ก์๋ ๋ง๋ค์์ผ๋, ์ด์ ์ค๋ฌด์์ ์ ์ฉํ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณด์! ์ฌ๋ฅ๋ท์์ ์น ๊ฐ๋ฐ ํ๋ก์ ํธ๋ฅผ ์งํํ ๋๋ ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ด ์ ๋ง ์ ์ฉํ ๊ฑฐ์ผ. ๐ช
1๏ธโฃ ์บ์ฑ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
๊ฐ์ ์์ฒญ์ ๋ฐ๋ณตํ๋ฉด ๋งค๋ฒ ์ธ๋ถ API๋ฅผ ํธ์ถํ๋ ๊ฑด ๋นํจ์จ์ ์ด์ผ. ์บ์ฑ์ ์ถ๊ฐํด์ ์ฑ๋ฅ์ ๋์ฌ๋ณด์! โก
// cache-proxy.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');
const NodeCache = require('node-cache'); // npm install node-cache
const app = express();
const PORT = 5000;
// ์บ์ ์ค์ (TTL: 5๋ถ)
const cache = new NodeCache({ stdTTL: 300 });
app.use(cors());
// ์บ์ ๋ฏธ๋ค์จ์ด
const cacheMiddleware = (req, res, next) => {
// GET ์์ฒญ๋ง ์บ์ฑ
if (req.method !== 'GET') {
return next();
}
const key = req.originalUrl;
const cachedResponse = cache.get(key);
if (cachedResponse) {
console.log(`โ
์บ์ ํํธ: ${key}`);
return res.json(cachedResponse);
}
console.log(`โ ์บ์ ๋ฏธ์ค: ${key}`);
// ์๋ res.json์ ์ ์ฅ
const originalJson = res.json.bind(res);
// res.json์ ์ค๋ฒ๋ผ์ด๋ํด์ ์บ์์ ์ ์ฅ
res.json = (data) => {
cache.set(key, data);
console.log(`๐พ ์บ์ ์ ์ฅ: ${key}`);
return originalJson(data);
};
next();
};
// ์บ์ ๋ฏธ๋ค์จ์ด ์ ์ฉ
app.use('/api', cacheMiddleware);
// ํ๋ก์ ์ค์
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}));
// ์บ์ ์ด๊ธฐํ ์๋ํฌ์ธํธ
app.delete('/cache', (req, res) => {
cache.flushAll();
res.json({ message: '์บ์๊ฐ ๋ชจ๋ ์ญ์ ๋์์ต๋๋ค' });
});
// ์บ์ ์ํ ํ์ธ
app.get('/cache/stats', (req, res) => {
const stats = cache.getStats();
res.json({
keys: cache.keys().length,
hits: stats.hits,
misses: stats.misses,
hitRate: stats.hits / (stats.hits + stats.misses)
});
});
app.listen(PORT, () => {
console.log(`๐ ์บ์ฑ ํ๋ก์ ์๋ฒ ์คํ ์ค: ${PORT}`);
});
2๏ธโฃ Rate Limiting (์์ฒญ ์ ํ)
์ธ๋ถ API๊ฐ ์์ฒญ ํ์ ์ ํ์ด ์๋ค๋ฉด? ํ๋ก์ ์๋ฒ์์ ๋ฏธ๋ฆฌ ์ ์ดํ ์ ์์ด! ๐ฆ
// rate-limit-proxy.js
const express = require('express');
const rateLimit = require('express-rate-limit'); // npm install express-rate-limit
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Rate Limiter ์ค์
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: {
error: '๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
retryAfter: '15๋ถ'
},
standardHeaders: true, // RateLimit-* ํค๋ ์ถ๊ฐ
legacyHeaders: false,
handler: (req, res) => {
console.log(`โ ๏ธ Rate limit ์ด๊ณผ: ${req.ip}`);
res.status(429).json({
error: '์์ฒญ ํ๋๋ฅผ ์ด๊ณผํ์ต๋๋ค',
message: '15๋ถ ํ์ ๋ค์ ์๋ํด์ฃผ์ธ์'
});
}
});
// ํน์ ๊ฒฝ๋ก์๋ง ์ ์ฉ
app.use('/api', limiter);
// ํ๋ก์ ์ค์
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true
}));
app.listen(5000, () => {
console.log('๐ Rate Limiting ํ๋ก์ ์๋ฒ ์คํ ์ค');
});
3๏ธโฃ ์ธ์ฆ & ๋ณด์ ๊ฐํ
API ํค๋ฅผ ํด๋ผ์ด์ธํธ์ ๋ ธ์ถํ์ง ์๊ณ ์๋ฒ์์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ! ๐
// secure-proxy.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
require('dotenv').config(); // npm install dotenv
const app = express();
// ํ๊ฒฝ ๋ณ์์์ API ํค ๋ก๋
const API_KEY = process.env.EXTERNAL_API_KEY;
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [];
// CORS ์ค์ (ํน์ ์ถ์ฒ๋ง ํ์ฉ)
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// ๊ฐ๋จํ ์ธ์ฆ ๋ฏธ๋ค์จ์ด
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
// ์ค์ ๋ก๋ JWT ๊ฒ์ฆ ๋ฑ์ ์ํ
if (!token || token !== process.env.CLIENT_TOKEN) {
return res.status(401).json({ error: '์ธ์ฆ์ด ํ์ํฉ๋๋ค' });
}
next();
};
// ์ธ์ฆ ์ ์ฉ
app.use('/api', authenticate);
// ํ๋ก์ ์ค์ (API ํค ์๋ ์ถ๊ฐ)
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
// ๋ชจ๋ ์์ฒญ์ API ํค ์๋ ์ถ๊ฐ
proxyReq.setHeader('X-API-Key', API_KEY);
console.log('๐ API ํค ์๋ ์ถ๊ฐ๋จ');
}
}));
app.listen(5000);
.env ํ์ผ ์์:
# .env
EXTERNAL_API_KEY=your_secret_api_key_here
CLIENT_TOKEN=your_client_token_here
ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
4๏ธโฃ ๋ก๊น & ๋ชจ๋ํฐ๋ง
๋ชจ๋ ์์ฒญ์ ๊ธฐ๋กํ๊ณ ๋ถ์ํ ์ ์๊ฒ ๋ง๋ค์ด๋ณด์! ๐
// logging-proxy.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const morgan = require('morgan'); // npm install morgan
const fs = require('fs');
const path = require('path');
const app = express();
// ๋ก๊ทธ ๋๋ ํ ๋ฆฌ ์์ฑ
const logDirectory = path.join(__dirname, 'logs');
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
// ๋ก๊ทธ ํ์ผ ์คํธ๋ฆผ
const accessLogStream = fs.createWriteStream(
path.join(logDirectory, 'access.log'),
{ flags: 'a' }
);
// Morgan ๋ก๊น
์ค์
app.use(morgan('combined', { stream: accessLogStream }));
app.use(morgan('dev')); // ์ฝ์์๋ ์ถ๋ ฅ
// ์ปค์คํ
๋ก๊น
๋ฏธ๋ค์จ์ด
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
};
console.log('๐ ์์ฒญ ๋ก๊ทธ:', JSON.stringify(log));
// ํ์ผ์ JSON ํ์์ผ๋ก ์ ์ฅ
fs.appendFileSync(
path.join(logDirectory, 'requests.json'),
JSON.stringify(log) + '\n'
);
});
next();
});
// ํ๋ก์ ์ค์
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
console.log(`โก๏ธ ํ๋ก์ ์์ฒญ: ${req.method} ${req.path}`);
},
onProxyRes: (proxyRes, req, res) => {
console.log(`โฌ
๏ธ ํ๋ก์ ์๋ต: ${proxyRes.statusCode}`);
},
onError: (err, req, res) => {
console.error('โ ํ๋ก์ ์๋ฌ:', err.message);
// ์๋ฌ ๋ก๊ทธ ์ ์ฅ
const errorLog = {
timestamp: new Date().toISOString(),
error: err.message,
url: req.originalUrl,
method: req.method
};
fs.appendFileSync(
path.join(logDirectory, 'errors.json'),
JSON.stringify(errorLog) + '\n'
);
}
}));
// ๋ก๊ทธ ์กฐํ API
app.get('/logs/recent', (req, res) => {
const logs = fs.readFileSync(
path.join(logDirectory, 'requests.json'),
'utf-8'
)
.split('\n')
.filter(line => line)
.map(line => JSON.parse(line))
.slice(-50); // ์ต๊ทผ 50๊ฐ
res.json(logs);
});
app.listen(5000);
๐ ์ค์ ๋ฐฐํฌ: ํ๋ก์ ์๋ฒ ์ด์ํ๊ธฐ
๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ ์๋ํ๋๋ฐ, ์ค์ ๋ฐฐํฌ๋ ์ด๋ป๊ฒ ํ ๊น? ํ๋ก๋์ ํ๊ฒฝ์์ ํ๋ก์ ์๋ฒ๋ฅผ ์์ ์ ์ผ๋ก ์ด์ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
๋ฐฐํฌ ์ต์ ๋น๊ต
| ๋ฐฐํฌ ๋ฐฉ์ | ๋์ด๋ | ๋น์ฉ | ์ฅ์ | ์ถ์ฒ ๋์ |
|---|---|---|---|---|
| Heroku | โญ | ๋ฌด๋ฃ~์ ๋ ด | ๊ฐํธํ ๋ฐฐํฌ, Git ์ฐ๋ | ๊ฐ์ธ ํ๋ก์ ํธ, ํ๋กํ ํ์ |
| Vercel/Netlify | โญ | ๋ฌด๋ฃ~์ ๋ ด | ์๋ฒ๋ฆฌ์ค, ์๋ ์ค์ผ์ผ๋ง | ์์ ๊ท๋ชจ ํ๋ก์ ํธ |
| AWS EC2 | โญโญโญ | ์ค๊ฐ | ์์ ํ ์ ์ด, ํ์ฅ์ฑ | ์ค๋ํ ํ๋ก์ ํธ |
| Docker + ํด๋ผ์ฐ๋ | โญโญโญโญ | ์ค๊ฐ~๋์ | ์ด์์ฑ, ํ์ฅ์ฑ, ๊ด๋ฆฌ ์ฉ์ด | ์ํฐํ๋ผ์ด์ฆ๊ธ |
Heroku์ ๋ฐฐํฌํ๊ธฐ (๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ)
Heroku๋ ์ ๋ง ๊ฐ๋จํด! Git push๋ง ํ๋ฉด ์๋์ผ๋ก ๋ฐฐํฌ๋ผ. ๐
1package.json ์ค์
{
"name": "cors-proxy-server",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"engines": {
"node": "18.x" // Node.js ๋ฒ์ ๋ช
์
},
"dependencies": {
"express": "^4.18.0",
"http-proxy-middleware": "^2.0.0",
"cors": "^2.8.5"
}
}
2Procfile ์์ฑ
web: node server.js
3ํฌํธ ์ค์ ์์
// server.js
const PORT = process.env.PORT || 5000; // Heroku๊ฐ ํฌํธ๋ฅผ ์๋ ํ ๋น
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
4Heroku CLI๋ก ๋ฐฐํฌ
# Heroku CLI ์ค์น ํ
heroku login
heroku create my-cors-proxy
git push heroku main
# ํ๊ฒฝ ๋ณ์ ์ค์
heroku config:set API_KEY=your_api_key_here
Vercel Serverless Functions ์ฌ์ฉํ๊ธฐ
Vercel์ ์๋ฒ๋ฆฌ์ค ๋ฐฉ์์ผ๋ก ํ๋ก์๋ฅผ ๊ตฌํํ ์ ์์ด. ํธ๋ํฝ์ด ์ ์ ๋ ๋น์ฉ ํจ์จ์ ์ด์ผ! ๐ฐ
// api/proxy.js (Vercel Functions)
const fetch = require('node-fetch');
module.exports = async (req, res) => {
// CORS ํค๋ ์ค์
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
try {
// ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์์ ํ๊ฒ URL ๊ฐ์ ธ์ค๊ธฐ
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํฉ๋๋ค' });
}
// ์ธ๋ถ API ํธ์ถ
const response = await fetch(url, {
method: req.method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.API_KEY}`
},
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined
});
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
console.error('Proxy error:', error);
res.status(500).json({ error: error.message });
}
};
ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉ:
// ํด๋ผ์ด์ธํธ ์ฝ๋
const targetUrl = encodeURIComponent('https://api.example.com/users');
fetch(`https://your-app.vercel.app/api/proxy?url=${targetUrl}`)
.then(res => res.json())
.then(data => console.log(data));
Docker๋ก ์ปจํ ์ด๋ํํ๊ธฐ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋์๋ ๋์ผํ๊ฒ ์คํํ ์ ์์ด! ๐ณ
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# ์์กด์ฑ ํ์ผ ๋ณต์ฌ
COPY package*.json ./
# ์์กด์ฑ ์ค์น
RUN npm ci --only=production
# ์์ค ์ฝ๋ ๋ณต์ฌ
COPY . .
# ํฌํธ ๋
ธ์ถ
EXPOSE 5000
# ํฌ์ค์ฒดํฌ ์ถ๊ฐ
HEALTHCHECK --interval=30s --timeout=3s \
CMD node healthcheck.js || exit 1
# ์คํ
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
proxy:
build: .
ports:
- "5000:5000"
environment:
- NODE_ENV=production
- API_KEY=${API_KEY}
restart: unless-stopped
volumes:
- ./logs:/app/logs
networks:
- proxy-network
networks:
proxy-network:
driver: bridge
์คํ ๋ฐฉ๋ฒ:
# ์ด๋ฏธ์ง ๋น๋
docker build -t cors-proxy .
# ์ปจํ
์ด๋ ์คํ
docker run -p 5000:5000 -e API_KEY=your_key cors-proxy
# ๋๋ docker-compose ์ฌ์ฉ
docker-compose up -d
๐ง ํ๋ก๋์ ํ๊ฒฝ ์ฒดํฌ๋ฆฌ์คํธ
โ
ํ๊ฒฝ ๋ณ์๋ก ๋ฏผ๊ฐํ ์ ๋ณด ๊ด๋ฆฌ (.env ํ์ผ ์ฌ์ฉ)
โ
HTTPS ์ฌ์ฉ (Let's Encrypt ๋ฑ์ผ๋ก SSL ์ธ์ฆ์ ์ค์ )
โ
Rate Limiting ์ ์ฉ (DDoS ๊ณต๊ฒฉ ๋ฐฉ์ง)
โ
๋ก๊น
์์คํ
๊ตฌ์ถ (์๋ฌ ์ถ์ ๋ฐ ๋ชจ๋ํฐ๋ง)
โ
ํฌ์ค์ฒดํฌ ์๋ํฌ์ธํธ ์ถ๊ฐ (/health)
โ
ํ๋ก์ธ์ค ๋งค๋์ ์ฌ์ฉ (PM2, Forever ๋ฑ)
โ
์๋ ์ฌ์์ ์ค์
โ
๋ฐฑ์
๋ฐ ๋ณต๊ตฌ ๊ณํ ์๋ฆฝ
PM2๋ก ํ๋ก์ธ์ค ๊ด๋ฆฌํ๊ธฐ
์๋ฒ๊ฐ ์ฃฝ์ผ๋ฉด ์๋์ผ๋ก ์ฌ์์๋๊ฒ ๋ง๋ค์! PM2๋ Node.js ํ๋ก์ธ์ค ๋งค๋์ ์ ํ์ค์ด์ผ. ๐
# PM2 ์ค์น
npm install -g pm2
# ์๋ฒ ์์
pm2 start server.js --name "cors-proxy"
# ํด๋ฌ์คํฐ ๋ชจ๋ (๋ฉํฐ ์ฝ์ด ํ์ฉ)
pm2 start server.js -i max --name "cors-proxy"
# ์๋ ์ฌ์์ ์ค์
pm2 startup
pm2 save
# ๋ชจ๋ํฐ๋ง
pm2 monit
# ๋ก๊ทธ ํ์ธ
pm2 logs cors-proxy
ecosystem.config.js ํ์ผ๋ก ์ค์ ๊ด๋ฆฌ:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'cors-proxy',
script: './server.js',
instances: 'max', // CPU ์ฝ์ด ์๋งํผ ์ธ์คํด์ค ์์ฑ
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 5000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
max_memory_restart: '500M', // ๋ฉ๋ชจ๋ฆฌ 500MB ์ด๊ณผ ์ ์ฌ์์
watch: false, // ํ๋ก๋์
์์๋ watch ๋นํ์ฑํ
ignore_watch: ['node_modules', 'logs'],
max_restarts: 10,
min_uptime: '10s'
}]
};
# ecosystem ํ์ผ๋ก ์คํ
pm2 start ecosystem.config.js
๐ ํธ๋ฌ๋ธ์ํ : ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค
ํ๋ก์ ์๋ฒ๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ์ด์๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ง
โ ๋ฌธ์ 1: ํ๋ก์ ์๋ฒ๋ ์๋ํ๋๋ฐ ์ฌ์ ํ CORS ์๋ฌ
์์ธ: ํด๋ผ์ด์ธํธ๊ฐ ์ฌ์ ํ ์ธ๋ถ API๋ฅผ ์ง์ ํธ์ถํ๊ณ ์์ ๊ฐ๋ฅ์ฑ
ํด๊ฒฐ:
// โ ์๋ชป๋ ์ฝ๋ - ์ฌ์ ํ ์ธ๋ถ API ์ง์ ํธ์ถ
fetch('https://api.example.com/users')
// โ
์ฌ๋ฐ๋ฅธ ์ฝ๋ - ํ๋ก์ ์๋ฒ๋ฅผ ํตํด ํธ์ถ
fetch('http://localhost:5000/api/users')
โ ๋ฌธ์ 2: ํ๋ก์ ์๋ฒ๊ฐ 504 Gateway Timeout ์๋ฌ
์์ธ: ์ธ๋ถ API ์๋ต์ด ๋๋ฌด ๋๋ฆฌ๊ฑฐ๋ ํ์์์ ์ค์ ์ด ์งง์
ํด๊ฒฐ:
// ํ์์์ ์ค์ ๋๋ฆฌ๊ธฐ
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
timeout: 30000, // 30์ด๋ก ์ฆ๊ฐ
proxyTimeout: 30000
}));
// ๋๋ fetch ์ฌ์ฉ ์
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeout));
โ ๋ฌธ์ 3: POST ์์ฒญ ์ body๊ฐ undefined
์์ธ: body-parser ๋ฏธ๋ค์จ์ด๊ฐ ์๊ฑฐ๋ ์์๊ฐ ์๋ชป๋จ
ํด๊ฒฐ:
const express = require('express');
const app = express();
// โ
ํ๋ก์ ๋ฏธ๋ค์จ์ด๋ณด๋ค ๋จผ์ ์ค์ !
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ๊ทธ ๋ค์ ํ๋ก์ ์ค์
app.use('/api', createProxyMiddleware({...}));
โ ๋ฌธ์ 4: ์ฟ ํค๊ฐ ์ ๋ฌ๋์ง ์์
์์ธ: ํฌ๋ก์ค ๋๋ฉ์ธ ์ฟ ํค ์ค์ ๋ฌธ์
ํด๊ฒฐ:
// ์๋ฒ ์ธก
app.use(cors({
origin: 'http://localhost:3000',
credentials: true // ์ฟ ํค ํ์ฉ
}));
// ํด๋ผ์ด์ธํธ ์ธก
fetch('http://localhost:5000/api/users', {
credentials: 'include' // ์ฟ ํค ํฌํจ
});
โ ๋ฌธ์ 5: ํ๋ก์ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ ๋์
์์ธ: ์บ์๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ ๋๋ก ์ ๋ฆฌ๋์ง ์์
ํด๊ฒฐ:
// ์บ์ TTL ์ค์
const cache = new NodeCache({
stdTTL: 600, // 10๋ถ
checkperiod: 120, // 2๋ถ๋ง๋ค ๋ง๋ฃ๋ ํญ๋ชฉ ํ์ธ
useClones: false // ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ
});
// ์ฃผ๊ธฐ์ ์ผ๋ก ์บ์ ์ ๋ฆฌ
setInterval(() => {
const keys = cache.keys();
console.log(`ํ์ฌ ์บ์ ํญ๋ชฉ ์: ${keys.length}`);
// ์ค๋๋ ํญ๋ชฉ ์๋ ์ญ์
if (keys.length > 1000) {
cache.flushAll();
console.log('์บ์ ์ด๊ธฐํ๋จ');
}
}, 60000); // 1๋ถ๋ง๋ค
๐ ์ฑ๋ฅ ์ต์ ํ ํ
ํ๋ก์ ์๋ฒ์ ์ฑ๋ฅ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์! โก
1๏ธโฃ ์์ถ ์ฌ์ฉํ๊ธฐ
const compression = require('compression');
app.use(compression());
์๋ต ํฌ๊ธฐ๋ฅผ 70-90% ์ค์ผ ์ ์์ด!
2๏ธโฃ Keep-Alive ํ์ฑํ
const http = require('http');
const server = http.createServer(app);
server.keepAliveTimeout = 65000;
์ฐ๊ฒฐ ์ฌ์ฌ์ฉ์ผ๋ก ์๋ ํฅ์!
3๏ธโฃ ํด๋ฌ์คํฐ ๋ชจ๋
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
app.listen(5000);
}
๋ฉํฐ ์ฝ์ด ํ์ฉ์ผ๋ก ์ฒ๋ฆฌ๋ ์ฆ๊ฐ!
4๏ธโฃ Redis ์บ์ฑ
const redis = require('redis');
const client = redis.createClient();
// Redis์ ์บ์ ์ ์ฅ
client.setex(key, 3600, JSON.stringify(data));
๋ถ์ฐ ์บ์ฑ์ผ๋ก ํ์ฅ์ฑ ํ๋ณด!
๐ ์ค์ ์์ : ์์ ํ ํ๋ก์ ์๋ฒ
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋ ๋ด์ฉ์ ์ข ํฉํ ์์ ํ ํ๋ก์ ์๋ฒ ์ฝ๋์ผ! ์ค๋ฌด์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด. ๐ผ
// production-proxy-server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');
const helmet = require('helmet'); // ๋ณด์ ํค๋
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const morgan = require('morgan');
const NodeCache = require('node-cache');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// ==================== ๋ณด์ ์ค์ ====================
app.use(helmet()); // ๋ณด์ ํค๋ ์๋ ์ค์
// CORS ์ค์
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS ์ ์ฑ
์ ์ํด ์ฐจ๋จ๋์์ต๋๋ค'));
}
},
credentials: true
}));
// ==================== ๋ฏธ๋ค์จ์ด ์ค์ ====================
app.use(compression()); // ์๋ต ์์ถ
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(morgan('combined')); // ๋ก๊น
// ==================== Rate Limiting ====================
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: { error: '๋๋ฌด ๋ง์ ์์ฒญ์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.' }
});
app.use('/api', limiter);
// ==================== ์บ์ฑ ์ค์ ====================
const cache = new NodeCache({
stdTTL: 300, // 5๋ถ
checkperiod: 60
});
const cacheMiddleware = (req, res, next) => {
if (req.method !== 'GET') return next();
const key = req.originalUrl;
const cachedData = cache.get(key);
if (cachedData) {
console.log(`โ
์บ์ ํํธ: ${key}`);
return res.json(cachedData);
}
const originalJson = res.json.bind(res);
res.json = (data) => {
cache.set(key, data);
return originalJson(data);
};
next();
};
// ==================== ํฌ์ค์ฒดํฌ ====================
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
cache: {
keys: cache.keys().length,
stats: cache.getStats()
}
});
});
// ==================== ์บ์ ๊ด๋ฆฌ API ====================
app.delete('/cache', (req, res) => {
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${process.env.ADMIN_TOKEN}`) {
return res.status(401).json({ error: '๊ถํ์ด ์์ต๋๋ค' });
}
cache.flushAll();
res.json({ message: '์บ์๊ฐ ์ด๊ธฐํ๋์์ต๋๋ค' });
});
// ==================== ํ๋ก์ ์ค์ ====================
app.use('/api',
cacheMiddleware, // ์บ์ฑ ์ ์ฉ
createProxyMiddleware({
target: process.env.API_TARGET || 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
timeout: 30000,
proxyTimeout: 30000,
// ์์ฒญ ์ ์ฒ๋ฆฌ
onProxyReq: (proxyReq, req, res) => {
// API ํค ์๋ ์ถ๊ฐ
if (process.env.API_KEY) {
proxyReq.setHeader('Authorization', `Bearer ${process.env.API_KEY}`);
}
console.log(`โก๏ธ ${req.method} ${req.path}`);
},
// ์๋ต ํ ์ฒ๋ฆฌ
onProxyRes: (proxyRes, req, res) => {
console.log(`โฌ
๏ธ ${proxyRes.statusCode} ${req.path}`);
},
// ์๋ฌ ์ฒ๋ฆฌ
onError: (err, req, res) => {
console.error('โ ํ๋ก์ ์๋ฌ:', err.message);
res.status(502).json({
error: '์ธ๋ถ API ํธ์ถ ์คํจ',
message: err.message,
timestamp: new Date().toISOString()
});
}
})
);
// ==================== 404 ์ฒ๋ฆฌ ====================
app.use((req, res) => {
res.status(404).json({
error: '์์ฒญํ ์๋ํฌ์ธํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค',
path: req.path
});
});
// ==================== ์๋ฌ ํธ๋ค๋ฌ ====================
app.use((err, req, res, next) => {
console.error('์๋ฒ ์๋ฌ:', err);
res.status(500).json({
error: '์๋ฒ ๋ด๋ถ ์ค๋ฅ',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// ==================== ์๋ฒ ์์ ====================
const server = app.listen(PORT, () => {
console.log(`
๐ ํ๋ก์ ์๋ฒ๊ฐ ์์๋์์ต๋๋ค!
๐ ํฌํธ: ${PORT}
๐ ํ๊ฒฝ: ${process.env.NODE_ENV || 'development'}
๐ฏ ํ๊ฒ: ${process.env.API_TARGET || 'https://api.example.com'}
`);
});
// ==================== Graceful Shutdown ====================
process.on('SIGTERM', () => {
console.log('SIGTERM ์ ํธ ์์ , ์๋ฒ ์ข
๋ฃ ์ค...');
server.close(() => {
console.log('์๋ฒ๊ฐ ์ ์์ ์ผ๋ก ์ข
๋ฃ๋์์ต๋๋ค');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT ์ ํธ ์์ , ์๋ฒ ์ข
๋ฃ ์ค...');
server.close(() => {
console.log('์๋ฒ๊ฐ ์ ์์ ์ผ๋ก ์ข
๋ฃ๋์์ต๋๋ค');
process.exit(0);
});
});
module.exports = app; // ํ
์คํธ๋ฅผ ์ํด export
ํ๊ฒฝ ๋ณ์ ์ค์ (.env):
# .env
NODE_ENV=production
PORT=5000
API_TARGET=https://api.example.com
API_KEY=your_secret_api_key_here
ADMIN_TOKEN=your_admin_token_here
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
๐งช ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
ํ๋ก์ ์๋ฒ๋ ํ ์คํธ๊ฐ ํ์ํด! Jest์ Supertest๋ก ํ ์คํธ๋ฅผ ์์ฑํด๋ณด์. ๐งช
// proxy-server.test.js
const request = require('supertest');
const app = require('./production-proxy-server');
describe('ํ๋ก์ ์๋ฒ ํ
์คํธ', () => {
test('ํฌ์ค์ฒดํฌ ์๋ํฌ์ธํธ', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('status', 'healthy');
expect(response.body).toHaveProperty('uptime');
});
test('์กด์ฌํ์ง ์๋ ๊ฒฝ๋ก 404', async () => {
const response = await request(app).get('/nonexistent');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error');
});
test('Rate Limiting ํ
์คํธ', async () => {
// 100๋ฒ ์ด์ ์์ฒญ
const requests = Array(101).fill().map(() =>
request(app).get('/api/test')
);
const responses = await Promise.all(requests);
const tooManyRequests = responses.filter(r => r.status === 429);
expect(tooManyRequests.length).toBeGreaterThan(0);
});
test('์บ์ฑ ๋์ ํ์ธ', async () => {
const path = '/api/users';
// ์ฒซ ๋ฒ์งธ ์์ฒญ
const response1 = await request(app).get(path);
// ๋ ๋ฒ์งธ ์์ฒญ (์บ์์์ ๊ฐ์ ธ์์ผ ํจ)
const response2 = await request(app).get(path);
expect(response1.body).toEqual(response2.body);
});
});
๐ฏ ์ค์ ์ฌ์ฉ ์ฌ๋ก๋ค
ํ๋ก์ ์๋ฒ๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ํ์ฉ๋๋์ง ๊ตฌ์ฒด์ ์ธ ์์๋ฅผ ์ดํด๋ณด์! ๐
์ฌ๋ก 1: ๋ ์จ API ํ๋ก์
OpenWeatherMap API๋ฅผ ํ๋ก์๋ก ๊ฐ์ธ์ API ํค๋ฅผ ์จ๊ธฐ๊ณ ์บ์ฑ ์ ์ฉ
// weather-proxy.js
app.get('/weather/:city', async (req, res) => {
const { city } = req.params;
const cacheKey = `weather_${city}`;
// ์บ์ ํ์ธ (10๋ถ)
const cached = cache.get(cacheKey);
if (cached) {
return res.json({ ...cached, cached: true });
}
try {
const apiKey = process.env.OPENWEATHER_API_KEY;
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
const response = await fetch(url);
const data = await response.json();
// ํ์ํ ๋ฐ์ดํฐ๋ง ์ถ์ถ
const weatherData = {
city: data.name,
temperature: data.main.temp,
description: data.weather[0].description,
humidity: data.main.humidity,
timestamp: new Date().toISOString()
};
cache.set(cacheKey, weatherData, 600); // 10๋ถ ์บ์ฑ
res.json(weatherData);
} catch (error) {
res.status(500).json({ error: '๋ ์จ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค' });
}
});
// ํด๋ผ์ด์ธํธ ์ฌ์ฉ
fetch('http://localhost:5000/weather/Seoul')
.then(res => res.json())
.then(data => console.log(data));
์ฌ๋ก 2: ์ด๋ฏธ์ง ํ๋ก์ & ๋ฆฌ์ฌ์ด์ง
์ธ๋ถ ์ด๋ฏธ์ง๋ฅผ ํ๋ก์ํ๋ฉด์ ๋ฆฌ์ฌ์ด์ง๊น์ง ์ฒ๋ฆฌ
// image-proxy.js
const sharp = require('sharp'); // npm install sharp
app.get('/image', async (req, res) => {
const { url, width, height } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL์ด ํ์ํฉ๋๋ค' });
}
try {
const response = await fetch(url);
const buffer = await response.buffer();
let image = sharp(buffer);
// ๋ฆฌ์ฌ์ด์ง ์ต์
if (width || height) {
image = image.resize(
width ? parseInt(width) : null,
height ? parseInt(height) : null,
{ fit: 'inside' }
);
}
// WebP๋ก ๋ณํ (์ฉ๋ ์ ์ฝ)
const optimized = await image.webp({ quality: 80 }).toBuffer();
res.set('Content-Type', 'image/webp');
res.set('Cache-Control', 'public, max-age=86400'); // 1์ผ ์บ์ฑ
res.send(optimized);
} catch (error) {
res.status(500).json({ error: '์ด๋ฏธ์ง ์ฒ๋ฆฌ ์คํจ' });
}
});
// ์ฌ์ฉ ์์
// <img src="http://localhost:5000/image?url=https://example.com/photo.jpg&width=300" />
์ฌ๋ก 3: GraphQL API ํ๋ก์
GraphQL ์ฟผ๋ฆฌ๋ฅผ ํ๋ก์ํ๋ฉด์ ์ธ์ฆ ์ฒ๋ฆฌ
// graphql-proxy.js
app.post('/graphql', async (req, res) => {
const { query, variables } = req.body;
try {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.GRAPHQL_TOKEN}`
},
body: JSON.stringify({ query, variables })
});
const data = await response.json();
// ์๋ฌ ๋ก๊น
if (data.errors) {
console.error('GraphQL ์๋ฌ:', data.errors);
}
res.json(data);
} catch (error) {
res.status(500).json({
errors: [{ message: 'GraphQL ์์ฒญ ์คํจ' }]
});
}
});
๐ ๋ณด์ ๊ฐํ ์ถ๊ฐ ํ
ํ๋ก์ ์๋ฒ์ ๋ณด์์ ๋์ฑ ๊ฐํํ๋ ๋ฐฉ๋ฒ๋ค์ด์ผ! ๐ก๏ธ
๐ JWT ์ธ์ฆ
const jwt = require('jsonwebtoken');
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'ํ ํฐ์ด ํ์ํฉ๋๋ค' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: '์ ํจํ์ง ์์ ํ ํฐ' });
}
};
app.use('/api', verifyToken);
๐ซ IP ํ์ดํธ๋ฆฌ์คํธ
const allowedIPs = process.env.ALLOWED_IPS?.split(',') || [];
const ipFilter = (req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!allowedIPs.includes(clientIP)) {
console.log(`์ฐจ๋จ๋ IP: ${clientIP}`);
return res.status(403).json({ error: '์ ๊ทผ ๊ฑฐ๋ถ' });
}
next();
};
app.use('/api', ipFilter);
๐ API ํค ๊ฒ์ฆ
const validateApiKey = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.CLIENT_API_KEY) {
return res.status(401).json({ error: 'API ํค๊ฐ ์ ํจํ์ง ์์ต๋๋ค' });
}
next();
};
app.use('/api', validateApiKey);
๐ก๏ธ XSS ๋ฐฉ์ง
const xss = require('xss-clean');
const mongoSanitize = require('express-mongo-sanitize');
app.use(xss()); // XSS ๊ณต๊ฒฉ ๋ฐฉ์ง
app.use(mongoSanitize()); // NoSQL Injection ๋ฐฉ์ง
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๐
CORS๊ฐ ๋ญ์ง๋ถํฐ ์์ํด์ ํ๋ก์ ์๋ฒ๋ฅผ ๋ง๋ค๊ณ , ๋ฐฐํฌํ๊ณ , ์ต์ ํํ๋ ๋ฐฉ๋ฒ๊น์ง ๋ชจ๋ ๋ค๋ค์ด!
ํต์ฌ๋ง ๋ค์ ์ ๋ฆฌํ๋ฉด:
๐ฏ ํต์ฌ ์์ฝ
1. CORS๋ ๋ธ๋ผ์ฐ์ ๋ณด์ ์ ์ฑ
๋ค๋ฅธ ์ถ์ฒ์ ๋ฆฌ์์ค ์ ๊ทผ์ ์ ํํ๋ ๋ฉ์ปค๋์ฆ์ด์ผ.
2. ํ๋ก์ ์๋ฒ๋ ์ค๊ฐ์ ์ญํ
๋ธ๋ผ์ฐ์ ์ ์ธ๋ถ API ์ฌ์ด์์ ์์ฒญ์ ๋์ ์ ๋ฌํด์ค.
3. ๋ค์ํ ๊ตฌํ ๋ฐฉ๋ฒ
http-proxy-middleware, ์ง์ ๊ตฌํ, CRA ํ๋ก์, ์๋ฒ๋ฆฌ์ค ๋ฑ
4. ์ถ๊ฐ ๊ธฐ๋ฅ์ผ๋ก ๊ฐ์น ์์น
์บ์ฑ, Rate Limiting, ์ธ์ฆ, ๋ก๊น
๋ฑ์ ์ถ๊ฐํ๋ฉด ๋ ๊ฐ๋ ฅํด์ ธ.
5. ํ๋ก๋์
๋ฐฐํฌ ๊ณ ๋ ค์ฌํญ
๋ณด์, ์ฑ๋ฅ, ๋ชจ๋ํฐ๋ง, ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ผผ๊ผผํ ์ฑ๊ฒจ์ผ ํด.
ํ๋ก์ ์๋ฒ๋ ๋จ์ํ CORS ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ ์ด์์ ๊ฐ์น๊ฐ ์์ด. API ํค ๋ณดํธ, ์ฑ๋ฅ ์ต์ ํ, ์์ฒญ ์ ์ด ๋ฑ ๋ค์ํ ์ด์ ์ ์ ๊ณตํ์ง. ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์น ๊ฐ๋ฐ ํ๋ก์ ํธ๋ฅผ ์งํํ ๋, ์ด๋ฐ ๊ธฐ์ ์ ํ์ฉํ๋ฉด ๋ ์์ ํ๊ณ ํจ์จ์ ์ธ ์๋น์ค๋ฅผ ๋ง๋ค ์ ์์ด! ๐ช
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ๋ฐ๋ผ ํ๋ค ๋ณด๋ฉด ๊ธ๋ฐฉ ์ต์ํด์ง ๊ฑฐ์ผ. ์ด ๊ธ์์ ๋ค๋ฃฌ ์ฝ๋๋ค์ ๋ชจ๋ ์ค์ ์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ ๊ฒ์ฆ๋ ๋ฐฉ๋ฒ๋ค์ด๋๊น, ์์ ์๊ฒ ํ๋ก์ ํธ์ ์ ์ฉํด๋ด! ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด, ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๋ ์ฌ๋ฅ๋ท์ ์ ๋ฌธ๊ฐ๋ค์๊ฒ ๋์์ ์์ฒญํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํจ๊ป ์ฑ์ฅํ๋ ๊ฒ ๊ฐ๋ฐ์์ ๋ฉ์ง ๋ฌธํ๋๊น! ๐
๐ ๋ ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด?
โ
MDN Web Docs - CORS ๊ณต์ ๋ฌธ์
โ
Node.js ๊ณต์ ๋ฌธ์ - HTTP ๋ชจ๋
โ
Express.js ๋ฏธ๋ค์จ์ด ๊ฐ์ด๋
โ
Docker & Kubernetes ํ์ต
โ
AWS/GCP/Azure ํด๋ผ์ฐ๋ ์๋น์ค
โ
๋ณด์ ๋ฒ ์คํธ ํ๋ํฐ์ค (OWASP)
์, ์ด์ ๋๋ CORS ๋ง์คํฐ์ผ! ๐ ์์ ๊ฐ์ ๊ฐ์ง๊ณ ๋ฉ์ง ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค์ด๋ด. ํ์ดํ ! ๐ชโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.