CORS란 무엇인가? 개념부터 해결 방법까지 완벽 정리 (feat. 트러블슈팅)
웹 개발을 하다 보면 누구나 한 번쯤은 콘솔 창에서 이 빨간색 에러 메시지를 마주하게 됩니다.
Access to fetch at 'https://api.example.com/data' from origin 'https://my-app.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
"아니, 내 서버에서 내 데이터를 가져오겠다는데 왜 막는 거야?"라며 답답해하신 적 있으시죠? 😅
이 에러의 주인공은 바로 **CORS (Cross-Origin Resource Sharing)**입니다. 브라우저 보안의 핵심이지만, 개발자들에게는 가장 골치 아픈 존재이기도 하죠. 오늘은 CORS가 도대체 무엇인지, 왜 필요한지, 그리고 어떻게 해결해야 하는지 아주 깊게 파헤쳐 보겠습니다.
CORS가 뭔가요?
**CORS(Cross-Origin Resource Sharing)**는 한국어로 '교차 출처 리소스 공유'라고 합니다. 쉽게 말해, 서로 다른 출처(Origin) 간에 리소스를 공유할 수 있도록 허용하는 정책을 말해요.
여기서 잠깐! **출처(Origin)**가 뭘까요?
URL의 Protocol, Host, Port 이 세 가지가 모두 같아야 같은 출처라고 봅니다.
https://www.google.comvshttps://www.google.com:80(포트 다름) -> 다른 출처http://api.google.comvshttps://api.google.com(프로토콜 다름) -> 다른 출처https://google.comvshttps://api.google.com(호스트 다름) -> 다른 출처
SOP (Same-Origin Policy)
사실 브라우저는 기본적으로 **SOP (Same-Origin Policy, 동일 출처 정책)**를 따릅니다. 보안상의 이유로 스크립트에서 다른 출처의 리소스에 접근하는 것을 막는 것이죠.
예를 들어, 제가 만든 악성 사이트 evil.com에 접속했는데, 거기 심어둔 스크립트가 여러분이 로그인한 bank.com에 몰래 요청을 보내서 돈을 빼가면 안 되잖아요? 그래서 브라우저는 기본적으로 다른 출처로의 요청을 차단합니다.
하지만 요즘은 프론트엔드(localhost:3000)와 백엔드(localhost:8080)를 분리해서 개발하는 경우가 많죠? 필연적으로 다른 출처 간의 통신이 필요해집니다. 이때 "안전한 요청이니까 허락해 줘!"라고 브라우저에게 알려주는 메커니즘이 바로 CORS입니다.
CORS 동작 원리: 어떻게 허락을 받나요?
CORS는 HTTP 헤더를 통해 동작합니다. 브라우저와 서버가 서로 "이 요청 괜찮아?" "응, 괜찮아!" 하고 대화를 나누는 과정이라고 보시면 돼요.
1. 단순 요청 (Simple Request)
가장 간단한 경우입니다. 예비 요청(Preflight) 없이 바로 본 요청을 보냅니다.
- 메서드가
GET,HEAD,POST중 하나여야 하고, - 헤더가
Accept,Content-Type등 기본적인 것만 포함되어야 합니다.
브라우저가 요청을 보낼 때 Origin 헤더를 붙여서 보냅니다.
GET /api/data HTTP/1.1 Origin: https://my-app.com
서버가 허용한다면 응답 헤더에 Access-Control-Allow-Origin을 실어서 보내줍니다.
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://my-app.com
2. 예비 요청 (Preflight Request)
대부분의 API 요청은 여기에 해당합니다. PUT, DELETE 메서드를 쓰거나 Authorization, Content-Type: application/json 같은 커스텀 헤더를 쓸 때 발생합니다.
브라우저는 본 요청을 보내기 전에 "나 이거 보내도 돼?" 하고 OPTIONS 메서드로 먼저 찔러봅니다. 이걸 Preflight라고 해요.
브라우저 (Preflight):
OPTIONS /api/data HTTP/1.1 Origin: https://my-app.com Access-Control-Request-Method: POST Access-Control-Request-Headers: content-type
서버 (응답):
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://my-app.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: content-type
서버가 "OK, 허용할게"라고 응답하면, 그제서야 브라우저는 진짜 요청을 보냅니다.
자주 겪는 CORS 에러와 해결 방법 (Troubleshooting)
에러 1: "No 'Access-Control-Allow-Origin' header is present"
가장 흔한 에러입니다. 서버가 응답할 때 Access-Control-Allow-Origin 헤더를 안 보내줬거나, 허용된 출처 목록에 내 프론트엔드 주소가 없는 경우입니다.
해결책:
백엔드 설정을 바꿔야 합니다. 프론트엔드에서 해결할 수 있는 문제가 아니에요! (물론 프록시를 쓰면 되지만, 근본적인 해결책은 서버 설정입니다.)
- Node.js (Express):
cors미들웨어를 사용하세요.const cors = require('cors'); app.use(cors({ origin: 'https://my-app.com', // 특정 도메인만 허용 credentials: true, })); - Spring Boot:
@CrossOrigin어노테이션이나WebMvcConfigurer설정을 사용하세요.
에러 2: Wildcard '*' 사용 불가 에러
withCredentials: true (쿠키나 인증 정보를 포함해서 요청) 옵션을 켰는데, 서버가 Access-Control-Allow-Origin: * (모두 허용)으로 설정되어 있으면 에러가 납니다. 보안상 인증 정보를 주고받을 때는 출처를 명확히 해야 하기 때문이죠.
해결책:
서버에서 * 대신 정확한 출처(https://my-app.com)를 명시해 줘야 합니다.
에러 3: 로컬 개발 환경에서 CORS가 뜰 때
개발할 때마다 백엔드 개발자분께 "CORS 열어주세요" 하기 번거롭죠?
해결책:
**프록시(Proxy)**를 사용하면 됩니다.
React(CRA)나 Vite 같은 도구들은 개발 서버에 프록시 기능이 내장되어 있습니다. 브라우저는 프론트엔드 개발 서버(localhost:3000)로 요청을 보내고, 개발 서버가 백엔드(localhost:8080)로 요청을 대신 전달해 주는 방식입니다. 서버 간 통신에는 CORS가 적용되지 않거든요!
// vite.config.js 예시 export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, };
마치며
CORS는 우리를 괴롭히려고 만든 게 아니라, 사용자의 정보를 보호하기 위한 중요한 안전장치입니다. 원리를 이해하고 나면 에러 메시지가 더 이상 무섭지 않을 거예요.
오늘 정리한 내용이 여러분의 삽질 시간을 조금이라도 줄여드렸기를 바랍니다! 🚀
혹시 개발하시다가 JSON 데이터 포맷팅이나 검증이 필요하다면, Pockit의 JSON Formatter & Validator 도구를 활용해 보세요. API 응답 데이터를 분석할 때 정말 유용하답니다.
그럼 오늘도 즐거운 코딩 하세요! Happy Coding! 😎
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요