본문 바로가기

개발 공부

CORS가 뭐지

이번엔 웹 개발자라면 한번쯤은 겪어봤을 CORS(Cross-Origin Resource Sharing) 정책에 대해 알아보려고 합니다. 사실 웹 개발을 하다보면 CORS 정책 위반으로 인해 에러가 발생하는 상황은 누구나 한 번 정도는 겪게 된다고 해도 과언이 아닙니다.

 

필자는 이번 국비 지원 교육에서 팀 단위 프로젝트를 진행하면서 CORS 에러를 겪게 되었는데, 로컬 서버에서 아마존으로 서버를 이전하면서 문제가 생겼다. 로그인부터 API 통신 쪽이 아예 안되는 와중에 스프링이나 비쥬얼 스튜디오에서는 에러가 발생하지 않아서 웹에서 에러 메세지를 확인해보니 이러한 메세지를 확인할 수 있었습니다.

Response to preflight request doesn't pass access control check: No 
'Access-Control-Allow-Origin' header is present on the requested resource

이는 웹 브라우저가 보안을 위해 서버에 사전 요청(preflight request)을 보낼 때 나타나는 메시지입니다. 이 오류는 서버가 해당 요청을 보낸 출처(origin)로부터 오는 HTTP 요청을 허용하도록 설정되지 않았음을 의미합니다. 처음 이 문제를 혼자 해결하려고 시도했을 때, 이 문제가 프론트엔드에서 해결해야 할 것인지 백엔드에서 해결해야 할 것인지조차 알 수 없었습니다. 처음에는 요청된 리소스에 'Access-Control-Allow-Origin' 헤더가 없어서 발생한 오류라고 생각했기 때문에, 이 헤더를 매번 보내는 것이 가능할지 고민했습니다. 하지만 이렇게 하려면 전체 전송 방식을 변경해야 하므로, 다른 방법을 찾기로 했습니다. 이 문제를 해결하기 위해 동료, 인터넷, 멘토에게 조언을 구했습니다. 모든 헤더에 대한 요청을 허용하도록 CORS 필터 설정을 하면 작동할 것이라고 확신했지만, 같은 오류가 계속 발생했고, 이 문제로 약 2일 동안 고민했습니다. 나중에 확인해보니 다른 팀원이 CICD를 자동으로 활성화했는데, 과정 중에 오류가 발생하여 내가 한 변경사항이 서버에 적용되지 않았다는 것을 알게 되었습니다.

 @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:3000"); // 허용할 도메인 설정
        config.addAllowedOrigin(reactServerUrl); // 외부 프론트 주소 허용 설정
        config.addAllowedOrigin(reactServerUrlNoPort);
        config.addAllowedHeader("*"); // 모든 헤더 허용
        config.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
        config.addExposedHeader("Refresh-Token"); // 노출할 헤더 추가
        config.addExposedHeader("Access-Token");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

이는 당시 내가 설정한 corsFilter 설정입니다. 허용할 도메인과 외부 프론트 주소등을 설정하고, 모든 헤더와 HTTP 메서드에 대해 통신을 허용하고 있습니다.

 

CORS란?

CORS는 웹 브라우저에서 구현되는 보안 정책으로, 다른 출처(origin)의 리소스에 접근하는 요청에 대해 서버가 명시적으로 허용한 출처만 접근할 수 있게 하는 규칙입니다. 쉽게 말하면 CORS(Cross-Origin Resource Sharing)는 이름 그대로 출처(Origin)가 다른 자원들을 공유한다는 뜻입니다. 이는 다른 출처의 자원에 접근해서 리소스를 사용한다는 의미입니다.

출처 (Origin)

https://google.com:80/search?page=1

 

