Trouble Shooting

[Facebook chat] WebSocket으로 실시간 채팅 구현하기

웹 애플리케이션에서 실시간 통신의 중요성은 더욱 커지고 있다. 특히, 채팅 애플리케이션, 금융 정보 대시보드, 온라인 게임 등은 사용자에게 실시간으로 데이터를 전송해야 하기 때문에 웹소켓 기술이 필수적이다. 이번 글에서는 ToggleDoc에서 웹소켓과 React-Query를 활용해 실시간 채팅 기능을 구현했던 경험을 기록해두었다.

웹소켓 기본 개념

웹소켓(WebSocket)은 클라이언트와 서버 간에 실시간 양방향 데이터 통신을 가능하게 하는 기술이다. HTTP 프로토콜과 달리, 웹소켓은 연결을 한 번 맺은 후 지속적인 데이터 교환이 가능해서, 실시간 동기화 기능이 요구되는 애플리케이션에 적합하다. 웹소켓 연결은 ws (비암호화) 또는 wss (암호화) 프로토콜을 사용하여 초기화된다.

React에서의 웹소켓 연동

React 환경에서 웹소켓을 연동하는 방법은 다음과 같은 단계로 요약할 수 있다.
  1. 웹소켓 인스턴스 생성: React 컴포넌트의 useEffect 내에서 WebSocket 객체를 생성하여 서버와의 웹소켓 연결을 초기화
  1. 이벤트 핸들러 설정: 웹소켓 연결에 대한 이벤트 리스너(onopen, onmessage, onerror, onclose)를 설정하여, 연결 상태 변경, 메시지 수신 등의 이벤트를 핸들링
  1. 데이터 전송 및 수신: send 메소드를 사용하여 서버에 데이터를 전송하고, onmessage 이벤트 핸들러를 통해 서버로부터 데이터를 수신
  1. 연결 종료 처리: 컴포넌트 언마운트 시 또는 필요에 따라 close 메소드를 호출하여 웹소켓 연결을 안전하게 종료

실시간 데이터 처리를 위한 접근 방식

  • 재연결 로직 구현: 네트워크 불안정성 등의 이유로 웹소켓 연결이 끊어질 수 있으므로, 자동 재연결 로직을 구현하는 것이 중요하다.
  • 상태 관리: React 상태(State) 또는 상태 관리 라이브러리를 사용하여 실시간으로 수신되는 데이터를 관리한다.
  • 효율적인 데이터 업데이트: 불필요한 렌더링을 방지하기 위해, 수신된 데이터를 효율적으로 처리하고 업데이트하는 방식을 고려해야 한다.

ToggleDoc 클라이언트에서의 웹소켓 연결 관리

ToggleDoc에서는 새로운 메시지가 도착했을 때 조직별로 구분하여 데이터를 싱크하는 기능을 구현해야 했다. 조직별 웹소켓 연결을 관리하기 위해 웹소켓 연결 시점에 조직의 ID를 포함한 메시지를 서버에 보내는 로직을 사용했다. 또한 웹소켓 연결이 끊어지는 것을 방지하기 위해 30초마다 서버에 핑을 보내는 로직을 추가했다. 아래 작성된 Custom Hook이 그 예시이다.
import { useQueryClient } from 'react-query'; import React, { useEffect } from 'react'; export default function useWebSocketConnect() { const queryClient = useQueryClient(); const organization = JSON.parse(localStorage.getItem('organization')); useEffect(() => { // 웹소켓 연결 const host = window.location.host; const socket = new WebSocket('wss://api.toggledoc.com/ws'); socket.onopen = (event) => { // 서버에 초기 연결 메시지 전송 const initMessage = { type: 'init', organizationId: organization.id }; socket.send(JSON.stringify(initMessage)); heartBeatInterval = setInterval(() => { socket.send(JSON.stringify({ type: 'ping' })); }, 30000); }; // 에러 처리 socket.onerror = (error) => { console.log(`WebSocket Error: ${error}`); }; // 새로운 메시지 수신 socket.onmessage = (event) => { const data = JSON.parse(event.data); console.log('need message sync >> ', data); // 채팅 데이터 동기화 queryClient.refetchQueries('FB-Message'); queryClient.refetchQueries('FB-Chats'); }; // 연결 종료 처리 socket.onclose = (event) => { console.log('WebSocket died'); }; return () => { socket.close(); }; }, []); }

ToggleDoc 서버에서 클라이언트 구분

서버 측에서는 클라이언트의 초기 연결 메시지를 통해 각 클라이언트에 조직 ID를 할당한다. 이 로직을 사용해서 서버는 조직별로 메시지를 분류하여 적절한 클라이언트에게만 메시지 업데이트를 전송할 수 있다.
import { Server } from 'ws'; let wss = null; // 웹소켓 초기화 및 각 클라이언트에 조직 ID를 할당 export const initializeWebSocket = (server) => { wss = new Server({ server, path: '/ws' }); wss.on('connection', (ws) => { ws.on('message', async (message) => { try { const messageData = JSON.parse(message.toString()); if (messageData.type === 'init' && messageData.organizationId) { // 클라이언트 조직 ID 할당 ws.organizationId = messageData.organizationId; console.log(`클라이언트에 조직 ID ${ws.organizationId}가 할당되었습니다.`); } } catch (error) { console.error('메시지 처리 중 오류가 발생했습니다:', error); } }); }); }; // 조직 ID로 클라이언트를 구분하여 싱크 메시지 전송 export const broadcastMessage = async (organizationId, senderId) => { try { wss.clients.forEach((client) => { if (client.organizationId === organizationId) { const payload = { type: 'MESSAGE_UPDATE', chatRoomId: senderId, messageText: 'New Message Arrived', }; client.send(JSON.stringify(payload)); } }); } catch (error) { console.error('error:', error); } };
 
React에서 웹소켓을 활용한 실시간 채팅 기능 구현은 복잡해 보일 수 있으나, 이러한 단계별 접근 방식을 통해 체계적으로 접근한다면 효과적으로 구현할 수 있다. 이번 포스트에서 소개한 방법을 실시간 데이터 동기화가 필요한 다양한 애플리케이션에 적용해 볼 수 있을 것 같다.