내가 찾은 CORS Error의 올바른 해결법
CORS Error에 대해서 알아보자!
* 예시를 위해 Flask를 사용했지만 Flask를 모르셔도 괜찮습니다. 다른 프레임워크에서도 지원하는 기본적인 기능만을 사용했습니다.

원인
Same Origin Policy와 Cross Origin Policy를 모른다면 실수하기 쉽다.
이 정책을 모른채로 코딩하다가는
난 잘 코딩했는데 아래와 같은 Error가 뜨면서 json 데이터 같은 외부 자원들을 읽어오지 못하는 경험을 할 수도 있다.
Access to XMLHttpRequest at '주소A' from origin '주소B' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
어떤 정책이길래 우릴 괴롭히는 것일까? Origin이라는 개념만 이해한다면 별 것 없다.
Origin? 출처?
먼저 여기서 사용되는 Origin(=출처)이라는 개념을 정확하게 알아야 한다.
웹에서 Origin은 간단하게 프로토콜, 주소, 포트번호의 튜플로 구분된다.
Origin = [프로토콜]://[Host의 IP 주소 또는 URL]:[포트번호]
- 단, 여기서 포트번호는 생략할 수 있다. 포트 번호를 생략하면 HTTP 프로토콜이면 80, HTTPS 프로토콜이면 443이 숨어있다고 생각하자.
즉, 아래의 셋 중 하나라도 다르다면 다른 Origin이다.
- 프로토콜(HTTP, HTTPS, FTP, ...)
- URL(a.com, b.com, cat.co.kr, ...)
- 포트번호(80, 433, 8080, ...)
(퀴즈) 다음 중 https://groot.com과 Origin이 같은 것은?
- https://www.groot.com
- https://not-groot.com
- http://groot.com
- https://groot.com:8443
- https://groot.com:80
- https://groot.com:443
답) 6.
https 프로토콜은 기본 포트인 443를 생략할 수 있으므로 6번과 같다.
나머지 선택지는 프로토콜, URL, Port 셋 중 하나가 다르므로 다른 출처이다.
1. 동일 출처 정책(Same-Origin Policy, SOP)
대부분의 웹 브라우저는 Same Origin Policy(SOP)라는 보안 정책을 준수한다.
내가 접속한 사이트(Origin)에서 다른 Origin에 요청한 것을 기본적으로 제한해서 어느정도 해커의 공격에 방어하는 것이다.
다른 Origin에서 온 자원들을 모두 사용할 수 없게 차단했다면 CDN과 같은 것을 사용하기 어려웠을 것이다. 그래서 <img>, <script>, <link>, <iframe>과 같은 특정 HTML Tag는 다른 Origin으로부터 온 것은 임베딩할 수 있게 허용해준다. (임베딩만 가능하고 데이터를 읽는 건 보안상의 이유로 차단한다.) 하지만 우리는 개발하다보면 다른 Origin으로부터의 자원을 불러오고 싶은 경우가 많다. 그렇기 때문에 CORS(Cross Origin Resource Sharing)이라는 것이 생겼다. 다른 Origin의 데이터를 읽고 싶으면 CORS 표준을 지켜서 내 사이트로부터의 응답에 "다른 Origin이더라도 허용해줘!" 라고 말해주면 된다.
Same Origin / Cross Origin Policy에 대해서 자세하게 이해하고 싶으면 아래의 영상을 추천한다! 설명도 깔끔하고 자세하게 예시도 보여줘서 이해하기 쉽다.
2. 교차 출처 리소스 공유( Cross-Origin Resource Sharing, CORS)?
SOP가 우리가 더 안전하게 웹을 탐색할 수 있게하지만 의도적으로 다른 리소스랑 상호작용하면서 개발하고 싶은 경우에는 너무 제한적이다. 내가 이해한 바로는 그럴 때 써라고 만든게 CORS이다. SOP를 좀 완화해준 느낌?
CORS를 사용해서 하나의 Origin만 읽는게 아니라 내가 명시한 다른 신뢰 가능한 Origin으로부터 받은 리소스를 읽어들이는 법에 관해 알아보자.
참고로 CORS Request는 Simple Request일 수도 있고 Preflight Request일 수도 있다.
* Preflight Request의 경우
다른 Origin 요청을 보낼 때 미리 내 요청을 받을 수 있는지 확인하기 위해서 사전 요청(Preflight Request)을 보낸다. 그러고 나서 가능하면 나의 실제 요청을 보내고 응답을 받는다.
CORS Error 해결법 1: CORS를 잘활용하자
신뢰 가능한 특정 사이트는 "다른 Origin이더라도 허용해줘!"라고 서버가 응답할 때 알려주면 된다.
단순히 응답 헤더파일에 예외 사이트를 설정하는 것으로 CORS Error는 해결된다.
Simple Request인 경우
요청이 아래와 같을 때
- HTTP Method가 GET, POST, HEAD 중 하나
- Content-Type이 아래 중 하나이다
- text/plain
- application/x-www-form-urlencoded
- multipart/form-data
1. 이 경우에는 브라우저가 받은 요청이 어떤 Origin에서 시작됐는지 헤더를 추가한다.
2. 서버는 받은 CORS 요청이 유효한지 아닌지 Access-Control-Allow-Origin 헤더로 응답해준다.
Preflight Request인 경우
요청이 아래와 같을 때
- Simple Request가 아닌 경우
Preflight Request는 CORS를 지원하지 않는 서버를 위해 존재하는 것 같다. 그래서 본래의 요청을 보내기 전에 사전 요청을 보내서 체크를 한다. (나중에 정확한 이유를 조사해서 추가하겠다. 고수분들이 댓글로 알려주시면 감사하게 받겠습니다.)
구체적으로 알아보자!
응답하는 서버 - "특정 사이트는 다른 Origin이라도 허용해!"
"Access-Control-Allow-Origin": 사이트명
허용할 Origin을 Access-Control-Allow-Origin 응답 헤더에 넣어주면 된다. 그러면 다른 Origin일지라도 json 데이터와 같은 자원들을 응답하고 읽을 수 있게 된다.
- 모든 사이트를 허용하는 경우: "Origin을 Access-Control-Allow-Origin": *
- 사실 이러면 이러한 보안 정책을 사용하는 의미가 없다.
- 특정한 사이트만 허용하는 경우: "Origin을 Access-Control-Allow-Origin": https://www.coding-groot.tistory.com/
예시 코드 (실제로 사용하기 위한 코드가 아니라 간단하게 동작만 보여주려고 만든 코드)
Python Flask Server로 간단하게 CORS가 어떻게 동작하는지 확인해보자.
먼저 CORS 설정을 해주지 않은 Flask Server에게 다른 Origin에서 요청을 보낸 후, 그 응답을 읽어보려고 하면 어떻게 되는지 보여주겠다.
Flask Server (CORS 설정X) : 외부 자원이 차단되는 경우
# 간단한 Flask 서버 import flask app = flask.Flask(__name__) @app.route("/", methods=["GET", "OPTIONS"]) def index(): my_res = flask.Response("차단되지롱") return my_res app.run(host="127.0.0.1", port=8888)
HTTP-GET 요청을 하면 "차단되지롱"이라는 문자열로 응답해주는 간단한 Flask 서버이다.
Access-Control-Allow-Origin 헤더를 설정해주지 않고 127.0.0.1:8888에 접속해서 실행해보겠다.

