CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 "다른 오리진(Origin)"에 있는 리소스에 접근하려고 할 때 보안상 제한을 두는 메커니즘이다.
프로토콜과 호스트 이름, 포트의 조합을 말한다.
예를 들어 https://test.com/test1 라는 주소에서 오리진은 https://test.com 을 뜻한다.
오리진은 다음 세 가지가 모두 동일해야 같은 오리진으로 간주한다.
| 구성 요소 | 예시 |
|---|---|
| 프로토콜 | http:// or https:// |
| 도메인(host) | example.com, localhost, 127.0.0.1 |
| 포트 번호 | :3000, :8080, :5000 |
즉,
http://localhost:3000 과 http://localhost:5000 은 다른 오리진이다.
-> 프로토콜과 도메인은 같지만 포트가 다르다
CORS는 보안상의 이유로 존재한다.
만약 아무 웹사이트에서든 임의의 API 서버로 요청을 보낼 수 있다면, 악의적인 사이트가 사용자의 쿠키나 인증 토큰을 몰래 이용해서 API 호출을 할 수도 있다.
그래서 브라우저는 다른 오리진의 리소스를 요청할 때, 서버가 명시적으로 허용(CORS Header)하지 않으면 요청을 차단한다.
프론트엔드에서 fetch('http://localhost:5000/api/data') 를 실행했을때 다음 과정을 거친다.
브라우저는 먼저 OPTIONS 요청을 보내서 서버에게 "이 요청을 해도 괜찮은가?" 묻는다
서버가 다음과 같은 헤더를 포함해 응답하면 요청을 허용한다.
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
그러면 실제 요청이 이어서 수행된다.
브라우저가 보안상 이유로 응답을 차단한다.
콘솔 메시지
```Access to fetch at 'http://localhost:5000/api/data' from origin 'http://localhost:3000'
has been blocked by CORS policy...
```
localhost:3000localhost:8080 또는 localhost:5000다른 포트에서 돌아가기 때문에 브라우저 입장에서는 다른 오리진으로 인식되어 CORS 에러가 발생한다.
Express.js
const cors = require('cors');
app.use(cors({ origin: 'http://localhost:3000' }));
Spring Boot
@CrossOrigin(origins = "http://localhost:3000")
서버가 응답 헤더에 Access-Control-Allow-Origin을 추가하도록 설정
개발 중이라 백엔드를 직접 수정할 수 없거나, 서버 설정이 귀찮을 때 자주 사용한다.
원리
프록시 서버가 브라우저로부터 같은 오리진으로 보이는 요청을 받은 뒤, 내부적으로 백엔드 서버로 요청을 전달한다.
브라우저는 이 과정을 인지하지 못하기 때문에 CORS 제한이 발생하지 않는다
예시 (React CRA)
// package.json
"proxy": "http://localhost:5000"
// 이후 요청
fetch('/api/data')
// 실제로 전달
프론트엔드(3000) → 프록시 → 백엔드(5000)
브라우저는 localhost:3000으로 요청했다고 생각하기 때문에 CORS 에러가 안 난다.
React, Vue, Vite, Next.js 등에서도 dev 서버 설정으로 프록시를 걸 수 있다.
Nginx 예시
location /api {
proxy_pass http://localhost:5000;
}
스프링 부트 + html + javascript 로만 이루어진 프로젝트를 배포했는데 CORS 문제가 생겨 이번에 CORS에 관해 알아보았다.
“스프링 부트 + HTML + JS로만 구성된 단일 프로젝트인데도 CORS 문제가 발생했다”
는 건, 프론트엔드(JS 코드)가 요청하는 대상이 브라우저 입장에서 “다른 오리진”으로 인식되었기 때문이었다.
/resources/static에 HTML/JS를 두고, REST API를 @RestController로 구현했다.
서버 안에서는 한 덩어리로 보이지만 브라우저 입장에서는 요청 주소가 다르면 다른 오리진이다.
| HTML을 연 주소 | JS가 요청하는 API 주소 | 오리진 일치 여부 | 결과 |
|---|---|---|---|
http://localhost:8080/index.html | http://localhost:8080/api/test | ✅ 같음 | CORS 문제 없음 |
http://127.0.0.1:8080/index.html | http://localhost:8080/api/test | ❌ 다름(도메인 다름) | CORS 에러 발생 |
https://myapp.com | https://api.myapp.com | ❌ 다름(서브도메인 다름) | CORS 에러 발생 |
https://myapp.com | http://myapp.com | ❌ 다름(프로토콜 다름) | CORS 에러 발생 |
즉, 한 스프링 프로젝트에서 배포했다 해도, 실제 서비스 환경에서 JS가 다른 오리진으로 요청하면 브라우저는 CORS를 검사한다.
// 전역 설정 (Spring Security나 MVC Config)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://www.myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
// Controller 단위 설정
@CrossOrigin(origins = "https://www.myapp.com")
@RestController
public class MyController {
// ...
}