각종 설치Issue & Solution & Enlightenment

CORS 에러는 무엇이고 어떻게 해결하는가

NandaNanda 2024. 3. 31. 08:30

참고: https://evan-moon.github.io/2020/05/21/about-cors/
https://beomy.github.io/tech/browser/cors/
https://xiubindev.tistory.com/115

실질적 해결 영상: 7:40초 가량 https://www.youtube.com/watch?v=d6suykcsNeY&t=257s

요약: Cross-Origin Resource Sharing의 줄임말로, 한국어로는 교차-출처 리소스 공유라고 한다. 교차 출처가 무엇이냐, 쉽게 말해 다른 출처라고 말할 수 있다.  CORS 에러가 어려운 이유 중 큰 이유가 CORS 에러를 실질적으로 겪는 곳은 프론트엔드이지만 해결해야하는 쪽은 백엔드이기 때문이다.

(출처(Resource)는 Protocol, Host, 포트번호를 의미한다. 여기서 중요한 한가지는 출처를 비교하는 로직이 서버단에서 구현되는 것이 아니라 브라우저단에서 이루어진다는 것이다.)

CORS 에러를 해결하는 방법 자체가 어렵지는 않아서 프론트든 백이든 해결방법을 알고 있다면 수월하게 해결할 수 있다(고한다.)

 

Cross-Origin Resource Sharing의 줄임말로, 한국어로는 교차-출처 리소스 공유라고 한다. 교차 출처가 무엇이냐, 쉽게 말해 다른 출처라고 말할 수 있다.

그럼 출처가 뭐지

URL 구조

출처가 무엇인지 알아보기 위해서는 URL 의 구조를 살펴보아야 한다.
서버의 위치를 의미하는 https://google.com 과 같은 URL 은 사실 여러 요소로 이루어져 있다.

💡 포트번호가 생략이 가능한 이유는 http, https 프로토콜의 기본 포트번호가 정해져 있기 때문이다. http://dsfds.com:870 과 같이 포트번호가 명시적으로 표기 된게 아니라면 http는 80번 https는 443번 포트가 디폴트 값이다.

출처(Origin)

이 때 출처는 Protocol, Host, 포트번호를 의미한다. 즉 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.


(일부러 위에 에러창 같이 캡쳐함.. 왜 공부하는지 다시 리마인드ㅎ)
브라우저 사용자 도구 콘솔창에 location.origin을 실행하면 출처를 확인할 수 있다.

같은 출처? 다른 출처?

웹페이지 주소 : https://brie.github.io

https://brie.github.io/about 같은 출처 Protocol, Host, Port 동일
https://brie.github.io/about?q=work 같은 출처 Protocol, Host, Port 동일
http://brie.github.io 다른 출처 Protocol 다름
https://brie.heroku.com 다른 출처 Host 다름
https://brie.github.io:81/about ? 일단 다른 출처 Port 다름

URL결과이유

여기서 마지막 경우 같은 경우는 위 내용으로만 비교하면 port 가 다른 경우인데 https의 디폴트 값인 443이 아니라 81로 설정해주었기 때문에 다른것임. 그치만 예시 주소에 포트번호가 명시되어있지 않기 때문에 실질적으로는 판단하기 애매하다고 한다. RFC 6454의 Comparing Origins 섹션에는 “만약 출처가 스킴/호스트/포트의 삼중 체계라면…” 이라는 전제가 붙어있기 때문에 어떻게 해석하냐에 따라 구현이 달라질 수 있기 때문이다.

✔️ http or https 👉🏼 프로토콜
✔️ naver.com or jyejyes.github.io 👉🏼 호스트
✔️ :80 or :81 or :443 👉🏼 포트번호
이 셋을 다시 기억하자 !

여기서 중요한 한가지는 출처를 비교하는 로직이 서버단에서 구현되는 것이 아니라 브라우저단에서 이루어진다는 것이다.
그래서 cors 정책을 위반하는 리소스 요청을 하더라도 서버단에서 같은 출처에서 온 요청만 받겠다는 설정을 따로 해둔것이 아니라면 일단 정상적으로 응답한다. 그 뒤 브라우저가 이 응답을 분석해서 CORS 위반이라고 생각하면 그 응답을 버린다.

(참고) SOP 개념도 알아두자

