강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지
유요한

작성한 질문수

채팅에 대해서 질문이 있습니다. ㅠㅠ

작성

·

50

0

...

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

답변 1

0

안녕하세요, 인프런 AI 인턴입니다.

유요한님, WebSocket 연결 문제를 해결하기 위해 아래 몇 가지 사항을 확인해보세요.

  1. WebSocket 경로 확인:

    • StompEndpointRegistry에서 설정한 경로 (/ws/chat)가 클라이언트에서 연결하려는 경로와 동일한지 확인하세요.
    • 클라이언트 측의 SockJS 경로와 서버의 경로가 /ws/chat으로 일치하는지 확인하세요.
  2. CORS 설정 확인:

    • WebSocket의 CORS가 허용되어 있는지 확인하세요. setAllowedOriginPatterns 메서드에 클라이언트 도메인이 포함되어 있는지 확인합니다.
  3. Spring Security 설정:

    • Spring Security를 사용하고 있다면, WebSocket 엔드포인트에 대한 접근 제어 설정이 되어 있는지 확인하세요.
  4. SockJS와 WebSocket 중복 설정 확인:

    • 서버에서 SockJS와 WebSocket 설정이 맞게 되어 있는지 확인하세요.
  5. RabbitMQ 설정:

    • RabbitMQ의 호스트, 포트, 사용자 인증 정보가 정확한지 확인하고 enableStompBrokerRelay 설정이 맞는지 점검합니다.
  6. Network 관련 사항:

    • Docker Compose로 RabbitMQ를 구성한 경우, 네트워크 설정이 올바르게 되어 있는지 확인하세요.
    • RabbitMQ가 제대로 실행되고 있고, 클라이언트에서 접근할 수 있는지를 확인합니다.

만약 위의 방법으로 해결이 안 된다면, 서버 로그를 자세히 확인하여 추가적인 오류 메시지가 있는지 살펴보시고, 문제가 지속될 경우 더 많은 정보를 제공해 주시면 도움이 될 수 있습니다.

더 자세한 내용은 Spring Boot Websockets Documentation도 참고해 보시기 바랍니다.

유요한님의 프로필 이미지
유요한

작성한 질문수

질문하기