웹개발을 하면서 CORS 문제를 한번씩 맞닥뜨린 경험이 있을 것이다.
오류는 프론트에서 발생하는데 결국 근본적인 해결은 백에서 해야 하기 때문에 양측 작업자가 다 해결방법을 모르는 상황이라면 실무에서 처음 마주쳤을 때 상당히 곤혹스러울 수 있다.
사실 해결하는 방법 자체가 어려운 것은 아니라서 프론트엔드나 백엔드 개발자 중 한명이라도 정책에 대해 잘 알고 있으면 생각보다 간단히 문제를 해결할 수 있으니, 원리와 해결책을 잘 알아두도록 하자.
들어가기 전 알아둬야 할 용어
출처(Origin)
Scheme(Protocol), Host, port 를 합친 것. 즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것.
ex) https://localhost:3000

출처 내의 포트 번호는 생략이 가능하지만, 출처에 포트 번호가 명시적으로 포함되어 있으면 이 포트번호까지 모두 일치해야 같은 출처라고 인정된다. console.log(location.orgin)
을 찍어보면 어플리케이션이 실행되고 있는 출처를 알아낼 수도 있다.
SOP(Same-Origin Policy)
동일 출처 정책이라고 한다. 말 그대로 같은 출처에서만 리소스를 공유하게 하는 정책이다.
그러나 웹에서 다른 출처에 있는 리소스를 가져와 사용하는 일은 굉장히 흔한 일이라 몇 가지 예외 조항을 두고 허용하고 있는데, 그 중 하나가 'CORS 정책을 지킨 리소스 요청'이다.
CORS(Cross Origin Resource Sharing)
CORS는 쉽게 말해 다른 출처를 가진 리소스를 공유하기 위한 정책이다. 우리가 겪는 CORS 이슈는 이 정책을 위반했기 때문에 발생하는 것이며, 겪는 입장에서는 귀찮지만 사실 CORS는 우리가 여기저기서 가져오는 리소스가 안전하다는 최소한의 보장을 해주는 방어막인 셈이다.
오류 내용
Access to fetch at ‘https://api.domain.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled
api 주소에서 로컬 주소로 가져오기 위한 액세스가 CORS 정책에 의해 차단되었다는 내용이다.
당연하다. api 서버 단에서 내 도메인에서의 접근을 허용해주는 Access-Control-Allow-Origin 헤더 설정을 해두지 않았기 때문이다.
CORS의 동작 방식
CORS는 브라우저에 관련된 정책이기 때문에, 클라이언트에서 외부 API 서버로 바로 요청을 보내면 CORS 문제가 발생한다.
즉, 서버에서 서버로 리소스를 요청할 때는 CORS 정책을 위반하지 않고 정상적으로 응답을 받을 수 있다.
그러므로 이 문제를 해결하는 방식은 다음과 같겠다.
- 내 api 서버로의 접근에서 CORS 문제가 발생했을 때
해당 api 서버에서 Access-Control-Allow-Origin 헤더에 내 Origin(출처)을 명시한다. - 외부 api 서버로의 접근에서 CORS 문제가 발생했을 때
1. 브라우저를 통하지 않고(클라이언트에서 외부 서버로 바로 요청을 하지 않고) 프록시 서버에서 외부 서버로 리소스를 요청한다.
2. 프록시 기능을 제공하는 라이브러리를 통해 우회한다.
3. 웹 브라우저 실행 옵션이나 플러그인을 통해 동일 출처 정책(SOP)을 회피한다.
단, 프론트에서는 주로 로컬 환경에 대한 CORS 문제 해결만 가능하고 배포 후에 발생하는 CORS 문제들은 서버 단에서 설정해줘야 한다고 볼 수 있다. 자세한 분류와 문제 해결에 대한 내용은 아래의 해결책 파트에 있다.
아래는 CORS 요청의 동작 방식에 대한 이해를 돕기 위한 내용이다.
브라우저는 다른 출처로 요청을 보낼 때 다음과 같은 절차를 거친다.
CORS 요청에는 2가지 방식이 있는데, simple request와 preflighted request다.
preflighted 요청은 진짜 요청을 보내기 전 미리 확인 요청을 보내보고, 통과하면 요청을 보내는 방식이라고 생각하고 아래를 읽어보자.
Simple request
simple 요청은 preflighted 요청을 보내지 않는다. 아래 조건을 모두 만족하면 simple request 다.
- GET 요청, HEAD, POST 중 한 가지 방식을 사용
- POST 방식일 경우 content-type이 아래 셋 중 하나여야 한다.
- application/x-www-form-unlencoded
- multipart/form-data
- text/plain
simple request 과정은 다음과 같다.
1. 요청을 보낸다.
2. 브라우저는 Host와 같은 헤더를 추가하는 것 외에도 Origin Request Header를 자동으로 추가한다.
GET /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
3. 서버에서 origin 리퀘스트 헤더를 확인한다. origin 값이 허용되면 Access-Control-Allow-Origin 요청 헤더를 origin 값으로 설정한다.
Http/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Content-Type: application/json
4. 응답을 받은 브라우저는 Access-Control-Allow-Origin 헤더가 탭의 출처와 일치하는지 확인한다.
Access-Control-Allow-Origin 값이 출처와 정확히 일치하거나, "*" 와일드카드 연산자를 포함하는 경우 검사를 통과한다.
서버는 요청의 출처에 따라 요청을 허용할지 결정할 수 있다. 브라우저는 Origin 요청 헤더를 정확하게 설정해야 한다.
Preflighted Request
일반적으로 우리가 웹 어플리케이션을 개발할 때 가장 많이 마주치는 시나리오다.
이 때 브라우저는 요청을 한번에 보내지 않고 확인을 위한 예비 요청과 본 요청으로 나누어서 서버로 전송한다.
본 요청을 보내기 전에 보내는 이 예비 요청을 Preflight라고 부르며, 이 요청에는 HTTP 메소드 중 OPTIONS
메소드가 사용된다.
예비 요청은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인한다.

