인프런 커뮤니티 질문&답변
리액트 & 스프링 부트 SSE 알람 기능 구현 CORS에러
해결된 질문
작성
·
254
0
헌재 상황은 NGINX로 HTTPS로 백엔드를 배포하고 있고 리액트는 로컬에서 작업 중인데 알람 기능을 구현할 때 OPTIONS가 CORS에러를 일으킵니다.
```
@Service
@RequiredArgsConstructor
@Log4j2
@Transactional
public class NotificationService {
// SSE 이벤트 타임아웃 시간
private static final Long DEFAULT_TIMEOUT = 24L * 60 * 60 * 1000; // SSE 연결 타임아웃 (1일)
private final CustomNotificationRepository customNotificationRepository;
private final MemberRepository memberRepository;
private final CommunityRepository communityRepository;
private final NotificationRepository notificationRepository;
// 메시지 알림
public SseEmitter subscribe(String memberEmail, String lastEventId) throws Exception {
// 회원 조회
MemberEntity findMember = memberRepository.findByMemberEmail(memberEmail);
// 매 연결마다 고유 이벤트 ID 부여
String eventId = makeTimeIncludeId(findMember);
log.info("eventId {} ", eventId);
// SseEmitter 생성후 Map에 저장
SseEmitter sseEmitter = customNotificationRepository.save(eventId, new SseEmitter(DEFAULT_TIMEOUT));
// 사용자에게 모든 데이터가 전송되었다면 emitter 삭제
sseEmitter.onCompletion(() -> {
log.info("onCompletion callback");
customNotificationRepository.deleteById(eventId);
});
// Emitter의 유효 시간이 만료되면 emitter 삭제
// 유효 시간이 만료되었다는 것은 클라이언트와 서버가 연결된 시간동안 아무런 이벤트가 발생하지 않은 것을 의미한다.
sseEmitter.onTimeout(() -> {
log.info("onTimeout callback");
customNotificationRepository.deleteById(eventId);
});
// 503 에러를 방지하기 위한 더미 이벤트 전송
sendToClient(eventId, sseEmitter, "알림 서버 연결 성공 [memberId = " + findMember.getMemberId() + "]");
// 클라이언트가 미수신한 Event 목록이 존재할 경우 전송하여 Event 유실을 예방
if (hasLostData(lastEventId)) {
sendLostData(lastEventId, findMember.getMemberId(), sseEmitter);
}
return sseEmitter;
}
private static @NotNull String makeTimeIncludeId(MemberEntity findMember) {
String eventId = findMember.getMemberId() + "_" + System.currentTimeMillis();
return eventId;
}
private void sendToClient(String eventId, SseEmitter sseEmitter, Object data) {
try {
sseEmitter.send(SseEmitter.event()
.name("connect")
.id(eventId)
.data(data));
} catch (IOException e) {
customNotificationRepository.deleteById(eventId);
throw new RuntimeException("알림 서버 연결 오류");
}
}
private boolean hasLostData(String lastEventId) {
return !lastEventId.isEmpty();
}
private void sendLostData(String lastEventId,
Long memberId,
SseEmitter sseEmitter) {
Map<String, Object> eventCaches = customNotificationRepository.findAllEventCacheStartWithByMemberId(memberId);
eventCaches.entrySet().stream()
.filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
.forEach(entry -> sendToClient(entry.getKey(), sseEmitter, entry.getValue()));
}
...이렇게 서비스를 구현하고
@RestController
@RequestMapping("/api/v1/notify")
@RequiredArgsConstructor
@Log4j2
public class NotificationController implements NotificationControllerDocs {
private final NotificationService notificationService;
// 메시지 알림
// SSE 통신을 위해서는 produces로 반환할 데이터 타입을 "text/event-stream"으로 해주어야 함
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
public SseEmitter subscribe(@AuthenticationPrincipal UserDetails userDetails,
@RequestHeader(value = "last-event-id", required = false, defaultValue = "") final String lastEventId,
HttpServletResponse response) {
try {
if (userDetails == null) {
throw new IllegalArgumentException("인증 정보가 필요합니다.");
}
response.setHeader("Connection", "keep-alive");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
String email = userDetails.getUsername();
SseEmitter responseEmitter = notificationService.subscribe(email, lastEventId);
log.info("Subscribed to email: " + email);
log.info("response: " + responseEmitter);
return responseEmitter;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
...NGINX 설정
server {
listen 443 ssl;
server_name meettify.store;
ssl_certificate /etc/letsencrypt/live/meettify.store/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meettify.store/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# SSE용 헤더 설정
location /api/v1/notify/subscribe {
# OPTIONS preflight 요청을 처리하도록 설정
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Max-Age' 3600 always;
return 204;
}
proxy_pass https://meettify.store; # Spring Boot 서버로 요청 전달
proxy_http_version 1.1;
proxy_set_header Connection ''; # SSE 연결 유지
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
# SSE 연결 타임아웃 방지
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 86400s;
keepalive_timeout 86400s; # 필요에 따라 시간 조정
add_header Access-Control-Allow-Origin "http://localhost:5173";
add_header Access-Control-Allow-Credentials "true";
}
# 기본 프록시 설정 (백엔드 애플리케이션)
location / {
proxy_pass http://ubuntu-api-1:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
}
이렇게 엔진엑스 설정까지 한다음
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.exposedHeaders("Cache-Control", "Content-Type", "X-Accel-Buffering")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
}
}
이렇게 설정했는데 계속 CORS에러가 발생합니다.

해당 문제가 지속적으로 반복중입니다. ㅠㅠ
답변 1
0
안녕하세요, 인프런 AI 인턴입니다.
유요한님께서 겪고 계신 CORS 문제는 보통 서버와 클라이언트 간의 도메인이 다를 때 발생합니다. 현 상황에서는 NGINX 설정과 Spring Boot의 CORS 설정에서 문제가 발생할 수 있습니다. 아래 몇 가지 확인 사항과 해결 방법을 제안드립니다.
NGINX 설정 확인:
- NGINX에서
location블록 내의if ($request_method = 'OPTIONS') { ... }가 제대로 작동하고 있는지 확인하세요. 예를 들어,add_header명령어가 제대로 동작하는지,return등 다른 설정이 선행되지는 않는지 확인해 보세요.
proxy_pass https://meettify.store;에서 프로토콜이http://인지https://인지 재확인하시고 맞게 설정되어 있는지 확인하세요.
- NGINX에서
Spring Boot CORS 설정:
WebMvcConfigurer에서 설정한allowedOrigins는http://localhost:5173인데, NGINX에서allow-origin헤더도http://localhost:5173로 설정되어 있는지 확인해보세요.
- 만약 계속해서 문제가 발생한다면,
allowedOrigins를 모두 허용하는"*"로 설정해 일시적으로 문제가 해결되는지 확인해 보세요.
Spring Boot 로그 확인:
- Spring Boot에서 SSE 연결 시 발생하는 예외 메시지를 확인하여, 자세한 에러 원인을 찾는 것이 좋습니다. 서버 쪽 로그에서 특정 오류 내용이 없는지 확인하세요.
- Spring Boot에서 SSE 연결 시 발생하는 예외 메시지를 확인하여, 자세한 에러 원인을 찾는 것이 좋습니다. 서버 쪽 로그에서 특정 오류 내용이 없는지 확인하세요.
확장 옵션:
- 만약 위의 방법으로도 해결되지 않는다면, 다양한 브라우저 개발도구의
Network탭에서 OPTIONS 요청과 응답의 헤더 정보를 자세히 확인하여 문제를 파악할 수 있습니다.
- 만약 위의 방법으로도 해결되지 않는다면, 다양한 브라우저 개발도구의
추가로, 외부 URL로 요청을 보내지 않는 일부 경우에는 CORS 설정을 생략할 수도 있음을 유념하세요. 하지만 개발 환경에서는 항상 보안 설정을 적절히 유지하는 것이 중요합니다.
이와 관련된 다른 자료들을 참조하고 문제가 해결되는지 확인해주세요. 더 궁금한 점이 있으면 언제든지 문의하세요.




