묻고 답해요
156만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨
리액트 & 스프링 부트 SSE 알람 기능 구현 CORS에러
헌재 상황은 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에러가 발생합니다.해당 문제가 지속적으로 반복중입니다. ㅠㅠ
-
미해결Kevin의 알기 쉬운 RxJava 2부
SSE 실습 소스코드 위치 문의
안녕하세요. 좋은 강의 감사드립니다. 마지막 SSE 실습 소스코드 위치 문의드립니다. 알려주신 Github에서는 보이지 않습니다. https://github.com/ITVillage-Kevin/rxjava-episode2 * 실습 파일명 예시: RxJavaPracticeApplication.java 감사합니다.