본문 바로가기
IT이야기/PROJECT

[React + SpringBoot] 1대1 랜덤채팅 프로젝트 - Spring&웹소켓&STOMP 설정 및 구현

by JI_NOH 2024. 3. 9.
반응형

목적

- 로스트 아크 게임 유저들을 위한 깐부 찾기 랜덤채팅 사이트

구성

- 프론트 : React, Shadcn, Javascript, Socket, STOMP
- 백 : SpringBoot, Java, MySQL, Redis, WebSocket, STOMP

 

 

 

 

 

웹소켓 + STOMP 설정


웹소켓과 STOMP에 대해서는 이전 포스팅에서 설명하고 넘어갔으니 스프링에서 어떻게 설정하는지에 대해서 자세히 알아보자. 첫 포스팅에도 간단히 첨부는 하고 넘어갔던 데이터들이다.

 

build.gradle

	// websocket - 실시간 통신
	implementation 'org.springframework.boot:spring-boot-starter-websocket'
	implementation 'org.webjars:sockjs-client:1.1.2'
	// stomp - 구독
	implementation 'org.webjars:stomp-websocket:2.3.3-1'
 

 

WebSocketConfig

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 메세지 구독 경로
        config.enableSimpleBroker("/sub");
        // 메시지 발행 경로 설정
        config.setApplicationDestinationPrefixes("/pub");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //Client에서 websocket 연결할 때 사용할 API 경로를 설정 - 채팅용
        registry.addEndpoint("/chat")
                .setAllowedOriginPatterns("*")
                .withSockJS();
        //Client에서 websocket 연결할 때 사용할 API 경로를 설정 - 매칭용
        registry.addEndpoint("/match")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
}
 

 

 

1.configureMessageBroker

웹소켓 설정을 찾아봤다면 해당 함수 내에서 enableSimpleBroker, setApplicationDestinationPrefixes

를 설정해주는 것을 많이 보았을 것이다.

 

우선 Spring에서 안내하는 기본 설정은 아래와 같다.

package com.example.messagingstompwebsocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gs-guide-websocket");
  }

}
 

어차피 configureMessageBroker 안에 선언하는 경로는 커스텀 설정이기 때문에 똑같이 할 필요는 없으며 여기서 사용되는 경로는 차후에 '클라이언트 -> 서버 / 서버-> 클라이언트'가 서로 메세지를 보낼 때 구독인지 발행인지 판단하는데 사용되는 경로라고 생각하면 되겠다.

 

프론트에서 아래와 같은 형태로 발행(/pub)과 구독(/sub)을 판단하며 이하 세부 주소는 MessageController에서 매핑하여 연결될 수 있게끔 작업한다.

// 발행
stomp.send('/pub/chat/exit', headers, data);
 
// 구독
stomp.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {
          const newMessage = JSON.parse(message.body);
          // 구독해서 받아온 메세지 구현부

        });
 

2. registerStompEndpoints

다음으로 registerStompEndpoints는 소켓 통신을 할 때 사용되는 endpoint를 설정하는 곳이다.

소켓 연결을 할 때, 어느 주소로 소켓 연결을 시작할 것 인지를 선언하는 곳인데 api 주소와 유사하다고 생각하면 된다.

 

이는 프론트에서 아래와 같은 형태로 sockJS연결할 때 사용된다.

const socket = new SockJS("http://localhost:8080/chat");
 

 

 

 

 

 

MessageController


1. 백엔드 STOMP구현

private final SimpMessageSendingOperations template;


@MessageMapping("/chat/enter")
public void enterUser(ChatMessages chat, SimpMessageHeaderAccessor headerAccessor) {
    chat.setMessage("채팅방에 참여하였습니다.");
    template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);

    // 입장 관련 Service 구현부

}

@MessageMapping("/chat/message")
public void sendMsg(ChatMessages message) {
    template.convertAndSend("/sub/chat/room/"+ message.getRoomKey(), message);

    // 메세지 관련 Service 구현부

}

@MessageMapping("/chat/exit")
public void exitUser(ChatMessages chat) {
    chat.setMessage("채팅방에서 퇴장하였습니다.");
    template.convertAndSend("/sub/chat/room/" + chat.getRoomKey() , chat);

    // 퇴장 관련 Service 구현부

}
 

