프로젝트 기록

[Waffle-market] React + stompJS를 이용한 실시간 채팅구현

lerrybe 2023. 1. 31. 02:06

들어가며

진행하고 있는 토이 프로젝트에서 웹소켓을 이용해 실시간 채팅 기능을 구현해 보았습니다.

🥕 당근 클론..

사실 클라이언트에서 polling을 통해 계속해서 서버에게 요청을 보내고 실시간(인 척하는) 정보를 받아올 수도 있겠으나, 서버에서 바뀐 정보가 없다면 불필요한 요청을 계속해서 보내게 되는 것이어서 HTTP 프로토콜이 아닌 웹소켓 프로토콜을 이용해 구현해보기로 결정, 관련 내용을 찾아보게 되었습니다.

 

참고로 이 글에서는 React + stompJS 을 사용한 클라이언트 단 내용만 다루는 포스팅이고, 같은 프로젝트의 웹소켓 + STOMP 을 사용한 스프링 예제는 아래 블로그에서 확인하실 수 있습니다!

 

 

[Spring & Dev] 웹소켓에 대하여.

0. 들어가며🏃🏻‍♂️ 진행 중인 팀 프로젝트에서 실시간 채팅 기능을 구현하기 위해 웹소켓을 사용해보았습니다. 예전 수업 과제로 실시간 코인 거래소를 만든 경험이 있는데 이 때는 Polling

kjhoon0330.tistory.com

 


HTTP Protocol

 

HTTP 프로토콜은 여러 특징이 있습니다. 그 중 connectionless, stateless 라는 것이 있는데,

서버는 클라이언트의 상태를 저장해두지 않고 그저 클라이언트의 request와 그에 맞춰 응답하는 서버의 response로 원하는 데이터를 주고받습니다. 또한 한 번 연결을 맺은 후 (서버가 클라이언트가 요청한 response를 보낸 후)에는 그 connection이 지속되지 않습니다.

https://github.com/VanHakobyan/HTTP-Protocol-Manipulation

이러한 HTTP 프로토콜을 사용할 때는 클라이언트의 요청이 있을 때만 서버가 응답을 내려주기 때문에 서버 쪽의 데이터에 변경이 있어도 클라이언트가 새로 요청하지 않는 한 화면의 데이터가 업데이트되지 않는다는 단점이 있습니다. 채팅과 같이 변경이 잦고 실시간으로 변경된 혹은 추가된 데이터를 받아올 때는 꽤 불편하기도 합니다.

 

위에서 언급했듯 Polling, Long polling과 같은 방법을 통해서 주기적으로 요청을 보낼 수도 있지만 아래와 같은 단점이 있습니다.

 

1. 서버 데이터가 변경되지 않았을 때 불필요한 요청을 보낼 수도 있다.

2. 자신이 정한 주기가 길 때는 업데이트가 바로바로 되는 것도 아니다. (자신이 정한 N초의 주기만큼 기다려야한다.)

 


 

웹소켓이란 ?

Transport 프로토콜 종류 중 하나로, 클라이언트/서버 사이의 양방향 통신을 통한 데이터 전송이 가능한 방식입니다.

HTTP 프로토콜과는 달리 stateful 하며, 처음 HTTP 프로토콜을 통해 연결한 후에는 connection을 유지할 수 있습니다. 

https://www.wallarm.com/what/a-simple-explanation-of-what-a-websocket-is
https://velog.io/@cksal5911/WebSoket-stompJSReact-%EC%B1%84%ED%8C%85-1

웹소켓의 동작 방식은 크게 다음과 같습니다.

 

1. 클라이언트는 먼저 서버에게 HTTP 프로토콜을 통한 request를 보낸다. 

    1-1. 이 때 클라이언트는 magic key (랜덤하게 생성된 키 값)을 생성해 서버에게 보낸다. 

2. 서버는 받은 key값을 바탕으로 클라이언트에게 응답을 보낸다. 

3. handshaking 과정 끝!

4. 이제부터 양방향으로 데이터를 전송합니다. 

5. 한 쪽에서 connet를 종료하는 신호를 보내면 웹소켓 연결이 끊어지게 됩니다.

 


stompJS ?

팀의 백엔드(스프링)에서 stomp라는 서브 프로토콜을 사용하기로 결정했고, 이에 따라 stomp 프로토콜 환경에서 동작할 수 있는 stompJS라는 라이브러리를 설치해 사용하였습니다.

 

