...
// 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.이렇게 계속해서 연결이 끊깁니다 어떻게 해야하나요?