SOP, Same-Origin Policy 의 약자이다.
웹 생태계에서는 다른 출처로부터 리소스 요청을 제한하는 것과 관련된 두 가지 정책이 존재한다. 하나는 CORS 이고 하나는 SOP 이다.
SOP 는 '같은 출처에서만 리소스를 공유할 수 있다' 는 규칙을 가진 정책이다.

그러나 웹이라는 환경에서는 다른 출처에 있는 리소스를 가져와서 사용하는 일은 매우 흔하다. 그래서 SOP 에 대한 예외 조항을 두고 이 예외에 해당하는 요청은 출처가 다르더라도 허용하기로 하였는데 이가 "CORS 정책을 지킨 리소스 요청" 이라고 할 수 있다. (참고로 CORS 라는 이름은 SOP 등장보다 빠르다고 한다 - ?)

그러므로 우리가 다른 출처로 리소스를 요청한다면 SOP 정책을 위반한 것이 되고, 거기에 예외 조항인 CORS 까지 지키지 않으면 아예 다른 출처의 리소스를 사용할 수 없게 되는 것이다.


📌 CORS의 동작원리가 뭐지 ?

기본적으로 웹에서 다른 출처로 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데 이 때 브라우저는 origin이라는 필드에 요청을 보내는 출처를 담아서 보낸다.

Origin: http://jyejyes.github.io 

이후 서버가 이 요청에 대한 응답을 할 때 응답헤더 Access-Control-Allow-Origin 라는 값에 이 리소스에 접근하는 것이 허용된 출처를 같이 보내주고, 응답을 받은 브라우저는 자신이 보낸 Origin 과 서버가 보내준 응답의 Access-Control-Allow-Origin 를 비교한 후 이 응답이 유효한지 판별한다.

기본적인 흐름은 이렇지만 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경되기 때문에 어떤 시나리오에 해당하는지 잘 파악한다면 CORS 에러에 대응하는 것이 더 수월해질 것이다.

✔️ 1. Preflight Request

가장 많이 마주치는 방식.
브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.

이 때 브라우저가 본요청을 보내기 전에 보내는 예비요청을 preflight 라고 부른다. (이 예비 요청에는 HTTP 메소드들 중 OPTIONS 메소드가 사용된다고 한다.- ?) 예비 요청의 목적은 본요청을 보내기 전에 브라우저가 이 요청을 보내는 것이 안전한지 확인하는 용도


preflight 방식도 위에서 설명한 출처 판별과 같은 방식으로 이루어진다.
앞에 예비 요청이 포함 될 뿐

  1. 브라우저가 보내는 http 프로토콜의 Origin 필드에 리소스를 요청하는 출처를 보낸다.
  2. 브라우저는 서버 응답의 Access-Control-Allow-Origin 필드 값을 보냈던 Origin 값과 비교하여 CORS 에러 여부를 판별한다.

이 때 CORS 에러는 예비 요청의 성공여부와는 별 상관이 없다. 브라우저가 CORS 여부를 판단하는 시점은 예비 요청의 응답을 받은 이후이기 때문이다.

예비 요청 자체가 실패해도 CORS 정책 위반으로 처리 될 수 있지만 중요한 것은 예비 요청의 성공/실패여부가 아니라
'응답 헤더에 유효한 Access-Control-Allow-Origin이 있는가'
이다. 그래서 예비 요청이 실패해서 성공 코드가 아니더라도 헤더에 저 값이 제대로 들어가있다면 CORS 정책 위반이 아니라는 의미이다.

✔️ 2. Simple Request

MDN의 cors 문서에서 Simple request 라고 부른다.
단순 요청은 예비 요청 없이 바로 서버에서 본 요청을 보내는 것이다.

즉, preflight 와 로직은 같지만 예비 요청의 유무만 다른 것이다.


하지만 아무 때나 simple request 를 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있는데 이 조건이 생각보다 까다롭기 때문에 잘 경험할 수 없는 방법이라고 한다.

✔️ 조건
1. 요청의 메소드는 get, head, post 중 하나
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
사실 이 부분은 잘 이해가 되지 않기 때문에 대략적으로 읽고 넘긴다.

✔️ 3. Credentialed Request

인증된 요청을 사용하는 방법.
이 방식은 CORS의 기본적인 방식이라기보다는 다른 출처 간 통신에서 보안을 좀 더 강화하고 싶을 때 사용된다.

기존 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이 때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션 이다.

3가지의 옵션이 들어갈 수 있으며 각 값들의 가지는 의미는 아래와 같다.

