Spring Boot WebSocket + Vercel 조합에서 CORS 403이 나는 이유는 설정을 두 곳에 해야 하는데 한 곳만 했기 때문. WebSocketConfig랑 SecurityConfig 둘 다 잡아야 함.
문제 상황
POP-SPOT 프로젝트에서 실시간 채팅 기능을 만들었다. 로컬에서는 잘 됐는데 Vercel에 올리자마자 아래 에러가 터짐.
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만 허용
수정 전 ❌
// 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.appsetAllowedOrigins()는 정확히 일치하는 주소만 허용하기 때문에 허용 목록에 없는 Vercel URL은 전부 차단됨.
수정 후 ✅
// WebSocketConfig.java
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp")
.setAllowedOriginPatterns("*") // 패턴 방식 → 모든 주소 허용
.withSockJS();
}
setAllowedOrigins → setAllowedOriginPatterns 로 한 줄만 바꿈. *는 모든 주소 허용이라 Vercel URL이 바뀌어도 상관없이 동작함.
원인 2번 — SecurityConfig에 CORS 설정 자체가 없었음
Spring Security를 쓰면 Security 레벨에서도 CORS를 따로 설정해야 함. 안 하면 Security가 먼저 요청을 막아버림.
수정 전 ❌
// 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();
}
수정 후 ✅
// 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가 "인증 없으면 안 돼"로 막아버리면 연결 자체가 안 됨. 그래서 이 요청만큼은 열어두는 것.
전체 흐름 정리
[Vercel 프론트] ──── WebSocket 연결 시도 ────▶ [Spring Boot 백엔드]
│
┌─────────────▼─────────────┐
│ SecurityConfig 관문 │
│ (1차 - 건물 입구) │
│ OPTIONS 요청 → 통과 ✅ │
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ WebSocketConfig 관문 │
│ (2차 - 소켓 입구) │
│ Origin 확인 → 통과 ✅ │
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ 연결 성공! 🎉 │
└───────────────────────────┘
수정 요약
결론
Spring Security 쓰는 프로젝트에서 WebSocket CORS 에러 나면 무조건 두 파일 열어야 함. WebSocketConfig만 고치면 절반만 고친 것.
Vercel은 배포마다 URL이 바뀌니까 setAllowedOrigins에 고정 URL 박아두면 로컬에서만 되고 실제 배포에선 항상 깨짐.