본문 바로가기
💻 트러블슈팅 (에러 해결)

브라우저의 붉은 악마! CORS 에러 원인과 스프링 부트 해결법 완벽 정리

by 데브프리 2026. 6. 24.

안녕하세요, 여러분의 든든한 IT 길잡이 데브프리입니다.

 

웹 개발을 하면서 프론트엔드(React, Vue 등)와 백엔드(Spring Boot)를 분리해서 개발해 본 분이라면, 두 서버를 연결하는 순간 브라우저 콘솔창에 뜨는 시뻘건 에러 메시지를 보고 절망한 경험이 있으실 겁니다.

 

Access to fetch at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

개발자들 사이에서 일명 '붉은 악마'라고 불리는 CORS(코스) 에러입니다. 백엔드 서버는 멀쩡히 돌아가고 있는데 왜 데이터를 주지 않는 걸까요?

 

오늘은 이 지긋지긋한 CORS 에러가 도대체 왜 발생하는 것인지 그 원인을 짚어보고, 스프링 부트(Spring Boot) 환경에서 단 1분 만에 우아하게 해결하는 방법까지 확실하게 정리해 드리겠습니다!

 

1. CORS 에러, 도대체 넌 누구냐? (출입국 심사대 비유)

CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 단어 그대로 "서로 다른 출처(Origin)끼리 데이터를 주고받는 것을 허용할지 말지 결정하는 정책"입니다.

CORS 에러 정책 브라우저 보안 원리
브라우저는 기본적으로 내가 속한 나라(출처)가 아닌 다른 나라(외부 서버)에서 가져온 물건(데이터)을 거부하는 방어 본능이 있습니다.

웹 브라우저(크롬, 사파리 등)는 무척이나 의심이 많고 보수적입니다. 기본적으로 '동일 출처 정책(SOP, Same-Origin Policy)'을 따르기 때문에, 내가 접속한 사이트의 주소와 데이터를 요청하는 서버의 주소가 조금이라도 다르면 해킹이나 악성 스크립트의 공격으로 간주하고 데이터를 냅다 차단해 버립니다.

  • 프론트엔드 주소: http://localhost:3000 (React)
  • 백엔드 주소: http://localhost:8080 (Spring Boot)

위 예시처럼 로컬 환경에서 개발할 때조차 포트 번호(3000 vs 8080)가 다르기 때문에 브라우저는 두 개를 아예 '다른 나라(출처)'로 인식합니다. 그래서 프론트엔드가 백엔드에 데이터를 달라고 요청하면, 브라우저가 중간에 개입해서 "어? 너네 출신 성분이 다르네? 허락받은 증명서(헤더) 있어? 없으면 차단!" 하고 막아버리는 현상이 바로 CORS 에러입니다.

 

2. 스프링 부트에서 CORS 에러 해결하는 2가지 방법

이 에러를 해결하는 원리는 간단합니다. 방어막을 치고 있는 브라우저에게 백엔드 서버가 "어, 저기 3000번 포트에서 오는 애, 내가 허락한 내 친구 맞으니까 막지 말고 통과시켜 줘!"라고 허가증(Access-Control-Allow-Origin 헤더)을 발급해 주면 됩니다.

스프링 부트 CORS 에러 해결 허가증 발급
백엔드 서버에서 "이 주소는 안전하다"라는 허가증(헤더)을 발급해 주면 브라우저의 보안 방어막을 무사히 통과할 수 있습니다.

스프링 부트 환경에서는 크게 2가지 방식으로 이 허가증을 발급할 수 있습니다.

방법 ①: @CrossOrigin 어노테이션 사용 (부분 적용)

가장 간단하고 직관적인 방법입니다. 데이터를 전달해 주는 컨트롤러(Controller) 클래스나 메서드 위에 어노테이션을 딱 한 줄만 붙여주면 됩니다.

Java
 
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
// 💡 아래 주소에서 오는 요청은 모두 허락하겠다는 뜻입니다!
@CrossOrigin(origins = "http://localhost:3000") 
public class BoardController {

    @GetMapping("/api/board")
    public String getBoardList() {
        return "CORS 에러 없는 평화로운 게시판 데이터";
    }
}

특정 컨트롤러에만 간단하게 허락을 해주고 싶을 때 유용하지만, 컨트롤러가 10개, 20개로 늘어나면 일일이 붙여주기 귀찮다는 단점이 있습니다.

방법 ②: WebMvcConfigurer를 통한 전역 설정 (실무 권장)

실무에서는 주로 애플리케이션 전체에 CORS 정책을 한 번에 적용하는 방식을 사용합니다. 설정 클래스(Configuration)를 하나 만들어두면 모든 컨트롤러에 일괄적으로 허가증이 발급됩니다.

Java
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration // 스프링의 설정 파일임을 선언
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 모든 API 주소에 대하여
                .allowedOrigins("http://localhost:3000") // 3000번 포트의 접근을 허용
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드
                .allowedHeaders("*") // 모든 헤더 허용
                .allowCredentials(true) // 쿠키나 인증 정보 포함 허용
                .maxAge(3600); // 1시간 동안 브라우저가 이 허락을 기억하게 함 (캐싱)
    }
}

이 설정 파일 하나만 프로젝트에 추가해 두면, 프론트엔드 개발자가 여러분을 향해 "백엔드 개발자님, CORS 에러 나요!"라고 외치는 일이 영원히 사라질 것입니다.

 

3. 요약: 트러블슈팅 체크리스트

CORS 에러가 발생했다면 당황하지 말고 아래 3가지를 체크하세요.

  1. 에러 주체 확인: 이 에러는 프론트엔드의 잘못도, 백엔드의 잘못도 아닙니다. 단순히 '브라우저'가 보안을 위해 중간에서 막은 것입니다.
  2. 도메인 및 포트 일치 여부: 요청을 보내는 주소와 받는 주소의 프로토콜(http/https), 도메인, 포트 중 하나라도 다르면 무조건 발생합니다.
  3. 백엔드 헤더 설정: 스프링 부트에서 @CrossOrigin을 쓰거나 WebMvcConfigurer 설정 파일에 프론트엔드의 정확한 주소(http://localhost:3000 등 마지막에 슬래시 / 없이)를 허용해 주었는지 확인합니다.

 

마무리

처음 웹 개발을 할 때 브라우저를 가득 채우는 빨간 글씨 때문에 많이 당황하셨죠?

 

하지만 CORS는 우리를 괴롭히려고 만든 에러가 아니라, 악의적인 해킹 사이트가 내 브라우저에 저장된 정보를 훔쳐가지 못하도록 보호해 주는 아주 고맙고 든든한 보초병입니다. 원리만 정확히 이해하면 아주 간단한 코드 몇 줄로 손쉽게 친구(안전한 출처)임을 증명할 수 있습니다.

 

오늘 준비한 트러블슈팅 가이드가 여러분의 붉은 에러 창을 깨끗하게 지워주었기를 바랍니다. 오류를 고친 경험은 여러분의 가장 큰 자산이 됩니다. 막히는 에러가 있다면 언제든 댓글로 남겨주세요.

 

지금까지 데브프리였습니다. 감사합니다!