1. 문제 발생
백엔드 REST API를 개발한 후, 프론트엔드(React)와 통신하려고 배포를 완료한 뒤 인증이 필요한 API 요청에서 CORS 에러가 발생했다.
2. CORS 에러는 왜 발생할까?
- CORS(Cross-Origin Resource Sharing)는 교차 출처 리소스 공유 정책이다. 쉽게 말해, 요청을 보내는 출처(origin)가 다를 때 발생하는 문제이다.
- Origin은 세 가지로 구성된다:
- Protocol: http 또는 https
- Domain: localhost 또는 example.com
- Port: 8080

이 세 요소 중 하나라도 다르면 출처가 다른 것으로 간주된다. 동일 출처 정책(Same-Origin Policy, SOP)은 이런 경우 리소스 접근을 차단한다. CORS 정책을 통해서는 교차 출처 리소스 사용을 허용할 수 있다.
동일 출처 정책(Same - Origin Policy)
동일 출처 정책은 SOP라고도 하는데 말 그대로 동일한 출처에 대한 정책을 말한다.
동일한 출처에서만 리소스를 공유할 수 있다
즉, 동일 출처(Same-Origin) 서버에 있는 리소스는 자유로이 가져올 수 있지만, 다른 출처(Cross-Origin)서버에 있는 이미지나 영상 같은 리소스는 접근 불가능하다.
교차 출처 리소스 공유 (Cross-Origin Resourse Sharing)
교차 출처 리소스 공유는 말 그대로 다른 출처의 리소스 공유에 대한 허용 정책이다.
개발을 하던 도중 우리를 괴롭히던 빨간 에러 메시지는 SOP 정책에 따라 다른 출처의 리소스를 차단하면서 발생된 에러였고, CORS는 이러한 다른 출처의 리소스를 얻기위한 해결방안이였다는 것이다.....
SOP 정책을 위반해도 CORS 정책에 따른다면 다른 출처의 리소스라도 허용한다는 뜻이다!!!
3. 예비 요청(Preflight Request)

브라우저는 중요한 요청을 하기 전에 해당 요청이 안전한지 확인하기 위해 예비 요청(Preflight Request)을 보낸다. 이 요청은 OPTIONS 메서드를 사용하여 서버가 해당 요청을 허용할지 미리 확인하는 과정이다.
하지만, 인증이 필요한 API 요청을 보낼 때 발생한 401 Unauthorized 에러는 이 프리플라이트 요청이 제대로 처리되지 않아서 발생했다.
이때 중요한 점은 HTTP메소드를 GET, POST가 아닌 OPTIONS라는 요청이 사용된다는 것이다.
4. 여러가지 시도
CORS 설정은 아래와 같이 진행했:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
이렇게 localhost:3000 프론트에서 로컬로 접근 할 때 모든 권한을 풀어줬다!
Preflight 예비 요청을 할 때 보내는 OPTIONS 메소드를 추가로 넣어줬다.
하지만 인증(토큰)이 필요하지 않은 요청은 모두 정상적으로 작동했다. 그래서 백엔드에서 처리할 수 있는 것을 다 했다고 생각을 하고 프론트에서 헤더에 토큰을 제대로 넣지 못해서 에러가 작동한다고 생각했다!!!
실제로 로그를 찍어본 결과 헤더에 jwt토큰이 잘 담기지 않고 있었다. 이렇게 순조롭게 해결이 되는 것 같았지만....
같은 에러가 계속 발생했다.

GPT에도 물어보고 블로그도 찾아보면서 다양한 시도들을 해보았다.
allowedOrigins("http://localhost:3000")
allowedOriginPatterns("*")
모든 도메인으로부터의 요청을 허용해보았지만 역시 해결을 하지 못했다.
5. 문제 해결
다양한 시도를 해본 끝에, 문제의 원인은 Preflight 요청이 제대로 처리되지 않은 것이었다. 그래서 먼저 Preflight 요청을 허용하는 것이 본 요청을 처리하는 데 필수적이라고 생각했다.
마지막으로 Spring Security 설정에 다음 코드를 추가했다:
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll() // 프리플라이트 요청를 허용
이 코드를 추가해보니 해결이 되었다... 3일간에 노력이 결실을 맺었다....
@Bean
protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.httpBasic().disable() // REST API는 UI를 사용하지 않으므로 기본설정을 비활성화
.csrf().disable() // REST API는 csrf 보안이 필요 없으므로 비활성화
.sessionManagement()
.sessionCreationPolicy(
SessionCreationPolicy.STATELESS) // JWT Token 인증방식으로 세션은 필요 없으므로 비활성화
.and()
.authorizeRequests() // 리퀘스트에 대한 사용권한 체크
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll() // 프리플라이트 요청를 허용
.antMatchers("/sign-api/sign-in", "/sign-api/sign-up",("/kakao/callback"),
"/sign-api/exception", "/api/files/**").permitAll() // 가입 및 로그인 주소는 허용
.antMatchers(HttpMethod.GET, "/api/v1/**").permitAll()
.antMatchers("**exception**").permitAll()
.anyRequest().hasRole("ADMIN") // 나머지 요청은 인증된 ADMIN만 접근 가능
.and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class); // JWT Token 필터를 id/password 인증 필터 이전에 추가
return httpSecurity.build();
}
6. 느낀점
이번 기회에 CORS ERROR에 대해서 자세히 알게 되었고, Spring Security, Jwt 인증 방식에 대해서 더 자세히 공부를 해봐야 겠다고 생각했다...
코드 한줄을 고쳐서 문제를 해결해서 허무하다고 생각 할 수도 있지만! 큰 산하나를 넘은 것 같아서 뿌듯했다. 다음에 다른 개발자 친구들이 CORS 에러가 떳다고하면 제일 먼저 가서 도와줄 수 있을 것이다 :)
참고
🌐 악명 높은 CORS 개념 & 해결법 - 정리 끝판왕 👏
악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이
inpa.tistory.com
'백엔드 > 스프링' 카테고리의 다른 글
| [테스트] nGrinder - 성능 테스트 환경 구성하기 (1) | 2024.11.24 |
|---|---|
| [백엔드] QueryDSL (1) | 2024.11.19 |
| [백엔드] Java Stream API에서 map과 collect 사용하기 (1) | 2024.10.14 |
| [백엔드] Spring Boot에서 GlobalExceptionHandler를 활용한 전역 예외 처리 마스터하기 (1) | 2024.10.06 |
| [백엔드] @Scheduled(Cron)를 프로젝트에 응용하기 (1) | 2024.09.30 |