간단한 Javascript 코드로 다른 Origin에서 Flask 서버의 응답을 받아서 읽어 오려고 시도한다.
// 간단한 HTTP-GET Request const http_req = new XMLHttpRequest() http_req.open("GET", "http://127.0.0.1:8888/") http_req.onload = () => console.log("Flask 서버로 부터의 응답은: " + http_req.responseText) http_req.send()
아래의 결과를 확인해보자. CORS Policy에 의해서 차단되었다!



Flask Server (CORS 설정O) : 외부 자원이 허용되는 경우
# 간단한 Flask 서버 import flask app = flask.Flask(__name__) @app.route("/", methods=["GET", "OPTIONS"]) def index(): my_res = flask.Response("차단되지롱") # Access-Control-Allow-Origin추가: '*'는 모든 사이트를 추가한다는 뜻. my_res.headers["Access-Control-Allow-Origin"] = "*" # 특정 사이트를 추가하려면 아래처럼 * 대신 넣으면 됨 # Example. my_res.headers["Access-Control-Allow-Origin"] = 'https://www.coding-groot.tistory.com/' return my_res app.run(host="127.0.0.1", port=8888)
HTTP-GET 요청을 하면 "차단되지롱"이라는 문자열로 응답해주는 Flask 서버를 수정했다.
모든 사이트(*)를 허용하도록 Access-Control-Allow-Origin 헤더를 설정했다.
다시 127.0.0.1:8888로 요청을 보내서 실행해보겠다.
(물론 모든 사이트를 허용하는 것은 Cross Origin Policy를 쓰는 이유가 없어지기 때문에 바람직하지 않다. 실제 개발이 아니라 블로그 글에서 작동 방식을 보여주기용으로만 사용하고 있다는 것을 주의하자.)

