[Spring/React] CORS? CORS에러를 해결해보자

kindof

·

2021. 8. 13. 13:48

1. 들어가면서

 

CORS 에러

 

CORS 에러는 누구나 한번쯤 만나보게 되는 고통스러운 문제입니다. 저 역시 인턴을 하면서 CORS 문제 때문에 하루종일 골머리를 앓았는데요.

 

이렇게도 해보고 저렇게도 해보고 하면서 결국 프록시 우회로 해결하긴 했는데, 그 시간을 되짚어보면서 정확히 CORS란 무엇이며 이로 인해 일어나는 에러를 어떻게 해결해야는지 정리해보려고 합니다.

 

 

 

2. 교차 출처 리소스 공유(CORS) 

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS) HTTP 헤더를 사용해서 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.

 

무슨 말일까요? 만약 https://domain-a.com의 자바스크립트 코드가 FetchAxios API를 사용하여 https://domain-b.com/data.json을 요청한다고 해봅시다.

 

이 때 API를 통해 리소스를 불러오는 웹 어플리케이션은 자신의 출처와 동일한 리소스만불러올 수 있고 이를 동일 출처 정책이라고 합니다(CORS랑 약간 구별되는 개념입니다). 그래서 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야만 합니다.

 

CORS에 대해 조금 더 이야기하기 전에 CORS를 이야기할 때 중요한 개념인 동일 출처 정책’과  Preflight Request에 대해 먼저 짚고 넘어가보겠습니다.

 

 

2-1. 동일 출처 정책(Same-Origin Policy, SOP)

두 URL의 프로토콜, 포트(명시한 경우), 호스트가 모두 같아야 동일한 출처라고 말한다.

 

여기서 프로토콜이란 쉽게 생각해서 httphttps의 차이이며,

호스트는 http://hostNameA.com/ http://hostNameB.com/의 차이,

그리고 포트는 http://hostName.com:80 http://hostName.com:81 의 차이라고 볼 수 있습니다.

 

그러면 위에서 예시로 든 https://domain-a.comhttps://domain-b.com/data.json은 호스트가 다르기 때문에 다른 출처라고 볼 수 있겠죠?!

 

동일 출처 정책은 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 이를 통해 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.

 

 

2-2. Preflight Request

Preflight Request는 아래와 같이 OPTIONS 메서드를 통해 다른 도메인의 리소스에 요청이 가능한 지 확인하는 요청을 말합니다.

 

Preflight Request - 예비 요청

 

예를 들어 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면, 브라우저는 서버에게 Preflight Request를 먼저 보내게 되고, 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내주게 됩니다.

 

이후 브라우저는 자신이 보낸 Preflight Request와 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 가능하다면 같은 곳으로 진짜 요청을 보내게 되는 것이죠.

 

이를 직접 살펴보기 위해 네이버 브라우저의 개발자 콘솔 도구에서 아래와 같이 저의 티스토리 블로그 파일에 대한 GET 요청을 보내보겠습니다. 그러면 아래처럼 에러 문구가 쭉 나오고, 네트워크 탭에서 Preflight Request의 요청, 응답 헤더를 볼 수 있습니다.

 

CORS 에러
Preflight Request

 

자 그러면, 에러 문구를 해석해보겠습니다. 일단 CORS 정책에 의해 요청이 실패했는데, 그 이유를 살펴보면 preflight request의 응답으로 돌아온 Response Header의 'Access-Control-Allow-Origin(서버 측 허가 출처)'과 현재 요청을 보내는 브라우저의 출처가 다르기 때문입니다.

 

즉, preflight request에서 응답으로 받은  'Access-Control-Allow-Origin'은 'https://studyandwrite.tistory.com'이기 때문에 Naver는 티스토리 블로그 자원 요청을 할 수 있는 Origin이 아니라는 뜻이죠. 이로 인해 결국 본 요청이 CORS 정책에 의해 막히게 되는 것입니다.

 

이해가 되셨나요?

 


 

3. CORS 해결하기

 

다시 돌아와서, 그러면 다른 출처에서 리소스를 가져와야 할 때는 어떻게 해야할까요? , CORS 에러를 해결하려면 어떻게 해야할까요?

 

예를 들어, https://foo.example 의 웹 컨텐츠가 https://bar.other 도메인의 컨텐츠를 호출하기 원한다고 해봅시다.

const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

 

이 때 서버로 전송되는 REQUEST는 아래와 같을 것입니다.

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

 

요청 헤더의 Origin을 보면 https://foo.example로부터 요청이 왔다는 것을 알 수 있습니다.

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]

