백엔드트러블슈팅

[Spring Boot] WebSocket STOMP + Vercel 배포 시 CORS 403 에러해결 방법

김동현
··3분 읽기

Spring Boot WebSocket(STOMP) 서버를 Vercel 프론트엔드와 연동할 때 발생하는 CORS 403 에러의 원인과 해결법을 실제 코드와 함께 정리합니다.

Spring Boot WebSocket + Vercel 조합에서 CORS 403이 나는 이유는 설정을 두 곳에 해야 하는데 한 곳만 했기 때문. WebSocketConfig랑 SecurityConfig 둘 다 잡아야 함.

문제 상황

POP-SPOT 프로젝트에서 실시간 채팅 기능을 만들었다. 로컬에서는 잘 됐는데 Vercel에 올리자마자 아래 에러가 터짐.

javascript
Access to XMLHttpRequest at 'https://api.popspot.co.kr/ws-stomp/info'
from origin 'https://popspot.co.kr' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present.

일반 HTTP API는 잘 되는데 WebSocket만 안 되는 황당한 상황.


CORS가 뭔지 모르는 분들을 위한 설명

popspot.co.kr (프론트)이 api.popspot.co.kr (백엔드)한테 데이터를 요청할 때, 백엔드가 먼저 확인함.

"이 주소에서 오는 요청, 내가 허용한 곳 맞아?"

허용된 주소면 데이터 줌. 아니면 브라우저가 막아버림. 이게 CORS.

이 허용 목록을 코드에서 설정해줘야 하는데, 반만 설정한 게 문제였음.


왜 WebSocket은 따로 설정해야 하나?

HTTP 요청이랑 WebSocket은 CORS 설정 위치가 다름. Spring Boot에 Security를 쓰면 설정해야 하는 곳이 두 군데.

1. WebSocketConfig — 소켓 연결 입구

2. SecurityConfig — 서버 전체 보안 관문

1번만 설정하고 2번을 빠뜨린 게 원인. 2번이 먼저 막아버리니까 1번까지 도달도 못 함.


원인 1번 — WebSocketConfig에서 고정 URL만 허용

수정 전 ❌

java
// WebSocketConfig.java
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws-stomp")
            .setAllowedOrigins(
                "http://localhost:3000",
                "https://popspot.co.kr"   // 고정 URL만 적어둠
            )
            .withSockJS();
}

Vercel은 배포할 때마다 URL이 바뀜.

  • 어제 배포: https://popspot-abc123.vercel.app
  • 오늘 배포: https://popspot-xyz789.vercel.app
  • setAllowedOrigins()는 정확히 일치하는 주소만 허용하기 때문에 허용 목록에 없는 Vercel URL은 전부 차단됨.

    수정 후 ✅

    java
    // WebSocketConfig.java
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-stomp")
                .setAllowedOriginPatterns("*")  // 패턴 방식 → 모든 주소 허용
                .withSockJS();
    }

    setAllowedOriginssetAllowedOriginPatterns 로 한 줄만 바꿈. *는 모든 주소 허용이라 Vercel URL이 바뀌어도 상관없이 동작함.


    원인 2번 — SecurityConfig에 CORS 설정 자체가 없었음

    Spring Security를 쓰면 Security 레벨에서도 CORS를 따로 설정해야 함. 안 하면 Security가 먼저 요청을 막아버림.

    수정 전 ❌

    java
    // SecurityConfig.java - CORS 설정 없음
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            // CORS 설정이 아예 없음 → Security가 그냥 다 막음
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }

    수정 후 ✅

    java
    // SecurityConfig.java
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 필터 연결
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() // OPTIONS 요청 인증 면제
                .requestMatchers("/api/**", "/ws-stomp/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOriginPatterns(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
        config.setAllowedHeaders(List.of("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 이 줄이 핵심.

    WebSocket은 연결 전에 OPTIONS 메서드로 사전 요청을 먼저 보냄. 이걸 Preflight 요청이라고 함. 로그인 안 한 사람이 이 요청을 보내야 하는데, Security가 "인증 없으면 안 돼"로 막아버리면 연결 자체가 안 됨. 그래서 이 요청만큼은 열어두는 것.


    전체 흐름 정리

    javascript
    [Vercel 프론트] ──── WebSocket 연결 시도 ────▶ [Spring Boot 백엔드]
                                                            │
                                              ┌─────────────▼─────────────┐
                                              │   SecurityConfig 관문      │
                                              │  (1차 - 건물 입구)          │
                                              │  OPTIONS 요청 → 통과 ✅    │
                                              └─────────────┬─────────────┘
                                                            │
                                              ┌─────────────▼─────────────┐
                                              │  WebSocketConfig 관문      │
                                              │  (2차 - 소켓 입구)         │
                                              │  Origin 확인 → 통과 ✅    │
                                              └─────────────┬─────────────┘
                                                            │
                                              ┌─────────────▼─────────────┐
                                              │        연결 성공! 🎉        │
                                              └───────────────────────────┘

    수정 요약


    결론

    Spring Security 쓰는 프로젝트에서 WebSocket CORS 에러 나면 무조건 두 파일 열어야 함. WebSocketConfig만 고치면 절반만 고친 것.

    Vercel은 배포마다 URL이 바뀌니까 setAllowedOrigins에 고정 URL 박아두면 로컬에서만 되고 실제 배포에선 항상 깨짐.