[Web 일반] CORS 에러 발생과 해결 방법
목차
CORS란 무엇인가?
CORS(Cross-Origin Resource Sharing)는 "교차 출처 리소스 공유"라는 의미로, 브라우저에서 다른 도메인, 포트 또는 프로토콜로부터 리소스를 요청할 때, 해당 출처(도메인, 프로토콜, 포트)의 리소스를 공유할 수 있게 해주는 보안 메커니즘이다.
쉽게 말해, 브라우저가 A 웹사이트의 웹페이지를 동작시키고 있을 때, 해당 웹페이지가 외부의 B 웹사이트의 데이터를 가져오려고 할 때 필요한 보안 정책이다.
사례로 이해하기
내가 mywebsite.com이라는 도메인에서 운영하는 블로그를 가지고 있다고 가정해보자. 블로그에 날씨 위젯을 추가하기 위해 weatherapi.com의 API를 사용하려고 한다.
// mywebsite.com의 프론트엔드 코드
fetch('https://api.weatherapi.com/v1/current.json')
.then(response => response.json())
.then(data => console.log(data));
이때 브라우저 콘솔에 아래의 에러가 발생할 수 있다. 이것이 바로 CORS 에러이다.
Access to fetch at 'https://api.weatherapi.com/v1/current.json' from origin 'https://mywebsite.com' has been blocked by CORS policy
CORS가 필요한 이유
CORS는 웹 보안을 위해 매우 중요하다.악의적인 웹사이트가 사용자의 민감한 정보에 마음대로 접근하는 것을 방지하기 때문이다.
📌 CORS가 없다면?
- 악성 웹사이트가 사용자의 은행 웹사이트 데이터를 무단으로 가져갈 수 있다.
- 다른 사이트의 API를 무제한으로 호출하여 서버에 부하를 줄 수 있다.
- XSS(Cross-Site Scripting) 공격이 더 쉬워질 수 있다.
CORS는 어떻게 동작하는가?
Preflight 요청
브라우저는 실제 요청을 보내기 전에 'preflight' 요청이라는 사전 검증을 먼저 수행한다. 이는 OPTIONS 메소드를 사용하는 HTTP 요청이다.
예를 들어, 나의 웹사이트에서 다음과 같은 API 요청을 한다고 가정해보자:
fetch('https://api.weatherapi.com/v1/current.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ city: 'Seoul' })
});
이때 브라우저와 서버는 다음과 같은 순서로 요청을 처리한다:
1. 브라우저가 Preflight 요청 전송
OPTIONS /v1/current.json HTTP/1.1
Host: api.weatherapi.com
Origin: https://mywebsite.com
Access-Control-Request-Method: POST // 실제로 보내고자 하는 HTTP 메서드
Access-Control-Request-Headers: Content-Type // 실제로 사용할 헤더들
여기서 각 헤더의 의미는 다음과 같다:
- Origin: 요청을 보내는 출처(도메인, 프로토콜, 포트).
- Access-Control-Request-Method: 이후에 실제로 보내고자 하는 HTTP 메서드를 서버에 알려준다.
- Access-Control-Request-Headers: 이후에 실제로 사용할 헤더들을 서버에 알려준다.
2. 서버의 Preflight 응답
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://mywebsite.com // 허용된 출처
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 허용된 HTTP 메서드들
Access-Control-Allow-Headers: Content-Type // 허용된 헤더들
Access-Control-Max-Age: 86400 // Preflight 응답 캐시 시간(초)
서버는 각각의 요청 헤더에 대응하는 허용 범위를 응답 헤더로 알려준다:
- Access-Control-Allow-Origin: 이 리소스에 접근이 허용된 출처
- Access-Control-Allow-Methods: 허용된 HTTP 메서드들의 목록
- Access-Control-Allow-Headers: 허용된 헤더들의 목록
- Access-Control-Max-Age: Preflight 요청의 결과를 캐시할 시간(초)
3. 브라우저가 실제 요청 전송
- Preflight 응답이 성공적이면, 그제서야 실제 POST 요청을 보낸다.
- 만약 Preflight 응답이 실패하면, 브라우저는 실제 요청을 보내지 않고 CORS 에러를 발생시킨다.
언제 Preflight가 필요한가?
브라우저는 CORS 요청을 '단순 요청'과 'Preflight가 필요한 요청' 두 가지로 구분한다. 하지만 중요한 점은, 모든 요청에 CORS 정책이 적용된다는 것이다.
1. Simple Request(단순 요청)
다음 조건을 모두 만족하는 경우에는 Preflight 요청이 생략된다(자세한 건 공식문서의 여기 참고):
- HTTP 메소드가 다음 중 하나:
- GET
- HEAD
- POST
- 헤더가 다음만 포함:
- Accept
- Accept-Language
- Content-Language
- Content-Type (아래 값만 허용)
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 요청에 ReadableStream 객체가 사용되지 않음
- XMLHttpRequest.upload에 이벤트 리스너가 등록되지 않음
주의: 단순 요청이라도 서버가 적절한 CORS 헤더(Access-Control-Allow-Origin)를 응답하지 않으면 브라우저는 여전히 CORS 에러를 발생시킨다!
2. Preflight가 필요한 요청
좀 더 복잡한 요청의 경우, 브라우저는 본 요청 전에 Preflight 요청을 먼저 보낸다. 다음 중 하나라도 해당되면 Preflight가 필요하다:
1. 특별한 메서드 사용 (PUT, DELETE 등)
// PUT 메서드 사용 - Preflight 필요
fetch('https://api.example.com/data', {
method: 'PUT',
body: JSON.stringify({ name: 'John' })
});
2. application/json 사용 (웹 API에서 가장 일반적)
// Content-Type: application/json 사용 - Preflight 필요
fetch('https://api.example.com/data', {
method: 'POST', // POST라도 JSON을 사용하면 Preflight 필요
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'John' })
});
3. 커스텀 헤더 추가
// 커스텀 헤더 'Authorization' 사용 - Preflight 필요
fetch('https://api.example.com/data', {
method: 'GET', // GET이라도 커스텀 헤더가 있으면 Preflight 필요
headers: {
'Authorization': 'Bearer token123'
}
});
현대 웹 개발에서는 대부분의 API 요청이 다음 특징을 가진다:
- JSON 데이터 사용
- Authorization 헤더를 통한 인증
- RESTful API (PUT, DELETE 등 사용)
따라서 실제로는 대부분의 API 요청이 Preflight를 필요로 하며, 서버는 반드시 적절한 CORS 설정을 해야 한다. '단순 요청'은 오히려 드문 케이스라고 볼 수 있다.
CORS 해결 방법
1. 서버 측 설정
가장 일반적인 해결 방법은 서버에서 적절한 CORS 헤더를 설정하는 것이다.
FastAPI 예제:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://mywebsite.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
allow_credentials=True,
)
allow_origins | 허용할 출처(도메인) 목록을 지정. 여기서는 https://mywebsite.com만 허용. |
allow_methods | 허용할 HTTP 메서드 (GET, POST 등)를 지정. |
allow_headers | 클라이언트가 보낼 수 있는 헤더를 지정. ["*"]는 모든 헤더 허용. |
allow_credentials | 쿠키, 인증 정보 포함 요청을 허용할지 여부. True면 허용. |
2. 프록시 서버 사용
자체 프록시 서버를 통해 API 요청을 우회하는 방법도 있다.
// 프론트엔드에서의 요청
fetch('/api/weather') // 자체 서버로 요청
.then(response => response.json())
.then(data => console.log(data));
# FastAPI를 사용한 백엔드 프록시 서버
from fastapi import FastAPI
import httpx
app = FastAPI()
@app.get("/api/weather")
async def get_weather():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.weatherapi.com/v1/current.json")
return response.json()
이 방식은 프론트엔드에서 직접 외부 API를 호출하지 않고, 자체 백엔드 서버를 통해 우회하는 방법이다.
이 방식이 작동하는 이유는 서버 간의 통신에는 CORS 정책이 적용되지 않기 때문이다. 브라우저에서 직접 외부 API를 호출할 때는 CORS 제한이 있지만, 백엔드 서버에서 외부 API를 호출할 때는 이러한 제한이 없다.
CORS 설정이 불가능한 외부 API를 사용해야 할 때 유용한 해결책이 될 수 있다.
자주 발생하는 CORS 에러와 해결 방법
1. "No 'Access-Control-Allow-Origin' header is present"
이 에러는 서버에서 CORS 헤더를 설정하지 않았거나 Preflight 요청에 대한 응답이 올바르지 않은 경우 발생한다. 서버에서 OPTIONS 요청에 대한 처리와 함께 적절한 CORS 헤더를 추가해야 한다.
2. "Method not allowed"
허용되지 않은 HTTP 메서드를 사용할 때 발생한다. 서버의 CORS 설정에서 Access-Control-Allow-Methods 헤더에 해당 메서드를 추가해야 한다. 예를 들어 PUT 요청이 차단된다면, 서버에서 PUT 메서드를 명시적으로 허용해야 한다.
3. "Request header field Content-Type is not allowed"
허용되지 않은 헤더를 사용할 때 발생하는 에러이다. 특히 application/json과 같은 Content-Type을 사용할 때 자주 발생한다. 서버의 CORS 설정에서 Access-Control-Allow-Headers에 필요한 헤더를 추가해야 한다.
핵심 포인트 정리
- 모든 CORS 요청은 기본적으로 브라우저의 보안 정책을 따른다.
- 서버 측에서 적절한 헤더 설정으로 대부분의 문제를 해결할 수 있다.
- 개발 환경과 프로덕션 환경의 CORS 설정을 구분하여 관리하는 것이 좋다.