Why?
부스트캠프 멤버십에서 최종 프로젝트를 진행하면서 기술공유를 할 기회가 있었는데,
프로젝트를 ncloud 인스턴스에 배포하면서 겪은 문제를 정리한 글이다.
개발을 하면서 상당히 짜증나는 이슈인 CORS 에러를 해결하기 위해 학습하면서
웹팩 개발 서버의 proxy 옵션에 대해서 더 이해하였고, 설정하는 방법 등을 포함 해 정리하였다.
CORS?
배포 환경 뿐 아니라 심지어 로컬 환경에서도 자주 보이는
녀석 아니
친구..
CORS. 일명 코스는 Cross Origin Resource Sharing의 약자이다.
mdn에 의하면
CORS는 한 출처에서 실행 중인 웹 애플리케이션이
다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
웹 애플리케이션은 리소스가 자신의 출처와 다를 때 교차 출처 HTTP 요청을 실행한다고 한다.
브라우저에서 API 요청시 브라우저의 현재 주소와, API 서버의 출처가 일치해야 요청한 데이터와 만날 수 있다.
다른 출처에서 보안이 담긴 API 요청을 하려면, CORS 설정이 필요하다.
여기서 출처는, 프로토콜 + 호스트 + 포트 로 이루어진다.
출처의 구성요소에 포트까지 포함되어 있는 것을 볼 수 있다.
출처의 구성에 따르면, localhost:3000 -> localhost:3001 으로의 요청 또한 다른 출처로 취급된다는 것이다.
실제로, 로컬 환경에서 3000번 포트를 리액트 프로젝트, 3001번 포트를 express 서버 등으로 돌려서 요청해보면
CORS 에러를 만날 수 있다.
그래서 요청과 응답시 출처와 관련된 부분은?
브라우저는 요청 헤더 Origin
에 출처
를 함께 담아서 보내준다.
서버는, 응답 헤더에서 Access-Control-Allow-Origin
이라는 값에 허용된 출처
를 내려주고,
브라우저는 자신이 보낸 요청의 Origin
, 그리고 응답의 Access-Control-Allow-Origin
값을 비교해서 유효한지 검증한다.
만약 여기서 브라우저가 유효하지 않다고 판단하면, 사용자는 서버로부터 응답을 받을 수 없게 된다.
Fetch API
Fetch API를 통한 요청 시에, 특히 로그인 등의 기능을 구현할 때 CORS 이슈를 맞이할 것이다.
mdn에서 fetch에 대한 설명을 보면,
fetch API는 same-origin(동일한 출처)가 아니면 쿠키를 보내지 않는 것이 기본적으로 설정되어 있기 때문이다.
로그인 기능은 보통 쿠키를 많이 이용하기 때문에, 쿠키가 보내지지 않는 현상을 맞이하면서 cors에러를 맞이한다.
(이게 참 처음에는 대체 왜 쿠키가 안오는거야? 안보이는건가? 하면서 당황스럽더라.)
아무튼, 그래서 자격증명(credentials) 라는 옵션을 추가해줘야 한다.
Credentials(자격증명)
fetch를 이용해 브라우저에서 자격 증명이 포함된 인증서를 보낼 때 자격증명(credentials)에 대한 옵션을 추가해야 한다.
crentials에는 3가지 옵션이 있는데, 기본값인 'same-origin', 'omit', 그리고 'include' 가 있다.
same-origin
같은 origin(출처)를 가질 때만 자격증명을 전송한다.
- localhost:3000 -> localhost:3000 으로의 요청만이 자격증명을 포함하겠다는 것이다.
omit
자격증명을 포함하지 않아서 브라우저 보안을 유지하는 것을 원한다면 부여하는 옵션이다.
자격증명을 누락시키겠다는 의미 그대로 생각하면 되겠다.
include
cross-origin 요청에서도 자격 증명을 포함하는 옵션값이다.
- 이 옵션을 통해, 다른 출처로의 자격 증명을 포함한 요청을 보낼 수 있다.
그래서 다른 출처에 요청해야 하는 상황에서 자격증명이 필요한 요청에서의 CORS 에러를 해결하려면,
서버와 클라이언트 모두 설정을 해야 한다.
1. 서버에서는 특정한 도메인의 요청만 허용
해야 한다.
우리의 서버는 OPEN API가 아니기 때문에.
OPEN API일 경우 모든 도메인의 요청에 대해 허용해야 한다.
2.위에서 언급 했듯이, 브라우저에서 다른 도메인으로의 요청시 fetch API가 기본적으로 쿠키를 보내지 않는다.
그러므로 자격증명(ex. 쿠키)를 보내기를 허가해야 한다.
서버에서 cors 설정하기
서버에서 특정 도메인의 요청만 허용하기 위해서는, cors 미들웨어에서 2가지 조건이 필요하다.
1. Access-Control-Allow-Origin
에는 *를 사용할 수 없으며, 명시적인 URL
이어야한다.
2. 응답 헤더에는 반드시 Allow-Control-Allow Credentials: true
를 설정해야 한다.
Access-Control-Allow-Origin을 특정해줘야 하는 이유는,
브라우저의 fetch API 사용시 'crudentials:include' 옵션을 부여하면 다음과 같은 에러를 볼 수 있다.
crudentials:include 모드의 요청을 받은 서버는
응답 헤더의 Access-Control-Allow-Origin 옵션에 와일드카드(*)가 아닌 다른 명시적인 url을 알려줘야 한다.
즉 "응답해줄 url을 특정하세요." 라고 친절하게 안내해준다.
이제 서버의 코드에 CORS 옵션을 추가해보면,
origin 옵션에 클라이언트 프로젝트의 url을 등록하고,
credentials 옵션을 true로 설정해준다.
이렇게 되면 서버는 위에서 언급한 2가지 조건을 만족한다.
그 밖의 maxAge 등의 옵션들에 대해서는 생략하겠다.
클라이언트에서 cors 설정하기
클라이언트에서는 요청 시 자격증명을 전송하는 credentials 옵션을 추가하면 된다.
fetch API 사용시 자격 증명(ex. 쿠키)을 포함해야 한다면 credentials:'include' 옵션을 추가한다.
axios 사용시 withCredentials:true 옵션을 추가한다.
axios는 기본값 설정이 가능한데, 모든 요청에 대해 기본값으로 사용하는 옵션이다.
이렇게 서버, 클라이언트 모두 설정이 끝나면
클라이언트는 요청 시 자격증명을 포함하여 요청을 하게 되며,
서버는 cors 설정에서 요청을 허용할 클라이언트(출처)를 지정했으므로, 더이상 CORS 에러는 발생하지 않게 되겠다.
프로젝트를 배포하거나, 배포한 API 서버를 이용해야 한다면?
가상환경 등에 프로젝트를 배포하는 경우에서도 동일하다.
가상환경에서 프론트엔드 서버와 백엔드 서버가 존재한다면, 백엔드 서버에서 프론트엔드 서버의 요청만을 특정해주면 된다.
그러나 만약, 가상환경에 배포한 백엔드 API 서버를 로컬환경(내 pc의 localhost)에서 이용하고 싶다면?
현재 백엔드 서버는 가상환경의 프론트엔드 서버의 요청만을 허용해주고 있다.
내가 직접 백엔드 서버 코드에 접근할 수 있다면 백엔드 서버에서 로컬서버에 대한 요청 또한 cors 옵션에 추가해주면 된다.
하지만 내가 백엔드 서버에 접근할 수 없다면?
내 팀원인 백엔드 개발자에게 옵션 추가를 요청하거나, 내가 손쓸 방법이 없을 수도 있다.
이런 상황에서 내 프론트엔드 프로젝트에 웹팩이 적용되어 있다면, 웹팩의 proxy 옵션을 사용해보자.
(A,B가 안되면 이걸 해라! 라고 표현한 것 처럼 되었지만, 내가 백엔드 서버를 제어 할 수 있어도 프록시를 사용하면 훨씬 편하다.)
(물론 http-proxy-middleware 패키지를 사용하는 방법도 있고, 간단하게 몇 줄의 코드로 프록시를 만들수도 있다.)
웹팩 개발 서버의 Proxy
현재 우리가 백엔드 서버에 대한 제어권이 없다면,
로컬호스트의 리액트 등의 프론트엔드 프로젝트에서 API 서버로 보내는 자격증명이 필요한 요청은 모두 CORS에러를 맞이할 것이다.
웹팩 개발 서버의 proxy 옵션을 이용하면 이 CORS에러를 피할 수 있다.
Proxy 설정하기
프록시는 일종의 브라우저와 요청할 백엔드 서버 사이에서 중개 역할을 하는 서버이다.
아주 간단하게, 요청을 보낼 url을 proxy 옵션에 적어주고 프로젝트를 webpack-dev-server로 실행하면 된다.
위와 같은 그림이 프록시가 동작하는 방식인데,
이 프록시의 작업 수행 절차를 짧게 설명하면
1. 브라우저에서 API 요청시 API 서버에 직접 요청하는 것이 아닌,
지금 리액트 등이 구동되는 데브서버 주소로 요청한다.
2. 웹팩 데브 서버에서 요청을 받으면, API 서버에게 전달한다.
3. API 서버에서의 응답을 받은 내용을 다시 브라우저에게 반환한다.
이렇게 프록시를 설정하고 나면 이제 fetch 작업을 수행할 때
fetch("http://localhost:8081/api/room")
대신,fetch("/api/room")
로 요청해주면 된다.
→ 요청 주소 도메인을 생략하면, 현재 도메인(React의 경우 localhost:3000) 으로 요청을 보내는데
→ 위에서 정리한 proxy가 3000번으로의 요청을 받아서 8081번에 요청하고 응답 데이터를 3000번으로 돌려준다.
크롬 개발자도구의 네트워크 탭에 가면, http://localhost:3000으로 요청한 모습을 볼 수 있다.
그런데 왜 프록시 옵션을 추가하면 요청이 잘 수행되는가?
프록시 옵션을 부여하면, 현재 도메인으로 보내고 프록시가 대신 요청을 수행하겠다는 말은 이해했다.
그래서 왜 프록시의 요청은 서버가 거절하지 않는 것인지 궁금했다.
요약하자면,
서버에서 서버로 요청을 하게될 때에는 브라우저의 규약인 CORS 정책에 영향을 받지 않는다.
그러한 이점을 이용하여 Proxy Server라는 출처를 통하기 때문에
CORS 정책을 위반하지 않게되어 우회 할 수 있게 된다.
프록시는 헤더를 추가하거나 요청을 허용/거부하는 역할을 중간에서 해줘서Access-Control-Allow-Origin : *의 헤더를 담아 응답
한다.
나는 CRA로 프로젝트를 만들었는데, webpack.config.js 설정 파일이 없다.
create-react-app 명령을 통해 프로젝트를 만들면, 웹팩 설정 파일이 보이지 않는다.
하지만, 리액트 프로젝트를 경험했다면 웹팩이 동작하고 있다는 것을 알고 있을 것이다.
이것은 CRA에서 숨겨놨기 때문에 없는 것 처럼 보이는 것이다.
- node_modules/react-scripts/config 디렉토리에 접근해보면 웹팩과 여러 설정 파일들을 볼 수 있다.
CRA로 설치한 프로젝트의 package.json을 보면 react-scripts 의존성이 보이는데,
이 패키지에서 리액트의 설정 파일들을 자동으로 관리해준다.
npm run eject
명령을 수행하면 이 설정 파일들을 프로젝트로 꺼낼 수 있는데,
문제는 한 번 꺼내면 롤백이 불가능하다.
즉 이 명령을 통해 설정 파일들을 꺼냈다면 이제 react-scripts로 인한 자동 업데이트가 불가능하니,
웹팩 설정 등에 변화가 생긴다면 직접 수동으로 업데이트 해줘야 한다.
정말 꺼낼거니? 그럼 롤백이 불가능하단다. 라는 경고를 보여준다.
webpack.config.js에 프록시 설정을 하려면 꺼내야 하지 않나?
간편하게도 package.json에 옵션을 추가해주면 프록시를 설정할 수 있다.
webpack 설정파일에 설정하는 것 처럼
proxy 옵션에 요청하고자 하는 API 서버의 주소를 부여한다.
심지어는 여러 프록시 서버를 설정할 수도 있다.
이와 같이 설정하면
1. fetch('/api/customers'); // https://api.example1.com/api/customers
2. fetch('/auth/my-info'); // https://api.example2.com/auth/my-info
3. fetch('/images/gogo.png'); // https://api.example3.com/images/gogo.png
각기 다른 3개의 서버에 보내는 요청에 프록시를 사용할 수 있다.
개발환경과 배포환경에서 요청을 다르게 하기
배포 환경에서는 웹팩개발서버를 사용하지 않으므로 프록시 옵션을 사용할 수 없다.
물론 우리는 이미 위에서 배포 환경에서의 cors 옵션들에 대해 설정을 마쳐놨다.
그리고 로컬환경에서 개발시에는 웹팩개발서버의 프록시 옵션을 사용하면 되겠다.
그렇게 하기 위해서는,
배포 환경에서는 https://api.example.com
으로 브라우저가 직접 요청을 보내야 하고,
개발 환경에서는 /
(자기 자신) 으로 요청을 보내서 프록시가 처리하도록 해야 한다.
process.env.NODE_ENV === 'development'
와 같은 옵션으로 현재 환경의 모드를 파악하면,
baseUrl 와 같은 변수에 설정해두면, 환경 모드에 따라 변수를 달리하기 때문에 더이상 개발자는 요청경로에 대해 신경쓰지 않아도 된다.
글을 마치며
사실 프로젝트를 진행하면서, 프록시 설정에 대해서 잘 알지 못한 상태에서 우선 유용해보였던 프록시 설정을 해서 개발을 진행했었고
가상환경에 배포하고 나니 이유모를 에러가 발생했었다.
ERROR SyntaxError: Unexpected token < in JSON at position 0
다음의 에러였는데,
열심히 검색해서 수정하고 수정해도 결국 해결이 되지 않았었다. 아마 이 에러가 발생하는 이유도 다양한 것 같다.
차근차근 다시 생각해보다가 가상환경에 배포를 하면서, 리액트 앱 자체가 아닌 빌드 결과의 스태틱 파일들만을 올려두고
그 파일을 실행해서 프론트엔드 프로젝트를 배포했었다는 점을 생각했다.
요약하자면, 가상환경에 배포한 프로젝트는 웹팩이 동작하지 않는 것이다.
프록시 옵션을 사용하기 위한 설정만을 두고 배포를 진행했으니, api 요청이 제대로 동작할 리가 없었던 것.
그래서 급하게 서버와 클라이언트에 cors 옵션, credentials와 baseUrl을 설정해줬고, 제대로 동작하더라..
(사실 별 것 아닌 것 처럼 썼지만, 데모 전날 새벽에 이 문제를 맞이했던 상황이었고
JSON at position 0에 대한 에러 파악이 안되어서 팀원들 모두가 새벽동안 열심히 고민했었다..)
아무튼 다음 날 아침에 이 이슈에 대해서 간단하게 정리를 했고,
마침 기회가 되어서 우리 조원들과 약 60여명 정도의 부스트캠프 캠퍼분들과 공유하는 좋은 경험을 할 수 있었다.
참고한 출처
https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch
https://developer.mozilla.org/ko/docs/Web/API/Request/credentials
https://velog.io/@geunwoobaek/CORS-%EC%9B%90%EC%9D%B8-%EB%B0%8F-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95
https://react.vlpt.us/redux-middleware/09-cors-and-proxy.html
'Browser' 카테고리의 다른 글
비동기 작업 취소하기. "AbortController" (0) | 2022.07.24 |
---|---|
웹 크롤링과 웹 크롤러 봇 (0) | 2021.11.14 |