CORS (Cross-Origin Resource Sharing)
중요도: ⭐⭐⭐⭐⭐
웹 개발에서 반드시 이해해야 하는 보안 정책입니다.
개념
CORS(Cross-Origin Resource Sharing)는 웹 페이지가 다른 도메인, 프로토콜, 포트의 리소스에 접근할 수 있도록 허용하는 메커니즘입니다.
동일 출처 정책 (Same-Origin Policy)
보안상의 이유로 웹 브라우저는 동일한 출처의 리소스만 접근을 허용합니다. 출처는 프로토콜 + 도메인 + 포트가 모두 같아야 합니다.
출처 비교 예시
// 기준 URL: https://example.com:443/page
// ✅ 같은 출처
https://example.com:443/api/users
https://example.com/other-page
// ❌ 다른 출처
http://example.com/api/users // 프로토콜 다름 (https vs http)
https://api.example.com/users // 서브도메인 다름
https://example.com:8080/api/users // 포트 다름
https://other.com/api/users // 도메인 다름브라우저에서 출처 확인
// 현재 페이지의 출처 확인
console.log(window.location.origin); // "https://example.com"
// URL 파싱하여 출처 구성 요소 확인
const url = new URL("https://api.example.com:8080/users");
console.log(url.protocol); // "https:"
console.log(url.hostname); // "api.example.com"
console.log(url.port); // "8080"
console.log(url.origin); // "https://api.example.com:8080"CORS 동작 방식
1. Simple Request (단순 요청)
다음 조건을 모두 만족하는 요청:
- 메서드: GET, HEAD, POST 중 하나
- 헤더: Accept, Accept-Language, Content-Language, Content-Type만 허용
- Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용
// Simple Request 예시
fetch("https://api.example.com/users", {
method: "GET",
headers: {
"Content-Type": "text/plain",
},
});
// 서버 응답에 CORS 헤더가 포함되어야 함
// Access-Control-Allow-Origin: https://mysite.com2. Preflight Request (사전 요청)
Simple Request 조건을 만족하지 않는 경우, 브라우저가 실제 요청 전에 OPTIONS 메서드로 사전 요청을 보냅니다.
// Preflight가 필요한 요청 예시
fetch("https://api.example.com/users", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token123",
},
body: JSON.stringify({ name: "John" }),
});Preflight 요청/응답 과정
# 1. 브라우저가 보내는 Preflight 요청
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://mysite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# 2. 서버의 Preflight 응답
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# 3. 실제 요청 (Preflight 성공 시)
PUT /users HTTP/1.1
Host: api.example.com
Origin: https://mysite.com
Content-Type: application/json
Authorization: Bearer token123CORS 헤더 설명
서버 응답 헤더
Access-Control-Allow-Origin
// 특정 출처 허용
res.setHeader("Access-Control-Allow-Origin", "https://mysite.com");
// 모든 출처 허용 (보안상 위험)
res.setHeader("Access-Control-Allow-Origin", "*");
// 동적으로 출처 허용
const allowedOrigins = ["https://mysite.com", "https://myapp.com"];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}Access-Control-Allow-Methods
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);Access-Control-Allow-Headers
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With",
);Access-Control-Allow-Credentials
// 쿠키나 인증 정보 포함 요청 허용
res.setHeader("Access-Control-Allow-Credentials", "true");
// 클라이언트에서는 credentials: 'include' 필요
fetch("https://api.example.com/users", {
credentials: "include",
});Access-Control-Max-Age
// Preflight 결과를 86400초(24시간) 동안 캐시
res.setHeader("Access-Control-Max-Age", "86400");CORS 해결 방법
1. 서버에서 CORS 헤더 설정
Node.js (Express)
// 기본 CORS 미들웨어
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization",
);
// Preflight 요청 처리
if (req.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
// cors 패키지 사용 (권장)
const cors = require("cors");
// 모든 출처 허용
app.use(cors());
// 특정 설정
app.use(
cors({
origin: ["https://mysite.com", "https://myapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);
// 라우트별 설정
app.get("/api/public", cors(), (req, res) => {
res.json({ message: "Public endpoint" });
});
app.post(
"/api/private",
cors({
origin: "https://mysite.com",
credentials: true,
}),
(req, res) => {
res.json({ message: "Private endpoint" });
},
);Spring Boot (Java)
@CrossOrigin(origins = "https://mysite.com", maxAge = 3600)
@RestController
public class UserController {
@GetMapping("/users")
public List<User> getUsers() {
return userService.getAllUsers();
}
}
// 글로벌 CORS 설정
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://mysite.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}PHP
<?php
header('Access-Control-Allow-Origin: https://mysite.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
// Preflight 요청 처리
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
?>2. 프록시 서버 사용
Webpack Dev Server
// webpack.config.js
module.exports = {
devServer: {
proxy: {
"/api": {
target: "https://api.example.com",
changeOrigin: true,
pathRewrite: {
"^/api": "",
},
},
},
},
};
// 사용법
// 기존: fetch('https://api.example.com/users') - CORS 에러
// 변경: fetch('/api/users') - 프록시를 통해 우회Vite
// vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "https://api.example.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
};Next.js
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
};3. JSONP 방식 (레거시)
JSONP는 GET 요청만 가능하고 보안상 취약하므로 현재는 권장하지 않습니다.
// JSONP 요청
function jsonpRequest(url, callbackName) {
const script = document.createElement("script");
script.src = `${url}?callback=${callbackName}`;
document.head.appendChild(script);
}
// 콜백 함수 정의
window.handleResponse = function (data) {
console.log(data);
// 사용 후 script 태그 제거
document.head.removeChild(
document.querySelector('script[src*="callback=handleResponse"]'),
);
};
// JSONP 요청 실행
jsonpRequest("https://api.example.com/users", "handleResponse");
// 서버에서는 콜백 함수로 감싸서 응답
// handleResponse({"users": [...]})4. 개발 환경에서의 임시 해결책
# Chrome 브라우저를 CORS 비활성화 모드로 실행
google-chrome --disable-web-security --user-data-dir="/tmp/chrome_dev"
# CORS 우회 브라우저 확장 프로그램
# - CORS Unblock
# - Allow CORS실제 개발 시나리오
SPA에서 API 호출
// React 앱에서 외부 API 호출
const fetchUsers = async () => {
try {
const response = await fetch("https://api.external.com/users", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
credentials: "include",
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
if (error.name === "TypeError" && error.message.includes("CORS")) {
console.error("CORS error - check server CORS configuration");
}
throw error;
}
};에러 처리 및 디버깅
// CORS 에러 감지
const handleCorsError = (error) => {
if (
error instanceof TypeError &&
(error.message.includes("CORS") ||
error.message.includes("Network request failed"))
) {
console.error("CORS Error Detected!");
console.log("Possible solutions:");
console.log("1. Configure CORS headers on the server");
console.log("2. Use a proxy server");
console.log("3. Make sure the request is from the correct origin");
// 사용자에게 친화적인 에러 메시지 표시
showUserError("서버 연결에 문제가 있습니다. 잠시 후 다시 시도해주세요.");
}
};면접 팁
CORS 관련 질문에서는 단순히 해결 방법만 나열하지 말고, 왜 이런 정책이 존재하는지(보안상 이유), 어떤 상황에서 발생하는지, 그리고 각 해결 방법의 장단점을 함께 설명할 수 있어야 합니다.
Edit on GitHub
Last updated on