앞서 간단하게 언급했는데

프론트에서 연결 후 , /pub/chat/~ 로 발행하게 된다면 내가 만든 컨트롤러에서 @MessageMapping("/chat/~") 어노테이션 경로로 해당 요청을 받을 수 있게 된다.

 

그리고 발행 내용을 받았으니 서버에서는 해당 내용을 구독자들에게 뿌리는데 그 부분이 아래 코드다.

template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);
 

 

 

2. 백엔드와 프론트엔드 간 STOMP

그럼 쉬운 이해를 위해 프론트 <-> 백 사이의 통신이 어떻게 이루어 지는지 보자.

const [stompClient, setStompClient] = useState(null);

// socket오픈
useEffect(() => {
  const socket = new SockJS("http://localhost:8080/chat");
  const stomp = Stomp.over(socket);

  setStompClient(stomp);
}, []);

// 채팅방 입장
useEffect(() => {
    if (stompClient) {
      let headers = {Authorization: getCookie('accessToken')};
      // stomp연결 및 /pub/chat/enter경로로 메세지DTO발행
      stompClient.connect(headers, () => {
        if(props.enter == true && stompClient){
          stompClient.send('/pub/chat/enter', {}, 
          JSON.stringify({ 
            type: 'ENTER',
            roomKey: roomInfo.roomKey,
            sender: uuId,
            message: "",
            sendTime: moment().format('YYYY-MM-DDTHH:mm:sszz')}
          ));
        }
    }
  }, [stompClient, roomInfo]); 
 
    @MessageMapping("/chat/enter")
    public void enterUser(ChatMessages chat) {

        chat.setMessage("채팅방에 참여하였습니다.");
        template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);

        chatRepository.saveMessages(chat, ChatMessages.MessageType.ENTER);
        chatRoomRepository.updateLastChat(chat.getRoomKey(), chat.getSendTime(), chat.getMessage());
    }
 
// 구독
useEffect(() => {
    if (stompClient) {
        stompClient.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {
          const newMessage = JSON.parse(message.body);
          setChatHistory((prevHistory) => [...prevHistory, newMessage]);
        });
    }
  }, [stompClient]); 
 

프론트에서는 sockJS를 연 순간부터 해당 엔드포인트로 양방향 통신이 이루어 지고 있다.

    const socket = new SockJS("http://localhost:8080/chat");

 

발행을 프론트에서 행한다면

    stompClient.send('/pub/chat/enter', {},

 

백에서는 MessageMapping을 통해 발행 내용을 받고

    @MessageMapping("/chat/enter")

 

메세지 가공이 필요하다면 가공을 한 후 해당 발행내용 타겟 구독자들에게 다시 발행해준다.

여기서 chat.getRoomKey()의 경우 해당 방 구독자들만 받아볼 수 있는 키 값이라고 보면 되겠다.

    chat.setMessage("채팅방에 참여하였습니다.");

    template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);

 

프론트에서는 앞서 sockJS연결한 정보에 관해 stompClient에 갖고 있고, 해당 객체에 작업이 들어온다면 그걸 받는 useEffect처리를 해둔 상황이다.

    useEffect(() => { ), [stompClient]);

 

그리고 내 구독함에 메세지가 들어온다면 해당 내용을 화면에다 뿌려준다.

    stompClient.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {

        const newMessage = JSON.parse(message.body);

        setChatHistory((prevHistory) => [...prevHistory, newMessage]);

    });

 

 

 

다 적용하고 직접 겪어보면 쉬운데 처음 찾아 볼 때는 다들 쓰고 있는 엔드포인트도 다르고

프론트에서 어떻게 쓰이고 -> 백에서 어떻게 받고 -> 그 반대는 다시 어떻게 하는지에 대한 설명이 없어서 상당히 헤맸었다.

 

이 정도만 알면 소켓 통신에 대해서는 어떻게 쓰는지 쉽게 이해할 수 있으리라 생각된다.

다음은 Redis 사용에 관해 포스팅해보겠다. 마찬가지로 처음쓰면서 삽질을 많이 했던 파트다.

 

반응형