CORS는 무엇이고 왜 에러가 발생할까?
시리즈
스터디- 1.동적 배열
- 2.알면 유용한 자료구조들
- 3.모노레포(Monorepo) 제대로 이해하기
- 4.CORS는 무엇이고 왜 에러가 발생할까?← 현재
- 5.포인터

1. CORS란 무엇일까?
프론트엔드나 백엔드 개발을 하다 보면 API 요청을 보냈을 때 브라우저 콘솔에 CORS 관련 에러가 발생하는 경우가 있습니다. 요청 URL이 올바르고 서버가 정상적으로 실행되고 있는데도 응답을 읽지 못하는 상황입니다.
CORS(Cross-Origin Resource Sharing)는 교차 출처 리소스 공유를 의미합니다. 브라우저에서 실행되는 웹 애플리케이션이 다른 출처의 서버에 요청하고, 그 응답을 읽을 수 있도록 허용하는 보안 메커니즘입니다.
예를 들어 프론트엔드와 백엔드가 다음과 같이 실행되고 있다고 가정하겠습니다.
- 프론트엔드:
http://localhost:3000 - 백엔드:
http://localhost:8080
프론트엔드에서 다음과 같이 API를 호출할 수 있습니다.
fetch("http://localhost:8080/api/users")
.then((response) => response.json())
.then((data) => console.log(data));
두 주소는 포트가 다르기 때문에 서로 다른 출처입니다. 따라서 브라우저는 해당 요청에 CORS 정책을 적용합니다.
서버가 요청 출처를 허용하지 않거나 필요한 응답 헤더를 보내지 않으면 브라우저에서 응답을 읽지 못할 수 있습니다.
2. Origin과 동일 출처 정책(SOP)
Origin이란?
Origin(출처)은 URL을 구성하는 다음 세 가지 요소로 결정됩니다.
- Protocol(프로토콜):
http,https - Host(호스트):
example.com,localhost - Port(포트):
3000,8080
이 중 하나라도 다르면 서로 다른 출처로 판단합니다.
기준 URL이 http://example.com:3000이라고 가정하면 다음과 같습니다.
| URL | 출처가 같은가? | 이유 |
|---|---|---|
http://example.com:3000/about |
O | 경로만 다름 |
https://example.com:3000 |
X | 프로토콜이 다름 |
http://api.example.com:3000 |
X | 호스트가 다름 |
http://example.com:8080 |
X | 포트가 다름 |
URL의 경로, 쿼리, 프래그먼트는 출처를 구분하는 기준에 포함되지 않습니다.
동일 출처 정책은 왜 필요할까?
동일 출처 정책(Same-Origin Policy, SOP)은 브라우저에서 실행되는 스크립트가 다른 출처의 리소스에 자유롭게 접근하지 못하도록 제한하는 보안 정책입니다.
예를 들어 사용자가 특정 서비스에 로그인한 상태에서 악성 웹사이트에 접속했다고 가정하겠습니다. 브라우저가 모든 웹사이트의 스크립트에 다른 사이트의 응답을 자유롭게 읽을 수 있도록 허용한다면, 악성 사이트가 사용자의 권한으로 접근 가능한 정보를 읽으려 할 수 있습니다.
동일 출처 정책은 이러한 교차 출처 간의 무제한적인 응답 접근을 제한하는 역할을 합니다.
하지만 실제 웹 서비스에서는 프론트엔드와 백엔드가 서로 다른 출처에서 실행되는 경우가 많습니다. 이때 서버가 허용한 출처에서 응답을 읽을 수 있도록 하는 메커니즘이 바로 CORS입니다.
즉, SOP가 기본적인 접근 제한 정책이라면 CORS는 서버가 허용한 범위에서 교차 출처 응답을 읽을 수 있도록 하는 메커니즘입니다.
3. CORS 에러는 왜 발생할까?
CORS 에러는 브라우저가 교차 출처 응답을 읽기 위한 조건을 충족하지 못했을 때 발생할 수 있습니다. 대표적인 원인은 다음과 같습니다.
허용 출처가 설정되지 않은 경우
가장 대표적인 원인은 서버 응답에 Access-Control-Allow-Origin 헤더가 없거나, 요청한 출처가 허용되지 않은 경우입니다.
예를 들어 프론트엔드가 http://localhost:3000에서 실행되고 있다면 서버는 다음과 같은 응답 헤더를 설정할 수 있습니다.
Access-Control-Allow-Origin: http://localhost:3000
이 헤더는 해당 출처에서 응답을 읽을 수 있도록 허용한다는 의미입니다.
반면 서버가 다음과 같이 설정되어 있다면 문제가 발생할 수 있습니다.
Access-Control-Allow-Origin: http://localhost:4000
실제 프론트엔드 출처는 http://localhost:3000이므로 허용된 출처와 일치하지 않습니다. 이 경우 브라우저는 응답을 자바스크립트에 제공하지 않습니다.
프리플라이트 요청이 실패한 경우
프리플라이트(Preflight)는 실제 API 요청을 보내기 전에 서버가 해당 요청을 허용하는지 확인하는 과정입니다. 브라우저는 특정 조건에 해당하는 교차 출처 요청에 대해 OPTIONS 메서드로 프리플라이트 요청을 보냅니다.
예를 들어 다음과 같은 요청을 보낸다고 가정하겠습니다.
fetch("https://api.example.com/users", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
body: JSON.stringify({
name: "Kim",
}),
});
이 요청은 PUT 메서드와 Authorization 헤더를 사용하므로 프리플라이트 요청이 필요합니다.
브라우저는 실제 요청을 보내기 전에 다음과 같은 요청을 전송합니다.
OPTIONS /users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
서버는 허용하는 출처와 메서드, 헤더 등을 응답해야 합니다.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
브라우저가 응답을 확인하고 요청이 허용되었다고 판단하면 실제 API 요청을 진행할 수 있습니다.
반대로 서버가 OPTIONS 요청을 처리하지 못하거나 필요한 메서드와 헤더를 허용하지 않으면 프리플라이트 단계에서 실패할 수 있습니다. 이 경우 실제 API 요청은 전송되지 않을 수 있습니다.
단, 모든 교차 출처 요청에서 프리플라이트가 발생하는 것은 아닙니다. 요청의 메서드와 헤더 등의 조건에 따라 프리플라이트 여부가 결정됩니다.
쿠키나 인증 정보를 포함한 요청에서 설정이 잘못된 경우
로그인 기능을 구현할 때도 CORS 에러가 발생할 수 있습니다.
예를 들어 교차 출처 요청에 쿠키를 포함하려면 다음과 같이 설정할 수 있습니다.
fetch("https://api.example.com/me", {
credentials: "include",
});
이 경우 서버에서도 자격 증명을 포함한 요청에 맞는 CORS 설정을 제공해야 합니다.
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
자격 증명을 포함한 요청에서는 다음 사항을 주의해야 합니다.
Access-Control-Allow-Credentials의 값은true여야 합니다.Access-Control-Allow-Origin에는*를 사용할 수 없으며, 요청 출처와 일치하는 구체적인 출처를 지정해야 합니다.- 쿠키가 실제로 전송되려면
SameSite,Secure등의 쿠키 정책도 충족해야 합니다.
따라서 CORS 설정이 올바르더라도 쿠키 정책이나 인증 로직에 문제가 있으면 요청이 정상적으로 처리되지 않을 수 있습니다.
CORS 에러가 발생하면 서버 요청도 실패한 걸까?
CORS 에러가 발생했다고 해서 반드시 서버에서 요청 처리가 실패한 것은 아닙니다.
예를 들어 서버가 API 요청을 정상적으로 처리하고 데이터를 반환했더라도, 응답에 필요한 CORS 헤더가 없다면 브라우저는 해당 응답을 자바스크립트에서 읽지 못하도록 제한할 수 있습니다.
이 경우 서버에서는 요청이 정상적으로 처리되었을 수 있지만, 프론트엔드에서는 응답을 사용할 수 없어 오류가 발생한 것처럼 보입니다.
따라서 CORS 에러가 발생했을 때는 서버의 응답 상태와 브라우저의 CORS 정책 위반 여부를 구분해서 확인해야 합니다.
4. CORS 에러는 어떻게 해결할까?
서버에서 CORS 설정하기
CORS는 브라우저가 적용하는 보안 정책이며, 일반적인 웹 서비스에서는 서버가 응답 헤더를 통해 교차 출처 요청을 허용할 수 있습니다. 따라서 백엔드에서 CORS 설정을 수정하는 것이 일반적인 해결 방법입니다.
프론트엔드에서는 요청 URL이나 메서드, 헤더 등이 올바른지 확인할 수 있으며, 개발 환경에서는 프록시를 활용할 수도 있습니다. 다만 프론트엔드 코드만으로 브라우저의 CORS 보안 정책을 임의로 해제할 수는 없습니다.
가장 일반적인 해결 방법은 백엔드에서 올바른 CORS 응답 헤더를 설정하는 것입니다.
일반적으로 다음 항목을 확인해야 합니다.
- 허용할 출처
- 허용할 HTTP 메서드
- 허용할 요청 헤더
- 필요한 경우 자격 증명 허용
- 프리플라이트 요청 처리
운영 환경에서는 실제 서비스에서 사용하는 프론트엔드 출처만 허용하는 것이 좋습니다. 모든 출처를 무조건 허용하기보다는 서비스의 요구 사항에 맞게 설정해야 합니다.
또한 CORS 설정은 백엔드 프레임워크나 서버 구성에 따라 적용 방법이 다르므로, 사용 중인 환경에 맞게 설정해야 합니다.
개발 환경에서 프록시 사용하기
개발 환경에서는 프록시를 활용해 CORS 문제를 해결할 수도 있습니다.
예를 들어 브라우저에서 API 서버로 직접 요청하는 대신 프론트엔드 개발 서버를 통해 요청을 전달하도록 구성할 수 있습니다.
브라우저
→ http://localhost:3000/api/users
→ 개발 서버 프록시
→ https://api.example.com/users
이렇게 구성하면 브라우저는 프론트엔드와 같은 출처의 주소로 요청하고, 프록시가 실제 API 서버로 요청을 전달합니다.
Next.js에서는 rewrites 등을 활용해 이러한 구성을 만들 수 있습니다.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
};
export default nextConfig;
이후 프론트엔드에서는 다음과 같이 요청할 수 있습니다.
fetch("/api/users");
다만 프록시는 브라우저의 요청 경로를 변경하는 방법이지, 백엔드의 CORS 정책 자체를 수정하는 것은 아닙니다. 운영 환경에서도 사용하려면 배포 환경의 프록시 구성과 인증 정보 전달 방식 등을 함께 확인해야 합니다.
mode: "no-cors"는 해결 방법일까?
CORS 에러를 해결하기 위해 다음과 같이 mode: "no-cors"를 사용하는 경우가 있습니다.
fetch("https://api.example.com/users", {
mode: "no-cors",
});
하지만 이는 일반적인 CORS 에러 해결 방법이 아닙니다.
no-cors 모드에서는 응답이 불투명한(opaque) 응답으로 처리되기 때문에 자바스크립트에서 응답 본문이나 일반적인 상태 코드 등을 읽을 수 없습니다.
따라서 API에서 데이터를 받아 화면에 표시하려는 경우에는 적절한 해결 방법이 아닙니다. 서버의 CORS 설정과 실제 요청 조건을 확인하는 것이 올바른 접근입니다.
5. CORS 에러를 디버깅할 때 확인할 것
실제로 CORS 에러가 발생하면 브라우저 개발자 도구의 Console과 Network 탭을 확인하는 것이 좋습니다.
- 요청 URL 확인: 프론트엔드와 API의 출처가 다른지 확인합니다.
- Console 확인: 브라우저가 출력한 CORS 오류 메시지를 확인합니다.
- Network 탭 확인:
OPTIONS요청이 발생했는지, 실패했다면 어떤 응답을 받았는지 확인합니다. - 응답 헤더 확인:
Access-Control-Allow-Origin등의 헤더가 올바른지 확인합니다. - 요청 조건 확인: HTTP 메서드와 요청 헤더가 허용되어 있는지 확인합니다.
- 인증 정보 확인: 쿠키를 사용하는 경우 자격 증명 설정과 쿠키 정책을 확인합니다.
특히 서버가 실제 요청에 정상적으로 응답했더라도 필요한 CORS 헤더가 없으면 브라우저에서 응답을 읽지 못할 수 있습니다.
또한 서버 연결 실패, TLS 인증서 오류, 리다이렉트 문제 등도 요청 실패의 원인이 될 수 있으므로, 콘솔 메시지만으로 원인을 단정하지 않고 Network 탭의 요청과 응답 정보를 함께 확인해야 합니다.
6. 마무리 및 정리
CORS는 단순히 프론트엔드에서 발생하는 오류가 아니라 브라우저의 보안 정책과 서버의 허용 설정이 연결된 개념입니다.
핵심 내용을 정리하면 다음과 같습니다.
- SOP는 교차 출처 리소스에 대한 스크립트의 접근을 제한하는 브라우저 보안 정책입니다.
- CORS는 서버가 허용한 출처에서 교차 출처 응답을 읽을 수 있도록 하는 메커니즘입니다.
- 프리플라이트는 특정 요청을 보내기 전에 서버의 허용 여부를 확인하는
OPTIONS요청입니다. - CORS 에러 해결을 위해서는 서버의 응답 헤더, 요청 메서드와 헤더, 자격 증명 설정 등을 확인해야 합니다.
오늘은 제가 평소에도 궁금했던 CORS에 대해 알아보았습니다. 평소에도 개발하면서 CORS 에러라는 소리를 많이 들었었는데 정작 이게 정확히 뭐 때문에 일어나는지 이해하지 못했습니다. 결국 CORS 에러를 해결하기 위해서는 단순히 프론트엔드 코드를 수정하는 것보다 브라우저가 어떤 요청을 보내고 서버가 어떤 응답을 반환하는지 이해하는 것이 중요하다고 생각합니다.