리액트에서 WebSocket, STOMP를 사용해 훅 만들기

WebSocket는 실시간 양방향 통신 역할을 담당하며, STOMP는 메시지를 전달하는 데 필요한 규칙과 형식을 정의한다.


내 프로젝트에서 필요한 기능

  • 사용자 2명이 독립된 방(코드)을 생성 및 참여
  • 정답 제출 및 점수 현황을 받아오는 웹소켓 통신
  • 여러 방에서 동시에 독립적으로 게임이 진행

Websocket 자체에는 방이나 네임스페이스 같은 개념이 없지만, STOMP를 활용하면 이 기능들을 비교적 쉽게 구현할 수 있다.

STOMP는 Topic 기반의 메시징을 제공하기 때문에, 방 개념을 토픽으로 매핑하면 된다.


STOMP를 활용한 방 기능 구현

STOMP는 방별로 고유한 토픽을 정의할 수 있다.

클라이언트는 특정 방의 토픽을 구독하고, 서버는 해당 토픽으로 메시지를 브로드캐스트하여 클라이언트가 방별 메시지만 수신하도록 구현할 수 있다.

방별 Topic 정의

각 방에 고유한 토픽을 설정한다.

  • 클라이언트는 특정 방에 입장할 때 해당 토픽을 subscribe하고, 메시지를 전송할 때는 해당 토픽으로 메시지를 send한다.

클라이언트의 구독 및 상태 관리

STOMP 클라이언트는 특정 방의 토픽을 구독한다.

stompClient.subscribe("/topic/room/123", (message) => {
  const data = JSON.parse(message.body); // 수신 메세지

  // 상태 업데이트
  setRoomInfo((prev) => ({ ...prev, ...data }));
});

메시지 전송

  • 특정 방의 모든 사용자에게 메시지를 보내려면 해당 토픽으로 메시지를 전송한다.

메시지 전송 예시

stompClient.send(
  "/app/room/123",
  {},
  JSON.stringify({ message: "Hello, Room 123!" })
);

STOMP를 이용해 WebSocket 통신을 구현하는 React 훅 만들기

1. 초기 상태 관리

사용자에게 실시간으로 전송되어야 하는 정보들을 관리한다.

  • 방 상태, 방 정보 등

2. WebSocket 클라이언트 초기화

Client 객체(STOMP 프로토콜을 통해 WebSocket 통신을 관리하는 객체) 생성한다.

client.current = new Client({
  webSocketFactory: () =>
    new Websocket(`${import.meta.env.VITE_WEBSOCKET_URL}`),
  reconnectDelay: 5000, // 재연결 대기 시간
  heartbeatIncoming: 4000, // 서버 -> 클라이언트 하트비트 간격
  heartbeatOutgoing: 4000, // 클라이언트 -> 서버 하트비트 간격
});
  • webSocketFactory를 사용해 HTTP 서버에 WebSocket 업그레이드 요청을 보낸다.
  • reconnectDelay를 설정해 연결 끊김 시 자동으로 재연결을 시도하는 대기 시간을 지정한다.
  • heartbeatIncomingheartbeatOutgoing을 통해 클라이언트와 서버 간 연결 상태를 유지할 수 있도록 하트비트 주기를 설정한다.

이때, Client 객체를 useRef로 초기화해야 재렌더링 시에도 STOMP 클라이언트 상태를 유지하며, 불필요한 렌더링을 방지할 수 있다.

3. 메시지 구독 및 응답 처리(상태 업데이트)

onConnect에서 Topic을 구독한다.

client.current.onConnect = () => { // WebSocket 연결 성공 후 로직

  // 구독
  client.current!.subscribe('/topic/구독할 토픽', (message) => {
    const updatedStatus = JSON.parse(message.body).body;
    // 구독한 메세지에서 온 응답을 1. 초기 상태를 변경하여 관리한다.
  });
};
  • 서버에서 해당 Topic에 대한 정보를 JSON으로 보내면, 이를 수신해 상태를 업데이트한다.

4. 메시지 전송 함수 생성

클라이언트가 WebSocket을 통해 서버로 메시지를 전송할 수 있도록 sendMessage 함수 생성한다.

const sendMessage = (destination: string, payload: PayloadProps) => {
  if (!client.current || !client.current.connected) {
    console.error("WebSocket이 아직 연결되지 않았습니다.");
    return;
  }

  client.current.publish({ destination, body: JSON.stringify(payload) });
};
  • destination은 서버가 처리할 경로
  • payload는 JSON으로 변환된 메시지 내용

5. 에러 처리

client.current.onStompError = (frame) => {
  console.error("STOMP 에러:", frame.headers["message"]);
};

6. 연결 시작

위의 설정들로 웹소켓 연결을 시작한다.

client.current.activate();

7. 연결 해제

컴포넌트 언마운트 시 연결을 해제한다.

client.current.deactivate();

전체 코드

import { useEffect, useRef, useState } from 'react';
import { Client } from '@stomp/stompjs';

const useWebsocket = () => {
  const client = useRef<Client | null>(null);

  // 방 상태 정의하기
  const [roomStatus, setRoomStatus] = useState('WAITING');

  useEffect(() => {
    // Websocket 연결 초기화
    client.current = new Client({
      webSocketFactory: () => new Websocket(`${import.meta.env.VITE_BASE_URL}/game-websocket`),
      debug: (str) => console.log(str),
      reconnectDelay: 5000,
      heartbeatIncoming: 4000,
      heartbeatOutgoing: 4000,
    });

    client.current.onConnect = () => {
      console.log('Websocket 연결 성공');

      // 방 상태 구독
      client.current!.subscribe('/topic/room-status', (message) => {
        const updatedStatus = JSON.parse(message.body).body;
        // 상태 업데이트
      });
    };

    client.current.onStompError = (frame) => {
      console.error('STOMP 에러:', frame.headers['message']);
    };

    client.current.activate();

    return () => {
      if (client.current) {
        client.current.deactivate();
      }
    };
  }, []);

  const sendMessage = (destination: string, payload: unknown) => {
    if (!client.current || !client.current.connected) {
      console.error('WebSocket이 아직 연결되지 않았습니다.');
      return;
    }

    client.current.publish({ destination, body: JSON.stringify(payload) });
  };

  return { sendMessage, 상태값 };
};

export default useWebsocket;