CORS 오류 해결하는 법 (왜 발생하는지 포함)
CORS 오류는 웹 페이지가 다른 오리진(다른 도메인, 포트, 프로토콜)의 응답을 읽으려 했는데, 그 서버가 이를 허용한다는 헤더를 보내지 않았다는 뜻이에요. 브라우저는 사용자를 보호하기 위해 그 응답을 차단해요. 해결책은 서버에서 여러분 사이트의 오리진에 대해 Access-Control-Allow-Origin 헤더를 반환하고 OPTIONS 프리플라이트 요청을 처리하는 것이고, 또는 자신의 백엔드를 통해 같은 오리진으로 요청을 보내 아예 피하는 것이에요.
요청이 서버에는 보통 잘 도달하기 때문에, 웹 개발에서 가장 헷갈리는 오류 중 하나예요. 이 가이드는 무슨 일이 일어나는지, 그리고 각 변형을 어떻게 고치는지 설명해요.
‘오리진’의 의미
오리진은 프로토콜, 도메인, 포트의 조합이에요.
| URL | 오리진 |
|---|---|
https://myapp.com/dashboard | https://myapp.com |
https://api.myapp.com/users | https://api.myapp.com (서브도메인이 달라서 다른 오리진) |
http://localhost:5173 | http://localhost:5173 |
http://localhost:3000 | http://localhost:3000 (포트가 달라서 다른 오리진) |
브라우저는 동일 출처 정책을 따라요. 페이지의 JavaScript는 자신과 같은 오리진의 응답은 자유롭게 읽을 수 있지만, 다른 오리진의 응답은 읽을 수 없어요. CORS(Cross-Origin Resource Sharing)는 서버가 옵트인하는 시스템이에요. “내 응답은 이 오리진들의 페이지가 읽어도 된다”고 선언하는 거죠.
사람들을 놀라게 하는 두 가지 결과가 있어요.
- 요청은 보통 서버에 도달해요. CORS는 대개 여러분의 코드가 응답을 읽는 것을 막을 뿐, 서버가 요청을 받는 것을 막지는 않아요. POST가 저장되었는데도 여러분은 오류를 볼 수 있어요.
- 브라우저만 이를 강제해요. curl, Postman, 서버 측 코드는 CORS를 완전히 무시해요. 그래서 “Postman에서는 된다”는 CORS에 대해 아무것도 알려 주지 않아요.
1단계: 정확한 오류 읽기
개발자 도구(우클릭 → 검사)를 열고 콘솔에서 전체 메시지를 읽으세요. Chrome의 문구가 어떤 문제인지 알려 줘요.
| 메시지에 포함된 내용 | 무엇이 잘못되었나 |
|---|---|
No 'Access-Control-Allow-Origin' header is present | 서버가 그 헤더를 아예 보내지 않았거나, 요청이 CORS 코드에 도달하기 전에 실패함(예: 404나 500) |
The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin | 서버가 여러분과 다른 오리진을 허용함 |
Response to preflight request doesn't pass access control check | OPTIONS 프리플라이트가 실패함 |
...must not be the wildcard '*' when the request's credentials mode is 'include' | 쿠키를 보내고 있는데 서버가 *로 응답함 |
Request header field authorization is not allowed | 서버가 Access-Control-Allow-Headers에 그 헤더를 나열하지 않음 |
그다음 네트워크 탭에서 실패한 요청(그리고 그 앞의 OPTIONS 요청)을 찾아, 상태 코드와 응답 헤더를 보세요.
상태 코드를 먼저 확인하세요. 많은 “CORS 오류”는 사실 변장한 서버 오류예요. 서버가 크래시하거나 404를 반환하면, 그 오류 응답에는 종종 CORS 헤더가 없어서 브라우저가 실제 문제 대신 CORS라고 보고해요. 500이나 404를 고치면 CORS 오류가 사라질 수도 있어요.
2단계: 그 API를 브라우저에서 호출해야 하는지 판단하기
헤더를 추가하기 전에 물어보세요. 브라우저가 이 API를 직접 호출해야 하나요?
- 비밀 키가 필요한 서드파티 API(OpenAI, Stripe 비밀 키, 대부분의 이메일 서비스)는 절대 브라우저에서 호출하면 안 돼요. 키가 모든 방문자에게 보이니까요. 대신 백엔드에서 호출하세요. 그러면 CORS 문제가 사라져요. 서버 간 요청은 CORS의 적용을 받지 않으니까요. API 키를 안전하게 보관하는 법을 참고하세요.
- 여러분이 통제하지 않고, CORS 헤더를 보내지 않는 서드파티 API는 여러분 쪽에서 고칠 수 없어요. 백엔드에서 호출하세요.
- 자신의 API라면 올바르게 설정해서 고칠 수 있어요. 아래에서 계속하세요.
3단계: 서버에서 고치기
서버는 오류 응답을 포함해 모든 응답에 올바른 헤더를 추가해야 해요.
실제 요청에는 다음을 보내세요.
Access-Control-Allow-Origin: https://myapp.com(프런트엔드의 정확한 오리진, 끝에 슬래시 없이)- 값이 요청한 오리진에 따라 달라진다면
Vary: Origin
프리플라이트 OPTIONS 요청에는 성공 상태(200 또는 204)와 함께 다음을 응답하세요.
Access-Control-Allow-Origin: https://myapp.comAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETEAccess-Control-Allow-Headers: Content-Type, Authorization(보내는 커스텀 헤더 전부)- 선택 사항으로
Access-Control-Max-Age: 600을 두면 브라우저가 프리플라이트를 일정 시간 캐시해요.
대부분의 프레임워크에는 이를 처리하는 패키지나 설정이 있어요. Express에서는 cors 패키지, FastAPI에서는 CORSMiddleware, ASP.NET Core에서는 AddCors, Spring에서는 @CrossOrigin이나 CORS 설정이에요. 일반적인 Express 설정 예시는 다음과 같아요.
import cors from "cors";
app.use(cors({
origin: ["https://myapp.com", "http://localhost:5173"],
credentials: true,
}));
CORS 처리는 라우트와 인증보다 앞에 두세요. 그래야 로그인 정보가 없는 프리플라이트 요청이 401로 거부되지 않아요.
AI 빌더를 쓴다면 정확하게 설명하세요.
https://myapp.com의 프런트엔드가https://api.myapp.com의 API를 호출하는데 “Response to preflight request doesn't pass access control check” 오류가 나. API에 CORS를 설정해서 정확히https://myapp.com과 미리보기 오리진만 허용하고, Content-Type과 Authorization 헤더와 함께 GET, POST, PATCH, DELETE를 허용해 줘. OPTIONS는 인증보다 먼저 처리하고, 오류 응답에도 CORS 헤더를 포함해 줘.
4단계: 쿠키와 인증 정보 처리하기
프런트엔드가 다른 오리진으로 쿠키(예: 로그인 세션)를 보낸다면, 양쪽이 합의해야 해요.
- 프런트엔드는
fetch에서credentials: "include"를 설정하거나, axios에서withCredentials: true를 설정해요. - 서버는
Access-Control-Allow-Credentials: true를 보내요. - 서버의
Access-Control-Allow-Origin은*가 아니라 정확한 오리진이어야 해요.
교차 사이트 쿠키에는 쿠키에 SameSite=None; Secure도 필요하고, 일부 브라우저는 서드파티 쿠키를 더 제한해요. 이것이 5단계의 접근법을 선호하는 또 다른 이유예요.
5단계: 동일 출처 설정으로 CORS 피하기
종종 가장 깔끔한 해결책은 아예 교차 출처 요청을 하지 않는 거예요. API를 페이지와 같은 오리진에서 제공하세요. 예를 들어 https://myapp.com/api/...처럼요.
- 개발 중에는 대부분의 개발 서버가 프록시를 할 수 있어요. Vite의
server.proxy설정으로/api를 다른 포트의 백엔드로 전달하면, 브라우저는 한 오리진하고만 통신해요. - 운영 중에는 호스트의 리라이트나 프록시 설정으로
/api를 백엔드에 라우팅하거나, 프런트엔드와 API를 한 서버에서 제공하세요.
그러면 프런트엔드는 fetch("/api/orders")처럼 상대 경로로 호출해요. 교차 출처 요청도, 교차 출처 프리플라이트도, 쿠키 문제도 없어요. 또한 미리보기와 운영 환경 사이에서 달라지는 API 도메인을 하드코딩할 필요도 없어요.
프런트엔드가 백엔드를 전체 URL 대신 상대 경로 /api로 호출하도록 바꿔 줘. 그리고 /api가 API 서버로 전달되도록 개발 프록시를 설정해 줘. 더 이상 필요 없는 CORS 설정은 모두 제거해 줘.
하지 말아야 할 것
fetch의mode: "no-cors". 아무것도 우회하지 않아요. 여러분의 코드에 비어 있고 읽을 수 없는 응답만 줄 뿐이에요.- CORS를 끄는 브라우저 확장 프로그램. 여러분의 브라우저만 바꿀 뿐이에요. 모든 방문자는 여전히 오류를 봐요.
- 실제 앱에서 쓰는 공개 “CORS 프록시” 서비스. 그곳을 지나가는 모든 것을 읽고 바꿀 수 있고, 그 서비스는 언제든 중단될 수 있어요.
- 로그인을 쓰는 API에
Access-Control-Allow-Origin: *. 어차피 인증 정보와 함께 동작하지 않고, 비공개 데이터에 잘못된 기본값이에요. - 인증 정보를 허용한 상태에서 어떤
Origin헤더든 그대로 되돌려주기. 그러면 어떤 웹사이트든 사용자를 대신해 로그인된 요청을 보낼 수 있어요. 허용 목록을 사용하세요.
AI로 만든 앱에서 흔한 원인
| 상황 | 해결책 |
|---|---|
| 프런트엔드가 비밀 키를 가진 서드파티 API를 호출함 | 호출을 백엔드로 옮김 |
| 미리보기에서는 되는데 게시된 도메인에서 실패함 | 게시된 오리진과 커스텀 도메인 오리진을 허용 목록에 추가하거나, 상대 경로 /api를 사용 |
| POST, PUT, DELETE에서만 오류 | 프리플라이트가 처리되지 않음. OPTIONS 처리와 허용 헤더를 추가 |
| 로그인을 추가한 뒤 오류가 생김 | 인증 정보에는 정확한 오리진과 Allow-Credentials가 필요함 |
| “CORS 오류”인데 네트워크 탭에는 500이 보임 | 서버 오류를 먼저 고침 |
API URL이 localhost로 하드코딩됨 | 상대 경로나 환경별 설정을 사용 |
Mythex의 CORS
Mythex 프로젝트에 웹사이트와 API 서버가 있으면, 둘 다 같은 프로젝트의 서비스로 실행되고 미리보기에서 전환해서 볼 수 있어요. 프런트엔드가 개발 중에는 프록시되는 상대 경로 /api를 통해 백엔드를 호출하도록 에이전트에게 요청하면, CORS 문제 대부분을 피할 수 있어요. 교차 출처 호출이 꼭 필요하다면, 콘솔의 정확한 오류와 관련된 오리진을 에이전트에게 알려 주세요. 게시된 앱은 미리보기와 주소가 다르므로 게시된 앱에서도 결과를 확인하세요. 자세한 내용은 문서의 다중 서비스 앱과 API 백엔드, 그리고 AI로 만든 앱 디버깅하는 법과 프런트엔드와 백엔드의 차이 가이드를 참고하세요.
CORS 체크리스트
- 전체 콘솔 오류와 실패한 요청의 상태 코드를 읽음
- 숨겨진 404나 500이 아님을 확인함
- 비밀 키를 쓰는 API는 브라우저가 아니라 백엔드에서 호출함
- 서버가 정확한 허용 오리진으로
Access-Control-Allow-Origin을 보냄 -
OPTIONS프리플라이트가 허용된 메서드와 헤더로 응답하며, 인증보다 먼저 처리됨 - 오류 응답에도 CORS 헤더가 포함됨
- 인증 정보: 정확한 오리진,
Allow-Credentials: true, 올바른 쿠키 설정 - 미리보기, 게시된 도메인, 커스텀 도메인 오리진을 모두 다루거나 상대 경로
/api를 사용함 -
no-cors, 공개 프록시, 아무 오리진이나 되돌려주는 방식을 쓰지 않음
자주 묻는 질문
CORS 오류는 무슨 뜻인가요?
한 오리진에서 로드된 페이지가 다른 오리진의 응답을 읽으려 했는데, 그 서버의 응답에 이를 허용하는 헤더가 없었다는 뜻이에요. 브라우저는 여러분의 코드가 응답을 읽지 못하도록 막아요. 해결책은 프런트엔드 코드가 아니라 서버에 있거나, 요청이 라우팅되는 방식에 있어요.
프런트엔드에서 CORS 오류를 고칠 수 있나요?
제대로는 고칠 수 없어요. mode: 'no-cors' 같은 설정은 응답을 코드에서 읽을 수 없게 숨길 뿐이에요. 서버가 올바른 Access-Control-Allow-Origin 헤더를 보내도록 하거나, 자신의 백엔드나 동일 출처 프록시를 통해 API를 호출하는 방법으로 고쳐야 해요.
Access-Control-Allow-Origin: * 는 안전한가요?
누구나 읽을 수 있는 공개 데이터, 예를 들어 로그인이 필요 없는 공개 API라면 괜찮아요. 하지만 쿠키나 다른 인증 정보와 함께 쓸 수 없고, 비공개 API라면 허용할 정확한 오리진 목록을 명시해야 해요.
Postman이나 curl에서는 되는데 브라우저에서는 왜 안 되나요?
CORS는 브라우저에서만 적용돼요. Postman이나 curl은 이를 적용하지 않으므로, 그곳에서는 요청이 성공해도 페이지 JavaScript가 같은 요청을 보내면 차단될 수 있어요.
프리플라이트 요청이란 무엇인가요?
JSON POST나 Authorization 헤더가 있는 요청처럼 단순하지 않은 요청에서는, 브라우저가 먼저 OPTIONS 요청을 보내 서버에 어떤 메서드와 헤더가 허용되는지 물어봐요. 그 OPTIONS 요청이 실패하거나 올바른 헤더가 없으면 실제 요청은 아예 전송되지 않아요.