서버는 이에 대한 응답으로 Access-Control-Allow-Origin: '*', 으로 응답하고, 이제 모든 도메인에서 해당 서버에 대한 리소스 접근이 허용됩니다.

 

 

!

 

 

 

인줄 알았지만,

 

잘 생각해보면 Access-Control-Allow-Origin은 클라이언트인 내가 하는 게 아니라, 리소스를 가져오길 원하는 서버에서 설정하는 것입니다.

 

따라서 CORS 에러의 근본적인 해결책은 서버에서 Access-Control-Allow-Origin 정책을 수정해줘야 하는 것이고, 내가 HTPP 응답 헤더인 Access-Control-Allow-Origin을 설정할 수 없는 것이죠.

 

그러면 어떻게 해야 할까요? 응답 헤더에 어떻게 하면 Access-Control-Allow-Origin을 넣을 수 있을까요?

 

📃 3-1. 이미 존재하는 프록시 서버 이용하기

서버 사이드는 CORS에 대해 서버에서 응답을 막는 것이 아니라, 브라우저에서 CORS 정책을 위반하면 응답을 파기합니다.

 

따라서, CORS 문제를 해결하기 위해 이미 구축된 프록시 서버를 사용하면 프록시 서버가 중간에 요청을 받은 뒤 HTTP 응답 헤더에 Access-Control-Allow-Origin : “*” 를 설정하여 보내줄 수 있고, 이를 통해 브라우저가 CORS 정책을 지킬 수 있게 되죠.

 

공부할 때 찾아본 프록시 서버에는 https://cors-anywhere-herokuapp.com이라는 사이트가 있었는데, 지금은 막힌 것 같습니다. 그러면 Access-Control-Allow-Origin : “*” 를 설정해줄 프록시 서버를 어디서 얻어야 할까요?

 

 

✅ 3-2. 미들웨어 서버 구축하기

제가 CORS 문제를 해결했던 방법입니다. 프로젝트를 할 때 스프링과 리액트를 사용하고 있었기 때문에 스프링을 미들웨어 서버로 사용하고, 실제 요청을 보내는 페이지에서 오는 응답을 스프링이 받아서 나에게 다시 보내주는 프록시 역할을 하게 만들었습니다.

 

이를 위해서는 첫번째로 React에서 package.json에 다음과 같이 프록시 설정을 해줘야 합니다. 그러면 스프링부트 서버가 띄워질 때 기본적으로 localhost:8080이 띄워지기 때문에 이를 이용한다는 계획입니다.

 

React 프록시 설정

 

그리고 요청을 보낼 때는 URL 주소 앞에 해당 프록시 서버의 URL을 붙여서 보내주면 되는데, 저는 스프링의 컨트롤러를 호출하기 위해 /api/{controllerName} 과 같이 URL을 만들어주었습니다.

 

그리고 GET 메서드를 POST 메서드로 바꾸고 실제 요청하는 URL이나 처리해야 할 작업들을 body에 넣어서 전송했습니다.

요청

 

한편, 스프링에서 리액트에 다시 넘겨줄 때 Access-Origin을 설정해줘야 하는데요. 이를 위해서 Configuration 파일 하나를 생성한 뒤, 아래와 같은 코드를 작성해줍니다.

@Configuration
public class CorsConfiguration implements WebMvcConfigurer{
    
    @Override
    public void addCorsMappings(CorsRegistry registry){
        registry.addMapping("/apply")
            .allowedOrigins("http://localhost:포트번호");
    }
}

 

그러면 결과적으로 스프링 컨트롤러에서는 아래와 같이 /apply에 맞게 동작하는 컨트롤러가 호출되고, 위 그림의 requestURL에 원하는 작업 요청을 보낸 후 응답을 받아 다시 클라이언트(리액트)에게 리턴해주게 됩니다.

 

code

 

지금까지 설명한 내용을 그림으로 표현하면 아래와 같은 구조가 됩니다. 리액트에서 요청하는 URL와 데이터를 Spring 서버에 넘겨주고, 스프링 서버에서 해당 URL에 요청을 보내고 응답을 받습니다. 그리고 최종적으로 해당 응답을 프론트엔드 사이드에 넘겨주게 됩니다.

 

 

 

정리

 

4. 나가면서 

CORS 에러를 해결하기 위해서 정말 많은 블로그들을 찾아보고 여러 방법을 시도했는데, 잘 해결이 되지 않아서 힘들었던 것 같습니다.

 

제가 올린 해결 방법도 누군가의 개발 환경에서는 안 될 수 있으니 자신이 지금 어떤 상황에 놓여있는지, CORS 에러가 왜 생기는지 생각해보고 차근차근 해결해나가면 좋을 것 같습니다.