preflighted 요청은 다음과 같은 과정을 거친다.
1. OPTIONS
메소드를 사용해 ajax 요청을 보낸다.
OPTIONS /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
2. 서버는 허용된 메소드 및 헤더를 지정해 응답한다.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Access-Control-Allow-Method: GET, POST, OPTIONS, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json
3. 헤더와 메소드가 통과되면, 브라우저는 본 CORS 요청을 보낸다.
POST /products/ HTTP/1.1
Host: api.domain.com
Authorization: token
Content-Type: application/json
Origin: https://www.domain.com
4. Access-Control-Allow-Origin 헤더에 올바른 출처가 있으므로 검사를 통과한다.
해결책
(서버/백) 운영 단계 - 근본적인 해결 방법
1. Access-Control-Allow-Origin 세팅
제일 정석적인 방법으로, 서버에서 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해주는 것이다.
하지만 서버를 따로 구축하지 않고 외부 서버에 리소스를 요청하고 있을 때는 사용할 수 없는 방법이다. 클라이언트와 서버를 모두 자신이 제어할 수 있다면 직접, 프론트만 제어하고 있는 상황이라면 백엔드 작업자에게 요청하여 이 헤더를 세팅하자.
Access-Control-Allow-Origin 헤더를 세팅할 때, 와일드카드인 *
를 사용하게 되면 아무 요청이나 받아먹겠다는 무방비한 상태가 되므로 보안적으로 심각한 이슈가 발생할 수 있다.
그러니 가급적이면 출처(origin)를 명시해주도록 하자.
Access-Control-Allow-Origin: https://www.domain.com
// express
const express = require('express');
const app = express();
app.get('api', (req, res) => {
res.setHeader('Access-Control-Allow-origin', 'https://www.domain.com');
res.send(data);
});
이 헤더는 Nginx나 Apache 같은 서버 엔진의 설정에서 추가할 수도 있지만, 그러면 복잡한 세팅을 하기 불편하기 때문에 소스 코드 내에서 응답 미들웨어 등을 사용해 세팅하는 것이 좋다.
Spring, Express, Django 같은 유명한 백엔드 프레임워크들은 모두 CORS 관련 설정을 위한 세팅이나 미들웨어 라이브러리를 제공하고 있으니 세팅이 어렵지는 않을 것이다.
2. CORS 미들웨어 사용
express 환경에서의 코드를 들어보겠다.
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'https://www.domain.com',
...
};
app.use(cors(corsOptions));
origin에 허용하고자 하는 도메인을 넣어서 response 헤더에 Access-Control-Allow-Origin 내용을 추가할 수 있다.
app.use(cors())
는 와일드카드 '*' 를 설정하는 것과 동일하므로 origin을 명시할 수 있도록 하자.
3. 내 서버에서 외부 서버로 프록싱
React 환경 프론트 코드
import axios from "axios";
...
axios
.get("/api/covid", {
params: {
pageNo: 1,
NumOfRows: 10,
startCreateDt: yesterdayDate,
endCreateDt: todayDate,
},
})
Express 서버단 코드
const express = require("express");
const axios = require("axios");
const path = require("path");
const app = express();
const port = process.env.PORT || 5000;
const http = require("http");
require("dotenv").config()
const covid_url = "http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19SidoInfStateJson";
const getCovidData = async (req) => {
let res;
try {
response = await axios.get(covid_url, {
params: { /* 생략 */ ),
})
} catch (e) {
console.log(e);
}
return res;
};
app.get("api/covid", (req, res) => {
getCovidData(req).then((response) => {
res.json(response.data.response.body)
})
});
app.use(express.static(path.join(__dirname, "client/build")));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname + "/client/build/index.html"))
});
서버를 배포하고 호스팅한 운영 상황이라면 클라이언트에서 서버로 요청할 때 CORS 문제가 일어나지 않을 것이고,
express로 구축한 서버에서 외부 API 서버로 요청할 때도 서버 간 통신이므로 CORS 정책을 위반하지 않는다.
(클라이언트/프론트) 개발 단계 - 로컬 환경에서의 CORS 문제 해결
1. Webpack Dev Server로 리버스 프록싱
사실 CORS 문제는 로컬에서 프론트엔드 어플리케이션을 개발할 때 가장 많이 마주친다. 백엔드에서 Access-Control-Allow-Origin 헤더가 세팅돼 있더라도, 일반적으로 http://localhost:3000
같은 범용적인 출처를 넣어주지는 않기 때문이다.
프론트 개발자는 대부분 웹팩과 webpack-dev-server
를 사용해 자신의 머신에 개발환경을 구축하게 되는데, 이 라이브러리의 프록시 기능을 사용하면 간단히 CORS 정책을 우회할 수 있다.
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.domain.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
}
}
}
이렇게 하면 로컬 환경에서 /api
로 시작하는 URL로 보내는 요청에 대해 브라우저는 localhost:8000/api
로 요청을 보낸 것으로 알고 있지만, 사실 뒤에서 웹팩이 https://api.domain.com
으로 요청을 프록싱해주기 때문에 마치 CORS 정책을 지킨 것처럼 브라우저를 속이면서도 우리는 원하는 서버와 자유롭게 통신할 수 있다. 즉, 프록싱을 통해 CORS 정책을 우회할 수 있다.
만약 webpack-dev-middleware와 Node 서버 조합으로 개발 환경을 구축했더라도, http-proxy-middleware 라이브러리를 사용하면 간단히 프록시 설정을 할 수 있다. 어차피 webpack-dev-server
도 내부적으로는 http-proxy-middleware
를 사용한다.
http-proxy-middleware
CRA로 개발 중일 경우)
1. http-proxy-middleware 라이브러리 설치
2. src 폴더 내에 setupProxy.js 파일 생성 후 아래와 같이 작성
const {createProxyMiddleware} = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
"/api",
createProxyMiddleware({
target: "https://localhost:5000",
changeOrigin: true,
})
);
}
위처럼 작성하면, 로컬 환경에서 http://localhost:3000/api
로 시작되는 요청을 라이브러리가 http://localhost:5000/api
로 프록싱 해주어 CORS 문제를 일으키지 않는다.
다만, 이 경우 실제 프로덕션 환경에서도 클라이언트 어플리케이션의 소스를 서빙하는 출처와 API 서버의 출처가 같은 경우에 사용하는 것이 좋다. 로컬에서는 잘 돌아가더라도, 어플리케이션을 빌드하고 서버에 올리면 더 이상 webpack-dev-server 가 구동하는 환경이 아니기 때문에 이상한 곳으로 API 요청을 보내기 때문이다.
2. Chrome 확장 프로그램 이용
Allow CORS: Access-Control-Allow-Origin
Easily add (Access-Control-Allow-Origin: *) rule to the response header.
chrome.google.com
이 확장을 설치하고 브라우저에서 활성화시키면, 로컬 환경에서 API를 테스트 시 CORS 문제를 해결할 수 있다.
실제 운영 환경에서는 Access-Control-Allow-Origin 헤더를 설정해 CORS 문제를 해결하고, 로컬 환경에서 개발할 때는 이 확장을 사용해서 CORS 문제를 해결하면 좋을 듯 하다.
3. 남이 만든 프록시 서버 사용
프록시 서버는 클라이언트가 프록시 서버 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다.
쉽게 말해 브라우저와 서버 간의 통신을 도와주는 중계서버라고 생각하면 된다.
https://cors-anywhere.herokuapp.com
위 서버는 그 프록시 서버 중 하나다.
이 서버를 사용하면 중간에 요청을 가로채서 HTTP 응답 헤더에 Access-Control-Allow-Origin: \*
를 설정해서 응답해준다. 그러므로 믿을 수 있는 외부 서버일 때만 사용하자.
axios({
method: "GET",
url: `https://cors-anywhere.herokuapp.com/https://api.domain.com`,
headers: {
'APIKey': ${your_API_key},
},
위와 같이 요청해야하는 URL 앞에 프록시 서버 URL을 붙여서 요청하면, CORS 문제를 간단히 해결할 수 있다.
참고글
CORS는 왜 이렇게 우리를 힘들게 하는걸까?
이번 포스팅에서는 웹 개발자라면 한번쯤은 얻어맞아 봤을 법한 정책에 대한 이야기를 해보려고 한다. 사실 웹 개발을 하다보면 CORS 정책 위반으로 인해 에러가 발생하는 상황은 굉장히 흔해서
evan-moon.github.io
Spring CORS
CORS란? CORS를 해결해보자 | 구보현 블로그
CORS란? CORS를 해결해보자 20200522 프로젝트를 하면서 프론트에서 서버에서 제공한 API로 요청하자, CORS 에러가 발생했다. 지금까지 CORS에러를 해결하기만 하고 정확히 CORS가 무엇이고 어떻게 동작
bohyeon-n.github.io
[WEB] 📚 CORS 개념 💯 완벽 정리 & 해결 방법 👏
CORS (Cross Origin Resource Sharing) CORS 정책은 우리가 가져오는 리소스들이 안전한지 검사하는 관문이다. 웹개발을 하는 사람들은 이 CORS 정책위반으로 인해 에러가 나는 상황을 많이들 겪어봤을것이
inpa.tistory.com
express CORS
나를 너무나 힘들게 했던 CORS 에러 해결하기 😂
🔥 사건의 발단 : 외부 API 호출 때는 바야흐로 2020년 3월. 프론트엔드 공부를 시작한 지 얼마 되지 않은 채 홀로 토이 프로젝트를 진행하던 중이었다. 코로나 바이러스 관련 웹서비스를 만들고자
xiubindev.tistory.com
'기타' 카테고리의 다른 글
[Github] Mac OS에서 Github에 프로젝트 올리기 (0) | 2022.08.31 |
---|---|
[Javascript] 비동기 처리, Promise, async/await (0) | 2022.07.13 |
SQL과 NoSQL의 차이, 장단점 (0) | 2022.06.23 |
websocket과 socket.io 의 차이 (0) | 2022.06.22 |
[Sourcetree] Bitbucket 협업 시 충돌 병합(Conflict Merge) 해결 방법 (0) | 2022.06.07 |