같은 Javascript HTTP GET 요청을 또 보내보자.
// 간단한 HTTP GET Request const http_req = new XMLHttpRequest() http_req.open("GET", "http://127.0.0.1:8888/") http_req.send() http_req.onload = () => console.log("Flask 서버로 부터의 응답은: " + http_req.responseText)


간단한 Javascript 코드로 다른 Origin에서 외부(Flask 서버) 응답을 받아서 읽어 오려고 시도했는데 잘 응답을 받아서 읽어왔다.
차이는 단 하나이다.

실제 코드
위의 코드는 어떻게 돌아가는 간단하게 보여주려고 짠 예시용 코드이다. 실제로는 여기에 추가적인 조치가 필요하다. 어떤 추가적인 조치가 필요한지 말하고 마무리하겠다.
다른 Origin에 요청을 보낼 때는 두 번의 요청을 한다고 했다. 첫 번째 사전 요청(Preflight Request)을 보내고 실제 요청을 보낸다. 사전 요청은 HTTP-OPTIONS 메서드로 보낸다. 사전 요청의 응답으로 실제 메시지 내용을 전달하지 않고 이게 어떤 환경에서 쓰이게 될 것인지 응답해줘야 한다. 어떤 종류의 HTTP 메서드를 허용할 것인가? 어떤 헤더의 요청을 허용할 것인가?를 알려줘야 한다. 어떤 HTTP 메서드를 허용할지 명시하지 않으면 기본적으로 GET, POST가 허용이 된다. 그렇기 때문에 우리가 위해서 했던 예시 코드가 동작했었던 것이다. 만약 위 서버에 자바스크립트로 HTTP-DELETE 메서드로 요청을 했다면 CORS Policy에 의해 차단당했을 것이다.
Flask Server : 사전 요청(Preflight Request)까지 올바르게 처리한 경우
# Flask 서버 import flask app = flask.Flask(__name__) @app.route("/", methods=["GET", "DELETE", "OPTIONS"]) def index(): my_res = flask.Response() http_method = flask.request.method if http_method == "OPTIONS": # 사전요청 print('--사전 요청(Preflight Request)--') my_res.headers.add('Access-Control-Allow-Origin', '*') my_res.headers.add('Access-Control-Allow-Headers', '*') my_res.headers.add('Access-Control-Allow-Methods', 'GET, DELETE') elif http_method == "GET": # 실제요청 print('--실제 요청--') my_res.headers.add('Access-Control-Allow-Origin', '*') my_res.set_data("가져왔지롱") elif http_method == "DELETE": # 실제요청 print('--실제 요청--') my_res.headers.add('Access-Control-Allow-Origin', '*') my_res.set_data("삭제했지롱") else: print(f"요구하지 않은 HTTP METHOD({http_method})입니다.") return my_res app.run(host="127.0.0.1", port=8888)
이 코드도 그렇게 깔끔한 코드는 아니지만 내가 전달하고 싶은 것은 사전 요청 부분이다!
위의 코드에서는 받은 요청 메서드가 OPTIONS인지 확인을 해서 사전 요청은 따로 처리하고 있다. 사전 요청은 HTTP OPTIONS 메서드를 사용한다. 특정 요청은 클라이언트가 HTTP OPTIONS 메서드로 허용할 Origin, Header, Method를 알려주고 다음 요청에서 실제 메시지를 보낸다.