이 라이브러리에서는 프로토콜 연결, 메시지 전송, 해당 채널 구독 기능을 제공하고 있습니다. 

(STOMP 서브 프로토콜에 대한 설명은 글 상단 백엔드 구현 링크에 있습니다.)

 


채팅 구현하기

구현 코드 일부와 함께 사용 예시를 살펴볼 텐데요, typescript + redux-toolkit 환경에서 작성하였습니다. 

$ yarn add @stomp/stompjs
$ yarn add -D @types/stompjs

 

서버와 연결할 클라이언트 객체 생성 및 활성화

기본적으로 stompJS에서는 클라이언트 객체를 생성할 수 있게 제공해주고, 몇 가지 옵션만 넣어주면 됩니다. 

(라이브러리 없이 구현할 때는 스스로 magic key를 생성해 서버에게 전송해야 합니다.)

 

연결 되었을 때 실행할 변수, 에러처리 담당하는 함수도 작성한 후 클라이언트를 활성화해 줍니다. 

import { Client } from '@stomp/stompjs'; // stompjs에서 제공하는 client 객체

const connect = () => {
  // client 객체 생성
  client.current = new Client({
    brokerURL: `ws://${서버주소}/ws-stomp`,
    debug: function (str) {
      console.log(str);
    },
    reconnectDelay: 5000, // 자동 재연결하는 딜레이
    heartbeatIncoming: 4000,
    heartbeatOutgoing: 4000,

    // 연결되었을 때 실행할 function
    onConnect: () => {
      subscribe();
    },
    // 에러처리를 담당하는 function
    onStompError: frame => {
      console.error(frame);
    },
  });

  client.current.activate(); // 클라이언트 활성화
  };

  // disconnect 함수
  const disconnect = () => {
    client.current.deactivate(); 
  };
};

 

그리고 connect 함수를 실행하고, 채팅방 unmount 시에는 연결을 끊습니다.

  useEffect(() => {
    connect();
    return () => disconnect();
  }, []);

 

메시지 보내기 (publish)

클라이언트 객체가 생성되고 서버와 연결되면, 이제 메시지를 보낼 수 있습니다.

detination에는 메시지가 도착하는 목적지 주소를, body에는 보낼 내용을 담으면 됩니다.

body는 백엔드와 협의해 정하면 되고, 방 번호 + message 내용 + 보낸 이의 id + 채팅 생성 날짜를 담아 보내줬습니다.

  const [message, setMessage] = useState('');
  const { me } = useAppSelector(state => state.users);
  
  // DESC: 메시지 보내기 (publish 과정)
  const publish = (message: string) => {
    if (!client.current.connected) return; // 연결 상태일 때만 publish
    if (!message.trim()) return; // 메시지 내용이 없으면 publish X
    if (message.length > 255) return; // 글자수 제한

    client.current.publish({
      destination: '/pub/message',
      body: JSON.stringify({
        roomUUID,
        message,
        senderId: me?.id, // 현재 나의 정보 (auth를 체크한 후 store에 저장된 정보 가져옴)
        createdAt: new Date(),
      }),
    });
    
    setMessage('');
  };

 

 

메시지 받기 (subscribe)

통로로 오가는 새로운 메시지를 받습니다. 

subscribe를 하면 body에 새로온 메시지에 대한 정보가 담겨오는데, 이를 새로운 채팅 메시지 배열에 담아 보여줬습니다.

  const [chatMessages, setChatMessages] = useState<ChatMessageType[]>([]);
  
  // DESC: 메시지 받는 길을 열어두었다. (subscribe 과정)
  const subscribe = () => {
    // 원하는 대상을 구독
    client.current.subscribe(`/sub/room/${roomUUID}`, ({ body }) => {
      const bodyObj: SubBodyType = JSON.parse(body);
      setChatMessages([
        ...chatMessages,
        {
          message: bodyObj.message,
          senderId: bodyObj.senderId,
          createdAt: bodyObj.createdAt,
          chatId: bodyObj.chatId,
        },
      ]);
    });
  };

 

 


참고자료

 

Using StompJs v5+

You can find samples at https://github.com/stomp-js/samples/.

stomp-js.github.io

https://velog.io/@cksal5911/WebSoket-stompJSReact-%EC%B1%84%ED%8C%85-1