ABOUT ME

-

Today
-
Total
-
  • 나를 너무나 힘들게 했던 CORS 에러 해결하기 😂
    Study/Frontend 2021. 4. 21. 18:39
    반응형

     

    🔥 사건의 발단 : 외부 API 호출

    때는 바야흐로 2020년 3월.

    프론트엔드 공부를 시작한 지 얼마 되지 않은 채 홀로 토이 프로젝트를 진행하던 중이었다.

    코로나 바이러스 관련 웹서비스를 만들고자 했고, React로 클라이언트단을 개발하고 Firebase로 배포까지 진행해보려고 했다.

    확진자 데이터를 지도에 보여주는 것을 구현하기 위해서 코로나 바이러스 확진자 데이터를 제공해주는 외부 API를 사용해야 했다.

    당시 내가 프로젝트를 시작했을 때만 해도 공공 데이터 포털에서 제공해주는 API가 없어서, 

    Dropper Lab이라는 곳에서 제공하는 API를 사용하기로 결정했다.

    (내가 프로젝트를 끝낼 즈음에 공식 API가 출시되었다.)

     

     

    API 문서를 보며 차근차근 코드를 작성한 후, React 클라이언트단에서 외부 API 서버에 요청을 했는데..? 

     

    당시 스크린샷은 없어서 임시로 구현해본 CORS 에러 메세지입니다.

     

    뜨아ㅏㅏㅏ악 😱😱😱😱

    초보 개발자를 두려움에 떨게 만드는 콘솔의 빨간 에러 메시지... 그가 등장하였다...

    (사실 에러 메시지는 나름 친절하게 에러를 설명해주기 때문에 한 번 더 코드를 살펴보거나 구글링만 해도 해결하기 쉽지만,)

    공부 시작한 지 얼마 안 된 초보 개발자는 그저 에러 메시지를 보면 당황스럽지...

     

    http://localhost:3000에서 http://openapi.data.xxx 로 보낸 요청이 CORS 정책에 의해서 차단되었다고????

    Access-Control-Allow-Origin 헤더가 요청된 리소스에 없다고??

     

    그래서 이게 대체 무슨 에러냐고?

    바로 CORS 정책을 위반해서 나온 에러라고!

    그렇다면 CORS 정책이 대체 뭐길래????

     

    ⚠️ CORS 관련 이슈는 왜 발생하는 걸까?

     

    출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

    " 교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

     

    교차 출처 요청의 예시: 

    https://domain-a.com의 프론트엔드 JavaScript 코드가 XMLHttpRequest를 사용하여

    https://domain-b.com/data.json을 요청하는 경우.

     

    보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한합니다. 예를 들어, XMLHttpRequest와 Fetch API는 동일 출처 정책을 따릅니다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다. " (출처: MDN)

     

    위 내용을 요약하자면,

    - CORS는 서로 다른 출처(Origin) 간에 리소스를 전달하는 방식을 제어하는 체제이며,

    - CORS 요청이 가능하려면 서버에서 특정 헤더인 Access-Control-Allow-Origin과 함께 응답할 필요가 있다.

     

    CORS 정책을 위반하여 서로 다른 출처를 가진 상태에서 무언가를 요청하게 되면 브라우저가 보안 상의 이유로 차단을 해버린다!

    예를 들어, 클라이언트 포트가 3000번이고 서버의 포트가 8000번 일 때,

    클라이언트에서 서버로 리소스를 요청했을 때 CORS 에러 메시지가 클라이언트 콘솔에 빨갛게 뜨고 데이터를 주지 않게 된다.

     

    CORS 문제를 해결하려면 동일한 출처에서 리소스를 요청하면 된다.

    간단하쥬~? 🤪

    (부들부들,, 이렇게 끝내면 정말 초보자들한테는,, 돌덩이를 맞을 것이야,,,)

     

     

    ♻️ CORS 해결 방법 1 ) 남이 만든 프록시 서버 사용하기

    출처: https://mobiviki.com

     

    당시 내가 개발하던 환경의 문제점을 정리해보자면 아래와 같다.

    - 첫 번째 : 클라이언트에서 외부 API 서버로 바로 요청을 보내서 CORS 문제가 발생

    - 두 번째 : 외부 API를 사용하고 있었기 때문에 내가 서버를 제어할 수 없으므로
      HTTP 응답 헤더인 Access-Control-Allow-Origin 를 설정할 수 없음

    - 세 번째 : 당시 프론트엔드를 배운지도 얼마 안 되어서 따로 서버를 구축하기에는 시간과 실력이 부족

     

    따라서 클라이언트에서 외부 서버로 바로 요청을 해버리는 것이 아니라 프록시 서버를 사용해서 우회하는 방법을 택했다.

    프록시 서버는 클라이언트가 프록시 서버 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 준다.

    쉽게 말해 브라우저와 서버 간의 통신을 도와주는 중계서버라고 생각하면 된다. 

     

    https://cors-anywhere.herokuapp.com

    나는 프록시 서버로 위 서버를 사용했다.

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

     

    axios({
      method: "GET",
      url: `https://cors-anywhere.herokuapp.com/https://api.dropper.tech/covid19/status/korea?locale=${city}`,
      headers: {
      'APIKey': COVID_APIKEY,
    },

     

    요청해야 하는 URL 앞에 프록시 서버 URL을 붙여서 요청하게 되면,

    클라이언트에서 서버로 리소스를 요청할 때 발생하는 CORS 문제를 아주 간단하게 해결할 수 있다.

     

     

    ♻️ CORS 해결 방법 2 ) 직접 프록시 서버 구축하기

    사용해왔던 외부 API가 서비스를 중단해서 내 서비스도 더 이상 작동하지 않는 것을 발견하게 되었다.

    그래서 다른 API로 교체할 겸, 이제는 서버를 구축해서 CORS 문제를 해결해봐야겠다는 생각이 들었다.

    나의 서버를 구축하게 되면 더이상 클라이언트에서 서버로 바로 요청하는 것이 아니라, 서버에서 서버로 요청할 수 있게 된다.

     

    CORS는 브라우저에 관련된 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다. 

    즉, 서버에서 서버로 리소스를 요청하면 CORS 정책을 위반하지 않고 정상적으로 응답을 받을 수 있다.

     

    기존에는 클라이언트를 Firebase에 배포해서 서비스하고 있었는데,

    Firebase의 Clound Functions를 통해서 서버를 구축하고 storage를 사용해봤는데, 생각보다 비용이 많이 나왔다.

    그래서 최소한의 비용으로 토이 프로젝트는 마무리하고자 Firebase 대신 Heroku를 사용해서 호스팅과 서버를 모두 해결하기로 했다.

     

    우선 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 (request) => {
    	let response
    	try {
    		response = await axios.get(covid_url, {
    			params: {
    			/* 생략 */
    			},
    		})
    	} catch (e) {
    		console.log(e)
    	}
    	return response
    }
    
    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"))
    })
    

     

    Heroku에 서버를 배포하고 호스팅까지 진행했으니, 클라이언트에서 서버로 요청할 때 CORS 문제도 일어나지 않고,

    express로 구축한 서버에서 외부 API 서버(http://openapi.data.go.kr)로 요청하게 되므로 전혀 문제가 되지 않는다.

     

    정말 많은 삽질을 거치고 거쳐 CORS를 다시금 해결하니 너무 감격스러웠다😭😭😭

    (Firebase로 서버 구축하는데 서칭 하면서 삽질하느라 2일 정도 소요됐는데, 비용 때문에 다시 Heroku로 돌아온 게 허무하기도 하고..)

     

     

    ♻️ CORS 해결 방법 3 ) 클라이언트 : http-proxy-middleware 사용하기

    위에 기술한 두 번째 해결 방법에서 누락된 방법 중 하나가 바로 http-proxy-middleware 라이브러리를 사용하는 것이다.

    배포하고 나서는 동일한 출처에 요청을 하므로 CORS 에러가 발생하지 않지만, 배포하기 전 개발 단계가 문제다.

    로컬 환경에서 React는 3000번 포트를 사용하고 Express는 5000번 포트를 사용하기 때문이다.

    그러면 3000번 포트에서 5000번 포트로 요청을 보내니까 당연히 CORS 문제가 발생하게 된다.

     

    이는 로컬 환경일 경우에 한정해서 http-proxy-middleware 라이브러리를 사용하여 클라이언트단에서 쉽게 해결할 수 있다.

     

     

     

     

     

     

     

    CRA로 개발 중일 경우, http-proxy-middleware 라이브러리를 설치하고,

    setupProxy.js 라는 파일을 src 폴더 내에 만들고 아래와 같이 코드를 작성한다.

     

    const { createProxyMiddleware } = require("http-proxy-middleware")
    
    module.exports = function (app) {
    	app.use(
    		"/api",
    		createProxyMiddleware({
    			target: "http://localhost:5000",
    			changeOrigin: true,
    		})
    	)
    }
    

     

    위와 같이 코드를 작성해 두면, 로컬 환경에서 http://localhost:3000/api로 시작되는 요청을

    라이브러리가 http://localhost:5000/api 로 프록싱 해주게 된다.

    따라서 브라우저는 클라이언트와 서버의 출처가 다르지만 같은 것으로 받아들이게 되어 CORS 문제를 일으키지 않는다.

     

     

    ♻️ CORS 해결 방법 4 ) 서버 : Access-Control-Allow-Origin 헤더 세팅하기

    사실 클라이언트에서 해결하는 방법 말고 서버에서 헤더를 세팅해주는 것이 가장 기본적인 CORS 해결 방법이다.

    나는 초기에 서버를 따로 구축하지 않고 외부 서버에 리소스를 요청해서 헤더를 세팅할 수 없었기 때문에 프록시 서버를 썼던 것인데,

    만약 클라이언트와 서버 모두 자신이 제어할 수 있다면 서버에서 Access-Control-Allow-Origin 헤더를 세팅해주는 것이 가장 좋다.

     

    const express = require('express');
    const app = express();
    
    app.get('/api', (req, res) => {
        res.header("Access-Control-Allow-Origin", "허용하고자 하는 도메인");
        res.send(data);
    });
    

     

    위와 같이 서버 쪽에 코드를 작성해 주면 되는데, Access-Control-Allow-Origin : 이런 식으로 작성하는 것만은 피하자.

    * 를 사용하면 모든 출처에서 오는 요청을 허용하는 것이기 때문에 지양하는 것이 좋고, 허용하고자 하는 도메인을 꼭 작성해주자.

     

     

    ♻️ CORS 해결 방법 5 ) 서버 : CORS 미들웨어 사용하기 

    나처럼 서버를 Express로 구축한 경우 Node.js 미들웨어 중 하나인 CORS 를 사용하여 쉽게 문제를 해결할 수 있다.

     

    const express = require('express');
    const cors = require('cors');
    const app = express();
    
    const corsOptions = {
        origin: '허용하고자 하는 도메인', 
    };
    
    app.use(cors(corsOptions));

     

    origin에 허용하고자 하는 도메인을 넣어주면 response 헤더에 Access-Control-Allow-Origin 내용이 추가가 된다.

    app.use(cors()) 이런 식으로 하게 되면 모든 출처에서 오는 요청을 허용하는 것이므로 지양하자.

     

     

    🌈 프론트엔드 개발자라면 겪어야만 했던 외로운 사투를 끝마치며...

    웹 개발자, 특히 프론트엔드 개발자라면 CORS 이슈는 무조건 넘어야만 하는 큰 산이다.

    초보 개발자일 때는 정말 서칭하며 해결하느라 몇 날 며칠을 보냈는데,

    그래도 나름 지금은 공부도 많이 한 상태여서, 해결 방법만 찾는 것이 아니라 CORS 자체를 이해하는데 많은 시간을 들였다.

    그 결과, 오랜만에 이렇게 블로그도 작성해보고, 웹 개발자에게는 기본인 cross origin 에 대해서 파헤쳐봤던 아깝지 않은 시간이었다.

     

    어떤 에러 메세지든 간에, 그 원인만 제대로 파악한다면 그에 맞는 올바른 해결 방법을 찾아 나갈 수 있을 것이다.

    나를 너무나 힘들게 했던 CORS 에러를 결국은 해결했던 것처럼!

     

     

     

     

     

    - 혹시나 포스팅에 잘못된 점이 있다면 주저 말고 피드백해주시면 감사하겠습니다.

    - 포스팅 작성에 참고한 링크는 아래와 같습니다.

    참고 문헌 1

    참고 문헌 2

    참고 문헌 3

    참고 문헌 4

    반응형

    댓글