* 참고로 Flask는 OPTIONS 메서드를 자체적으로 처리한다는 것 같다...? 그렇기 때문에 위 코드의 if http_method == "OPTIONS":
if문이 실행되지 않을 수도 있다. 코드는 참고만 하자.
* 사실 Flask에서는 내가 했듯이 귀찮게 헤더를 내가 추가하지 않고 간단하게 할 수 있는 모듈이 존재한다. 이번 글에서는 모든 환경에서 공통적으로 해당하는 내용을 보여주기 위해서 Flask의 자체적인 기능들을 쓰지 않고 설명한 것이다.
+ CORS 규격에 맞춰서 헤더를 설정하는 법을 이해하고 있다면 사실 굳이 모듈을 쓸 필요 없기도 하다.
CORS는 내가 어떤 Header, 어떤 Method, 어떤 Origin을 허용할 것인지도 세부적으로 설정할 수 있다.
"Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "https://www.codinggroot.tistory.com", "Access-Control-Allow-Methods": "OPTIONS, POST, GET"
내가 HTML을 호스팅한 Origin 주소만 허용 설정했고 허용할 Header를 설정했고 OPTIONS, GET 메서드만 허용 설정을 했다.
# 간단한 Flask 서버 import flask app = flask.Flask(__name__) @app.route("/", methods=["GET", "OPTIONS"]) def index(): my_res = flask.Response("차단되지롱") # Access-Control-Allow-Origin에 브라우저 HTML파일을 호스팅한 주소를 추가 my_res.headers["Access-Control-Allow-Origin"] = "http://localhost:3000" my_res.headers["Access-Control-Allow-Headers"] = "Content-Type" my_res.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" return my_res app.run(host="127.0.0.1", port=8888)
다른 것이 오면 잘 차단되는지 간단하게 확인해보자.

CORS Error 해결법 2: Proxy를 쓰자
해결법 1은 서버에서 처리해줘야 한다. 하지만 그게 안 되는 상황에서 쓸만한 방법이다.
특히, 이 방법은 프론트 개발을 하다가 간단히 Local 환경에서 띄워서 작업할 때 사용하기 좋다.
요청하는 곳의 Origin과 응답이 오는 곳의 Origin이 달라서 오류가 났었다. 사실 Origin만 일치시켜주면 해결이 된다.
Proxy를 쓰면 일치시켜줄 수 있다.
Proxy가 서버로 보내는 요청을 대리해서 요청해주고 응답을 해준다.
이제부터는 프론트쪽에서는 직접적으로 서버에 요청을 보내는 것이 아니라 Proxy에 보내면 되는 것이다.

