CORS(Cross-Origin Resource Sharing)란 무엇이며 어떻게 해결하나요?
힌트
Same-Origin Policy와 Preflight 요청을 생각해보세요.
정답 및 해설
CORS(Cross-Origin Resource Sharing)란 무엇이며 어떻게 해결하나요?
CORS는 브라우저의 **동일 출처 정책(Same-Origin Policy)**을 안전하게 완화하는 HTTP 헤더 기반 메커니즘입니다. 다른 출처(origin)의 리소스를 웹 페이지에서 요청할 수 있도록 서버가 명시적으로 허용하는 방식입니다.
Same-Origin Policy (동일 출처 정책)란?
브라우저는 보안을 위해 같은 출처(origin)에서 온 리소스만 기본적으로 접근할 수 있게 제한합니다. 출처(origin)는 프로토콜 + 호스트 + 포트 의 조합입니다.
https://example.com:443/page
프로토콜: https
호스트: example.com
포트: 443
동일 출처 / 다른 출처 판별
기준 URL: https://example.com/page
https://example.com/other -- 동일 출처 (경로만 다름)
https://example.com:8080/page -- 다른 출처 (포트 다름)
http://example.com/page -- 다른 출처 (프로토콜 다름)
https://api.example.com/page -- 다른 출처 (서브도메인 다름)
https://other.com/page -- 다른 출처 (호스트 다름)
Same-Origin Policy가 없다면?
// 악성 사이트 evil.com에서의 코드
// 사용자가 bank.com에 로그인된 상태라면
fetch('https://bank.com/api/transfer', {
method: 'POST',
body: JSON.stringify({ to: 'hacker', amount: 1000000 }),
credentials: 'include', // 쿠키(세션) 포함
});
// Same-Origin Policy가 없으면 이런 요청이 성공해버림!
SOP는 이런 CSRF(Cross-Site Request Forgery) 계열 공격을 방어합니다.
CORS란?
CORS는 서버가 HTTP 헤더를 통해 "이 출처에서의 요청을 허용한다"고 브라우저에 알려주는 메커니즘입니다. CORS 정책은 브라우저에서 강제되며, 서버 간 통신(Node.js에서 fetch 등)에는 적용되지 않습니다.
Preflight 요청
브라우저는 일부 요청 전에 실제 요청을 보내기 전 OPTIONS 메서드로 Preflight(사전 확인) 요청을 먼저 보냅니다.
단순 요청 (Simple Request) — Preflight 없음
다음 조건을 모두 만족하는 경우에만 Preflight 없이 바로 전송됩니다.
메서드: GET, POST, HEAD 중 하나
헤더: Accept, Content-Type(application/x-www-form-urlencoded, multipart/form-data, text/plain 만), ...
Preflight가 발생하는 경우
메서드: PUT, DELETE, PATCH 등
헤더: Authorization, Content-Type: application/json 등 커스텀 헤더 포함
-- Preflight 요청 --
OPTIONS /api/users HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
-- 서버의 Preflight 응답 --
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 -- 24시간 동안 Preflight 캐시
Preflight가 성공하면 브라우저는 실제 요청을 보냅니다.
-- 실제 요청 --
POST /api/users HTTP/1.1
Origin: https://frontend.com
Authorization: Bearer token123
Content-Type: application/json
{"name": "홍길동", "email": "hong@example.com"}
-- 서버 응답 --
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://frontend.com
Content-Type: application/json
{"id": 1, "name": "홍길동"}
CORS 해결 방법
1. 서버에서 CORS 헤더 설정 (가장 기본적인 방법)
Express.js (Node.js)
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 출처 허용 (개발 환경에서만!)
app.use(cors());
// 특정 출처만 허용
app.use(cors({
origin: 'https://myfrontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true, // 쿠키/인증 헤더 허용
}));
// 여러 출처 허용
const allowedOrigins = [
'https://myfrontend.com',
'https://staging.myfrontend.com',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
}));
수동으로 헤더 설정
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://myfrontend.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
res.header('Access-Control-Allow-Credentials', 'true');
// Preflight 요청 처리
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
2. 프록시 서버 사용
CORS는 브라우저에서만 적용됩니다. 프론트엔드 서버에서 프록시하면 브라우저 관점에서는 동일 출처 요청이 됩니다.
Next.js에서 API 라우트로 프록시
// pages/api/proxy/[...path].ts 또는 app/api/proxy/[...path]/route.ts
// App Router 방식
export async function GET(request: Request, { params }: { params: { path: string[] } }) {
const path = params.path.join('/');
const url = `https://external-api.com/${path}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.API_SECRET}`,
},
});
const data = await response.json();
return Response.json(data);
}
// 클라이언트에서는 /api/proxy/users 로 요청
// → Next.js 서버가 external-api.com/users 로 프록시
Vite 개발 서버 프록시
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.backend.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
3. Next.js의 rewrites/headers 설정
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://external-api.com/:path*',
},
];
},
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Authorization, Content-Type' },
],
},
];
},
};
자격 증명(Credentials)과 CORS
쿠키나 Authorization 헤더를 포함한 요청은 특별한 설정이 필요합니다.
// 클라이언트 — credentials 포함 요청
fetch('https://api.example.com/user', {
credentials: 'include', // 쿠키 포함
});
// 또는 axios
axios.get('https://api.example.com/user', {
withCredentials: true,
});
// 서버 — credentials를 허용할 때 wildcard(*) 사용 불가
// 잘못된 설정:
res.header('Access-Control-Allow-Origin', '*'); // X
res.header('Access-Control-Allow-Credentials', 'true'); // X — 이 조합은 오류
// 올바른 설정:
res.header('Access-Control-Allow-Origin', 'https://myfrontend.com'); // 특정 출처
res.header('Access-Control-Allow-Credentials', 'true');
CORS 오류 디버깅
-- 콘솔 오류 메시지 --
Access to fetch at 'https://api.example.com/data' from origin 'https://myfrontend.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
-- 확인 사항 --
1. Network 탭에서 Preflight(OPTIONS) 요청 확인
2. 응답 헤더에 Access-Control-Allow-Origin 존재 여부
3. 요청 Origin과 허용된 Origin 일치 여부
4. credentials 사용 시 wildcard(*) 설정 여부
정리
| 상황 | 해결 방법 |
|---|---|
| 자체 API 서버 있음 | 서버에 CORS 헤더 설정 |
| 외부 API 직접 호출 | 프록시 서버 사용 |
| Next.js 프로젝트 | API Routes 프록시 또는 rewrites |
| 개발 환경 | Vite/webpack-dev-server 프록시 |
| 브라우저 없는 서버 간 통신 | CORS 불필요 (SOP는 브라우저 정책) |
CORS는 서버가 아닌 브라우저가 강제하는 정책입니다. 따라서 근본적인 해결책은 항상 서버에서 적절한 헤더를 설정하는 것이며, 프록시는 이를 우회하는 방법입니다.