same-origin (기본값) 같은 출처 간 요청에만 인증 정보를 담을 수 있다
include 모든 요청에 인증 정보를 담을 수 있다
omit 모든 요청에 인증 정보를 담지 않는다

옵션 값설

만약 same-origin이나 include 와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin 만 확인하는 것이 아니라 좀 더 빡빡한 검사 조건을 추가하게 된다.

더 자세히 알아보면 좋지만 현재는 대략적인 방법만 알아보는 걸로 (22.04.26)


 

************************📌 CORS 에러 해결 방법  ************************

정석적인 방법 : 서버에서 해결하기

동작원리를 보면, 서버에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함하여 응답을 브라우저로 보내면 CORS 에러를 해결할 수 있다. 프론트단에서 CORS 에러를 발견했다면 서버에게 Access-Control-Allow-Origin에 유효한 값을 포함해서 달라고 요청해야 한다.

와일드 카드격인 * 를 사용하여 Access-Control-Allow-Origin에 헤더를 세팅하면 모든 출처에서 오는 요청을 받겠다는 의미이므로 당장은 편하겠지만 보안적으로 심각한 이슈가 발생할 수 있다.

그러니 Access-Control-Allow-Origin:특정주소 와 같이 출처를 명시해주도록 합시돠.

Node.js의 Express 는 cors 라는 서드파티 미들웨어를 지원한다고 한다. 이처럼 다른 프레임워크에서도 cors 를 해결해 주는 라이브러리가 존재한다.

구체적인 CORS문제 해결은 아래 링크의 동영상 7:40 부분에 나와있다. 요약하면 가장 기본적으로는 서버에서 해결해주는 방법이 있고 npm install cors 후 아래와 같이 하면 된다. (https://www.youtube.com/watch?v=d6suykcsNeY&t=257s)

출처: https://www.npmjs.com/package/cors

 

(Slack clone coding 에서 소개해준 방법)

나는 프론트인데.. : 프론트에서 해결하기

백엔드 개발자와 협업을 통해 만들어진 api 에서 CORS 에러가 났다면 백엔드 개발자에게 부탁해서 해결할 수 있지만 백엔드 개발자와 소통이 불가능한 상황이라면? 가령 오픈 API 를 사용한다던지, 프엔 개발자라면 혼자 해결해야한다.

1. 남이 만든 프록시 서버 사용하기

프록시 서버는 클라이언트가 프록시 서버 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다. 쉽게 말해 브라우저와 서버 간의 통신을 도와주는 중계서버라고 이해하면 된다.

요청해야하는 URL 앞에 프록시 서버 URL 을 붙혀 요청하게 되면 해결할 수 있다.

https://cors-anywhere/herokuapp.com

이 서버를 사용하면 중간에 요청을 가로채서 HTTP 응답헤더에 Access-Control-Allow-Origin : * 를 설정해준다.

axois({
  method:"GET",
  url:`https://cors-anywhere/herokuapp.com/{주소},
  header:{
	'APIKey':어쩌구
  }
})

sleact프로젝트에서의 실례. 프론트엔드에서 api로 시작하는 벡엔드에 보내는 요청은 주소를 3095로 바꾸어서 보내겠다는 뜻!!!

(devServer를 설치한 이유(중요): 1. 핫리로딩(hot reloading)(port, devMiddleware)을 하기 위해 2. 주소를 사기치기 위해(historyApiFallback) 3. CORS문제를 해결하기 위해 (proxy설정)

 

프론트엔드에서 api로 시작하는 벡엔드에 보내는 요청은 주소를 3095로 바꾸어서 보내겠다는 뜻!!! ==>> 이것 때문에 axios라이브러리를 사용할때 입력하는 url란에 전체 url(http://localhost:3095)이 아닌 단순히 /api/~ 만 입력하면 되는 것!!! 바로이 devServer의 proxy덕분이었음.

2. 내가 직접 서버 구축하기

pass 👉🏼

3. 로컬 한정 http-proxy-middleware 사용하기

배포하기 전 로컬환경 한정일 때 사용하면 좋은 라이브러리이다.
http-proxy-middleware를 설치하고
setupProxy.js 라는 파일을 src 폴더 내에 만들고 아래 코드를 작성해준다.


카카오톡 책 검색 api CORS 에러 때문에,, 아니 덕분에 CORS 가 무엇인지 동작원리가 어떻게 되는지, CORS 에러가 무엇인지 등등 자세히 공부할 수 있었던 시간이 된 것 같다. 이제 에러를 해결해보자.