Tip. 혹시 NodeJS를 쓴고 있다면 Webpack 개발 서버 프록시를 사용하면 편하다.
package.json에서 간단하게 proxy를 설정할 수 있다. 자세한 내용은 Velopert님의 글을 참고하자.
https://react.vlpt.us/redux-middleware/09-cors-and-proxy.html
CORS Error 해결법 3: CORS 기능을 끄기
어차피 웹 브라우저에서 검증하니 브라우저의 CORS 기능을 끄면 된다.
인정한다. 올바른 해결법은 아니다. 임시방편이다. 믿기 힘들겠지만 필자는 어쩔 수 없이 필요할 때가 있었다.
윈도우 업데이트조차 엄청 느리게 하는 폐쇄망에서 CORS가 적용이 안 된 버전의 Internet Explorer를 쓰다가 다음 날 출근하니 최신 Chrome을 쓰도록 모든 컴퓨터가 패치가 되었다. 그러니 교차 출처를 사용하던 내부 사이트가 모두 CORS에 의해 차단되며 난리가 났다. 보안상 허가되지 않은 프로그래밍을 하거나 외부 프로그램은 절대 사용하지 못했다. 사용해야 했던 사이트는 오래되어서 관리자조차 누군지 애매했다. 이로 인해 업무가 정체되기 시작했다.
정리하자면 이런 상황이라면 쓰자
- 웹 브라우저를 업데이트했더니 갑자기 잘 접속해서 쓰던 웹 사이트가 보안 프로그램의 응답을 불러오지 못하는 등 CORS에 의해서 구동이 안 되고
- 사내 보안 정책 때문에 프록시와 같은 설정을 하지 못하고
- 웹 사이트의 서버 설정을 건들 수 있는 권한도 없고
- 담당 개발자/운영자가 대응해줄 때까지 기다리기 힘든 상황
비활성화하는 법은 인터넷을 검색하면 자세히 나오기 때문에 간단하게 남겨두겠다.
# Chrome을 Web 보안 옵션을 비활성화해서 실행해주는 명령어입니다. # 처음의 chrome의 경로는 컴퓨터마다 다를 수 있습니다. ## MacOS 기준 open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security ## Windows 기준 "C:\Program Files\Google\Chrome\Application\chrome.exe" --user-data-dir=%LOCALAPPDATA%\Google\chromeTemp --disable-web-security
최소한의 웹 보안 정책도 비활성화게 되면 보안에 취약해지기 때문에 주의하자!
댓글
이 글 공유하기
다른 글
-
티스토리 사이드바에 GitHub Contribution Graph 넣는 방법
티스토리 사이드바에 GitHub Contribution Graph 넣는 방법
2020.06.06GitHub Contribution 그래프를 아래와 같이 임베딩하는 방법 1. 사이드바에 HTML을 넣을 수 있도록 해주는 배너 출력 플러그인을 적용한다 "블로그 관리 페이지 > 플러그인"에서 적용할 수 있다. 2. 사이드바에 모듈이 추가됐는지 확인한다 사이드바 페이지의 기본 모듈 카테고리에 [플러그인] HTML 배너출력이 있어야 한다. 3. HTML 배너출력 모듈을 우측 사이드바에 배치한다 노출되길 원하는 위치에 HTML 배너출력 모듈을 배치해주자. 4. GitHub Contribution 그래프를 가져오는 HTML 코드를 나에게 맞게 수정한다. 아래 코드의 22번째 줄 있는 "사용자명"을 자신의 GitHub 사용자명으로 바꾸고 전체를 복사한다. 나는 GitHub 사용자명이 IamGroooooot이기 … -
티스토리 글쓰기에서 완료 버튼 사라졌을 때 해결법
티스토리 글쓰기에서 완료 버튼 사라졌을 때 해결법
2020.05.04티스토리 편집창에서 하단의 완료 버튼이 사라질 때의 해결법! 티스토리 글을 쓰다가 맞춤법 검사를 하다 보면 가끔 하단의 바가 사라질 때가 있다. 그러면 맞춤법 검사를 완료하고 내 글을 올리고 싶은데 완료할 수가 없다….ㅠㅠ 어제 이 현상 때문에 글을 백업하다가 실수로 4시간 동안 쓴 글을 날렸는데 하…. ㅋㅋㅋㅋ 화나서 찾은 해결법을 공유해보고자 합니다. 해결법 맞춤법 검사 때문에 사라진 버튼은 우리 눈에서만 잠시 사라진 거지 실제로 삭제된 것이 아니다. 그렇기 때문에 클릭했을 때의 기능 자체는 사라지지 않았다! 기능을 직접 호출하면 된다. 프로그래밍을 몰라도 전혀 상관이 없고 그냥 따라 하면 별로 어렵지 않다! 1. 키보드의 F12키를 눌러서 개발자 도구를 연다 이렇게 생긴 창이 뜰 것이다. 2. … -
[Gatsby] Anchor Tag 대신 Link Component
[Gatsby] Anchor Tag 대신 Link Component
2020.04.11Gatsby에서 내부 페이지로 이동할 때 Link Component를 써야 하는 이유 Anchor Tag(텍스트) 이동하는 페이지를 전부 로딩한다 그렇기 때문에 이동하면서 번쩍거린다 부자연스러워 보인다 Link Component Gatsby’s component enables linking to internal pages as well as a powerful performance feature called preloading. 출처: https://www.gatsbyjs.org/docs/gatsby-link/#how-to-use-gatsby-link 내부 페이지 안에서 이동할 때 미리 로딩을 해서 자연스럽게 보이게 한다 gatsby 모듈에 정의되어 있다 링크로 이동할 때 (이질감 없이) 훨씬 자연스럽게… -
[리액트] 리액트 노트
[리액트] 리액트 노트
2020.02.25이 글은 리액트 튜토리얼 영상을 공부하면서 남기는 노트입니다. 영상에 있는 내용과 제가 아는 내용을 알아보고 정리하며 글을 쓰고 있습니다. 모든 출처는 아래에 있습니다. 리액트란? Open Source JavaScript Library (Framework가 아니라 Library이다!!) 장점 오직 UI를 만드는 용도이다보니 가볍다 페이스북이 유지 보수를 해준다 사용하는 사람도 많다보니 오류나 도움이 필요할 때 구글링하면 바로 나온다 인기가 많다 구체적인 장점을 더 알아보자. Component를 조합하는 방식 리액트는 전체 UI를 컴포넌트들을 조립하는 방식으로 구성한다. 그래서 다른 프로젝트의 컴포넌트를 가져와서 사용하기도 쉽고 재사용하기도 쉽다. (ex) Footer 컴포넌트만 다른 Footer 컴포넌트…
댓글을 사용할 수 없습니다.