이 url은 우리가 매일 마주하는 구글의 url입니다. 위의 구성요소 중에서 Protocol(https://) + Host(google.com) + Port(80) 이 세가지가 같으면 동일한 출처(Origin)이라고 합니다.

 

왜 다른처는 막아두나요?

우리는 웹에서 <img> <script> <frame> <video> 태그들을 사용하면서 새로운 자원들을 외부에서 가져올 수 있습니다.
만약 은행 홈페이지에 들어가던 중 악성 코드가 심어진 <script> 파일이 포함된 evil.com 페이지를 열게 된다면 어떤 일이 일어날까요?

 

안타깝게도 script 파일에 Delete/account 요청이 담겨있어서 페이지를 열자마자 유저의 계좌가 삭제되어버렸습니다.
이러한 예상치 못한 사고들을 막으며 보안을 강화하기 위해 다른 출처의 접근을 막는 정책이 등장하게 되었습니다.

 

동일 출처 정책 (Same-Origin Policy)


따라서 동일한 출처에서만 리소스를 공유할 수 있다는 정책을 사용하게 되었습니다.
여기서의 동일한 출처란 클라이언트와 서버가 같은 출처에 있다면 동일 출처이며 다른 서버에 있다면 다른 출처라고 취급합니다.



예를 들어, 위 사진에서 domain-a.com의 유저가 domain-b.com 서버에 요청하게 되면 호스트가 다르기 때문에 다른 출처에 요청한 상태인 것입니다.

도메인 외에도 같은 프로젝트 내에서 정의된 css 파일에 대한 요청은 동일 출처 요청이며
폰트의 경우 google과 같은 외부 사이트에서 리소스를 가져온다면 다른 출처 요청이라고 할 수 있습니다.


하지만 이렇게 엄격하게 차단해버리면 원하는 페이지를 만들 수 있게 될까요? 🤔
오픈된 인터넷 환경에서 다른 출처의 리소스를 가져와 사용하는 일은 흔한 일입니다.

따라서 몇 가지 예외 조항을 두고 다른 출처의 리소스를 허용하는 정책이 등장했습니다.
그것은 바로 CORS 정책을 지킨 리소스 요청입니다!

 

다른 출처 정책

사실 우리를 힘들게 했던 CORS의 시빨건 에러 메세지는 다른 출처의 리소스를 얻기 위한 해결 방안이었습니다.
동일 출처 정책(SOP)을 위반해도 CORS 정책을 지킨다면 다른 출처의 리소스도 불러올 수 있게 되는 것입니다!

그렇다면 어떻게 CORS 에러를 해결할 수 있을까요?

먼저 브라우저에서의 CORS 동작 과정에 대해서 살펴보겠습니다.


1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아서 전달


웹은 서버에 요청을 보낼 때 HTTP 프로토콜을 이용합니다.
이 때 "여기서 왔어요"를 나타내는 Origin이라는 값을 요청 헤더에 담아서 보냅니다.

2. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달


서버는 요청을 받고나서 클라이언트에게 이 "이 url은 리소스에 접근할 수 있어요"을 Access-Control-Allow-Origin 필더에 담아서 응답합니다. 이를 알았을 때, 내가 생각했던 헤더를 직접 추가하는 방식이 아니라 아까 설정에서 봤던 허용할 도메인 주소를 입력하면 Access-Control-Allow-Origin 헤더에 들어가서 브라우저에서 허용하는 방식이라고 생각이 정리되었습니다.

3. 클라이언트에서 응답을 비교해서 차단 여부를 결정


브라우저는 자신이 보냈던 요청의 Origin과 서버가 보낸 Access-Control-Allow-Origin 값을 비교해서 응답 사용 여부를 결정합니다. 만약 일치하지 않는다면 응답을 사용하지 않고 버리며 이러한 상황이 CORS 에러에 해당됩니다.

 

CORS 에러 해결법

결국 CORS 에러를 해결하기 위해서는 서버의 허용이 필요합니다. 만약 CORS 에러가 발생한다면 서버측에서 허용할 출처를 헤더의 Access-Control-Allow-Origin에 기재해서 응답하면 해결되는 것입니다.

그렇다면 클라이언트에서도 Origin 값을 변경하면 되지 않을까요?
좋은 시도였지만 Origin 값이 변경되면 브라우저에서 이를 감지하여 차단하기 때문에 가능하지 않습니다 🥲

 

🤔 정말 프론트에서 해결할 방법이 없나요?

저희도 백엔드 설정이 끝날 때까지 기다릴수만은 없죠..ㅎㅎ
프론트단에서도 아래의 방법을 통해 로컬 환경에서의 CORS 문제를 해결할 수 있습니다!

1. 크롬 확장 프로그램 이용
크롬에서는 CORS 문제를 해결하기 위한 확장 프로그램을 제공합니다!
해당 프로그램을 활성화하면 로컬 환경에서 CORS 문제를 해결할 수 있습니다.


2. 프록시(Proxy) 이용
프록시는 클라이언트와 서버 사이의 중간 대리점 역할을 합니다.
서버에서 따로 설정을 안 해서 CORS 에러가 발생하는 것이라면, 모든 출처를 허용한 대리점을 통해 요청을 하면 되는 것이죠!
Vite의 경우 아래와 같이 proxy 설정을 해줄 수 있습니다.

// vite.config.json
export default defineConfig({
  plugins: [react(), svgr()],
  server: {
    proxy: {
      // 경로가 "/api" 로 시작하는 요청을 대상으로 proxy 설정
      '/api': {
        // 요청 서버 주소 설정
        target: 'http://www.google.com',
        // 요청 헤더 host 필드 값을 서버의 호스트 이름으로 변경
        changeOrigin: true,
        // 요청 경로에서 '/api' 제거
        rewrite: (path) => path.replace(/^\/api/, ''),
        // SSL 인증서 검증 무시
        secure: false,
        // WebSocket 프로토콜 사용
        ws: true,
      },
    },
  },
});

 

인증 데이터 요청 시의 CORS

위에서 살펴본 동작 흐름은 가장 기본적인 흐름을 설명한 것이고
사실 CORS는 세 가지의 시나리오에 따라서 동작 방식이 달라집니다.

아래의 지식들은 CORS를 당장 해결하는데 필요한 필수 지식은 아니지만, 쿠키나 토큰과 같은 인증 데이터를 요청해야 한다면 반드시 알아야 하는 개념입니다.

예비 요청 (Preflight Request)

브라우저는 먼저 예비 요청을 보내어 통신이 잘 되는지 확인한 후 본 요청을 보냅니다.



이러한 예비 요청을 보내는 것을 Preflight라고 부르며 HTTP 메서드가 OPTIONS라는 특징이 있습니다.


단순 요청 (Simple Request)
단순 요청은 예비 요청을 생략하고 서버에 본 요청을 보낸 후 서버로부터 받은 Access-Control-Allow-Origin를 비교해서 CORS 정책 위반 여부를 검사하는 방식입니다.



사실 대부분의 HTTP 요청은 예비 요청으로만 이루어집니다.
왜냐하면 API 요청은 대부분 text/xml이나 application/json으로 통신하기 때문에 단순 요청에서 요구하는 Content-Type과 맞지 않기 때문입니다.

인증된 요청 (Credentialed Request)
쿠키와 토큰과 관련된 CORS를 해결하고자 하신다면 이 개념에 주목해주시길 바라겠습니다!

인증된 요청은 서버에게 자격 인증 정보를(Credential) 실어서 요청할 때 사용됩니다.


자격 인증 정보란 세션 ID가 저장된 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값들을 말합니다.

 

클라이언트에서 인증 정보 보내기

기본적으로 fetch와 같이 브라우저가 제공하는 요청 API들은 인증과 관련된 데이터를 요청 데이터에 담지 않도록 설정되어 있습니다.

따라서 credentials 옵션을 설정해주어야 인증 정보를 보낼 수 있습니다!

사용법은 메서드마다 살짝씩 다르기 때문에 예시 코드를 첨부하겠습니다.

fetch('https://example.com/users/login', {
  method: 'POST',
  credentials: 'include',
  body: JSON.stringify({
    userId: 1,
  }),
});
axios.post(
  'https://example.com/users/login',
  {
    userId: 1,
  },
  {
    withCredentials: true,
  },
);
$.ajax({
  ...
  xhrFields: {
    withCredentials: true,
  },
});

서버에서 헤더 설정하기

서버에서도 일반적인 CORS 요청과는 다르게 대응하는 것이 필요합니다.

Access-Control-Allow-Credentials 를 true로 설정
Access-Control-Allow-Origin 에 와일드카드("*") 사용 불가
Access-Control-Allow-Methods 에 와일드카드("*") 사용 불가
Access-Control-Allow-Headers 에 와일드카드("*") 사용 불가
인증 정보는 민감한 정보이기 때문에 출처를 정확히 기재해주어야 CORS 에러가 발생하지 않습니다.

 

글을 마치며

당장 문제를 해결하려고 cors에 대해 찾아보긴 했지만 이렇게 정리하면서 보니 cors가 어떻게 동작하는지 알 수 있었고, 첫인상은 안좋았지만 너무 미워할 대상이 아니라는 것도 알게되었습니다. CORS는 웹 개발 시에 반드시 마주하게 되는 개념이기 때문에 빨간 에러에 좌절하기 보다는 그 원인과 해결 방법에 대해 알아갈 기회라고 생각하시는 것을 추천드립니다.

'개발 공부' 카테고리의 다른 글

TPS(Transaction Per Second)  (0) 2025.03.28
WebSocket과 STOMP  (0) 2025.03.26
WebSocket 통신 방식이란?  (0) 2025.03.26
코딩테스트와 알고리즘 연습  (0) 2024.11.28
객체지향에 대한 이해  (4) 2024.10.15