채팅에 대해서 질문이 있습니다. ㅠㅠ
미해결
... // WebSocket 연결 및 STOMP 클라이언트 설정 useEffect(() => { const token = sessionStorage.getItem("accessToken"); // 세션에서 액세스 토큰을 가져옴 const socket = new SockJS(`http://localhost:8080/ws/chat`); // WebSocket 연결 const client = Stomp.over(socket); stompClientRef.current = client; // STOMP 연결 client.connect( { Authorization: `Bearer ${token}` }, (frame) => { setConnected(true); console.log("STOMP 연결 성공", frame); // 해당 채팅방에 대한 메시지 구독 client.subscribe(`/exchange/chat.exchange/room.${roomId}`, (msg) => { const receivedMessage = JSON.parse(msg.body); if (receivedMessage.type === "PLACE") { const place = JSON.parse(receivedMessage.message); setMessages((prev) => [...prev, { type: "PLACE", place }]); } else { setMessages((prev) => [...prev, receivedMessage]); } }); }, (error) => { console.error("STOMP 연결 실패:", error); alert("STOMP 연결 실패! 서버가 실행 중인지 확인하세요."); } ); // 채팅방 목록 가져오기 fetch(`${BASE_URL}/chat/rooms`, { headers: { Authorization: `Bearer ${token}`, }, }) .then((res) => res.json()) .then((data) => setChatRooms(data)) .catch((error) => console.error("채팅방 목록 가져오기 실패:", error)); return () => { if (client.connected) client.disconnect(); }; }, [roomId]); // 메시지 보내는 기능 const sendMessage = (message, type = "TALK") => { const token = localStorage.getItem("accessToken"); stompClientRef.current?.send( `/pub/chat.message.${roomId}`, { Authorization: `Bearer ${token}` }, JSON.stringify({ sender: currentUser, message, roomId, type, timestamp: new Date().toISOString(), }) ); if (type === "TALK") setNewMessage(""); }; ... 리액트는 다음과 같이 구성하고 @Controller @RequiredArgsConstructor @Log4j2 public class ChatController implements ChatControllerDocs { private static final String CHAT_EXCHANGE_NAME = " chat.exchange "; private final static String CHAT_QUEUE_NAME = "chat.queue"; private final ChatService chatService; private final RabbitTemplate rabbitTemplate; @Override // 클라이언트에서 서버로 보낸 메시지를 메시지를 라우팅 // @MessageMapping("chat.message")로 설정하여 클라이언트로부터 /pub/chat.message 목적지로 전송된 STOMP 메시지를 처리한다. /*RabbitMQ*/ @MessageMapping("chat.message.{roomId}") /*STOMP*/ // @MessageMapping("/{roomId}") // 구독한 클라이언트에게 response를 제공할 url 정의 // @SendTo("/topic/{roomId}") public ResponseEntity<?> sendMessage( // @Payload: 메시지의 body를 정의한 객체에 매핑합니다. @Payload ChatMessageDTO message, // @DestinationVariable: 구독 및 메시징의 동적 url 변수를 설정. RestAPI의 @PathValue와 같다. @DestinationVariable int roomId) { try { ChatMessageDTO msg = chatService.sendMessage(message); log.info ("Sent message: {}", msg); if (msg != null) { // RabbitMQ으로 메시지 전송 // template.convertAndSend() 메소드를 사용하여 메시지를 RabbitMQ로 전송한다. // 메시지는 chat.exchange 로 전송되며, 라우팅 키는 room. + 메시지의 방 ID로 구성된다. rabbitTemplate.convertAndSend(CHAT_EXCHANGE_NAME, "room." + roomId, message); } else { log.error("Failed to create chat message. User might not be in the chat room. User: {}, Room: {}", message.getSender(), message.getRoomId()); } return ResponseEntity.ok().body(msg); } catch (Exception e) { log.error("Error processing message: ", e); throw new ChatException(e.getMessage()); } } @Configuration @EnableRabbit @RequiredArgsConstructor public class RabbitConfig { // Queue (큐): RabbitMQ에서 메시지를 저장하는 장소 private static final String CHAT_QUEUE_NAME = "chat.queue"; // Exchange (교환기): 메시지를 Queue로 라우팅(보내는) 역할 private static final String CHAT_EXCHANGE_NAME = "chat.exchange"; // Routing Key (라우팅 키): Exchange가 메시지를 어떤 Queue로 보낼지를 결정하는 데 사용 private static final String ROUTING_KEY = "room.*"; @Value("${spring.rabbitmq.host}") private String host; @Value("${spring.rabbitmq.port}") private int port; @Value("${spring.rabbitmq.username}") private String userName; @Value("${spring.rabbitmq.password}") private String password; // Queue 등록 @Bean public Queue queue() { return new Queue(CHAT_QUEUE_NAME, true); } // Exchange 등록 @Bean public TopicExchange exchange() { return new TopicExchange(CHAT_EXCHANGE_NAME); } // Exchange와 Queue바인딩 @Bean public Binding binding(Queue queue, TopicExchange exchange) { return BindingBuilder .bind(queue) .to(exchange) .with(ROUTING_KEY); } // RabbitMQ와의 메시지 통신을 담당하는 클래스 @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); rabbitTemplate.setMessageConverter(jsonMessageConverter()); rabbitTemplate.setRoutingKey(ROUTING_KEY); return rabbitTemplate; } // RabbitMQ와의 연결을 관리하는 클래스 @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory factory = new CachingConnectionFactory(); factory.setHost(host); factory.setPort(port); factory.setVirtualHost("/"); factory.setUsername(userName); factory.setPassword(password); return factory; } // Queue를 구독(Subscribe)하는 걸 어떻게 처리하느냐에 따라 필요함. 당장은 없어도 됨. @Bean public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory, MessageConverter messageConverter) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(messageConverter); return factory; } @Bean public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { return new RabbitMessagingTemplate(rabbitTemplate); } // 메시지를 JSON형식으로 직렬화하고 역직렬화하는데 사용되는 변환기 // RabbitMQ 메시지를 JSON형식으로 보내고 받을 수 있음 @Bean public Jackson2JsonMessageConverter jsonMessageConverter() { //LocalDateTime serializable을 위해 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true); objectMapper.registerModule(dateTimeModule()); Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(objectMapper); return converter; } @Bean public Module dateTimeModule() { return new JavaTimeModule(); } } ... @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry .setErrorHandler(stompExceptionHandler) // 소켓 연결 URI다. 소켓을 연결할 때 다음과 같은 통신이 이루어짐 .addEndpoint("/ws/chat") .setAllowedOriginPatterns("http://localhost:5173") // SocketJS를 통해 연결 지원 .withSockJS(); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { log.info("--------------"); log.info("동작함"); registration.interceptors(stompHandler); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // url을 chat/room/3 -> chat.room.3으로 참조하기 위한 설정 registry.setPathMatcher(new AntPathMatcher(".")); registry.setUserDestinationPrefix("/sub"); // 클라이언트 구독 경로 // RabbitMQ 브로커 리레이 설정 registry.enableStompBrokerRelay("/exchange", "/queue", "/topic") .setRelayHost(host) .setRelayPort(61613) .setClientLogin(userName) .setSystemPasscode(password) .setSystemLogin(userName) .setSystemPasscode(password); } ... } 이렇게 구성했는데 rabbitMQ는 도커 컴포즈로 구성했습니다. 근데 다음과 같은 문제가 발생했는데 Chat.jsx:31 GET http://localhost:8080/ws/chat/285/4mxcvol4/jsonp?c=_jp.apvvzrr net::ERR_ABORTED 404 (Not Found) chat:1 Refused to execute script from ' http://localhost:8080/ws/chat/285/4mxcvol4/jsonp?c=_jp.apvvzrr ' because its MIME type ('') is not executable, and strict MIME type checking is enabled. 이렇게 계속해서 연결이 끊깁니다 어떻게 해야하나요?
- rabbitmq
- react
- stomp
- websocket
- springboot
- restapi
- jwt
- socketjs





