Waiting Room 개발

대규모 트래픽을 위한 WebSocket Waiting Room 개발기를 작성했습니다.

1. 들어가며

실시간 사용자 경험(UX)은 현대 웹 애플리케이션에서 필수적인 요구사항입니다. 특히, 대기룸과 같은 기능은 사용자에게 실시간 상태 업데이트와 안정적인 연결을 제공해야 합니다. 이번 글에서는 대규모 트래픽을 처리할 수 있는 WebSocket 기반 대기룸을 프론트엔드 관점에서 설계하고 구현한 경험을 공유합니다.

주요 내용

  • WebSocket을 활용한 실시간 대기룸 구현
  • 개발 과정에서 직면한 메모리 릭 문제 해결
  • 대규모 트래픽에 대응하기 위한 상태 관리 최적화


2. WebSocket 선택 이유와 대기룸 요구사항

기존 HTTP 폴링의 한계

HTTP 폴링 방식은 클라이언트가 일정 주기로 서버에 요청을 보내 상태를 확인합니다. 그러나 실시간성이 중요한 대기룸에서는 아래와 같은 한계가 존재합니다

  1. 네트워크 과부하: 요청-응답 간 불필요한 데이터 전송 발생
  2. 응답 지연: 실시간 상태를 반영하기 어려움

WebSocket의 장점

WebSocket은 양방향 통신을 가능하게 하며, 서버와 클라이언트 간 실시간 데이터를 주고받는 데 적합합니다. 대기룸에서 WebSocket을 사용하면 다음과 같은 장점이 있습니다

  1. 실시간 상태 업데이트: 대기 순서, 남은 인원 등 상태를 즉시 반영 가능
  2. 데이터 효율성: 초기 연결 이후 추가 요청 없이 지속적인 통신 가능
  3. 비용 효율성: 서버와의 지속적인 연결로 불필요한 리소스 사용 최소화


3. 프론트엔드 설계

대기룸 UI 주요 기능

  1. 실시간 대기 상태 표시
    • 남은 대기 인원과 예상 대기 시간을 실시간으로 사용자에게 제공.
    • 대기 상태가 변경될 때 UI를 즉각 업데이트.
  2. 부드러운 애니메이션 처리
    • 순서가 변경될 때 자연스러운 전환 효과로 사용자 경험 개선.
  3. 안정적인 WebSocket 연결


4. WebSocket 구현

WebSocket 연결과 상태 관리

WebSocket 연결과 관련된 로직은 커스텀 훅으로 분리해 재사용성과 유지보수를 높였습니다.

import { useEffect, useRef, useState } from 'react';
 
export const useWebSocket = (url: string) => {
  const socketRef = useRef<WebSocket | null>(null);
  const [messages, setMessages] = useState<string[]>([]);
  const [isConnected, setIsConnected] = useState(false);
 
  useEffect(() => {
    socketRef.current = new WebSocket(url);
 
    socketRef.current.onopen = () => setIsConnected(true);
    socketRef.current.onmessage = (event) =>
      setMessages((prev) => [...prev, event.data]);
    socketRef.current.onclose = () => setIsConnected(false);
 
    return () => {
      socketRef.current?.close(); // 컴포넌트 언마운트 시 연결 종료
    };
  }, [url]);
 
  return { socket: socketRef.current, messages, isConnected };
};

대기 상태를 UI에 실시간 반영

수신된 데이터를 상태에 반영하고, 이를 UI에 표시합니다.

import { useWebSocket } from './useWebSocket';
 
const QueueStatus = () => {
  const { messages, isConnected } = useWebSocket('ws://localhost:8080');
 
  return (
    <div className='queue-status'>
      {isConnected ? (
        <ul>
          {messages.map((msg, idx) => (
            <li key={idx}>{msg}</li>
          ))}
        </ul>
      ) : (
        <p>서버와 연결되지 않았습니다.</p>
      )}
    </div>
  );
};
 
export default QueueStatus;


5. 메모리 릭 방지와 성능 최적화

WebSocket 구현 과정에서 메모리 릭과 성능 저하를 방지하기 위한 다양한 접근 방법을 적용했습니다.

메모리 릭 위험 요인

  1. 이벤트 리스너 누적: 연결 반복 시 이벤트 리스너가 중복 등록될 가능성.
  2. 연결 누수: 컴포넌트 언마운트 시 WebSocket 연결이 닫히지 않으면 메모리 릭 발생.
  3. 대규모 메시지 관리: 불필요한 데이터를 클라이언트 메모리에 과도하게 저장.

해결 방안

  1. 클린업 함수로 리스너 제거 useEffect의 클린업 함수에서 이벤트 리스너를 제거하고 WebSocket 연결을 종료합니다.

    useEffect(() => {
      const ws = new WebSocket(url);
     
      const handleMessage = (event: MessageEvent) => {
        setMessages((prev) => [...prev, event.data]);
      };
     
      ws.addEventListener('message', handleMessage);
     
      return () => {
        ws.removeEventListener('message', handleMessage);
        ws.close(); // 연결 종료
      };
    }, [url]);
  2. 메시지 크기 제한 수신 메시지 크기를 제한하거나 데이터를 압축하여 메모리 낭비를 방지합니다.

    const MAX_MESSAGE_SIZE = 1024; // 메시지 크기 제한 (1KB)
     
    const handleIncomingMessage = (message: string) => {
      if (message.length > MAX_MESSAGE_SIZE) {
        console.warn('메시지가 너무 큽니다. 처리하지 않습니다.');
        return;
      }
      const parsedData = JSON.parse(message);
      updateState(parsedData);
    };
  3. 상태 업데이트 최적화 불필요한 상태 업데이트를 줄이기 위해 React.memozustand 셀렉터를 활용합니다.

    import { create } from 'zustand';
     
    const useQueueStore = create((set) => ({
      messages: [],
      addMessage: (msg) =>
        set((state) => {
          const newMessages = [...state.messages, msg];
     
          // 최대 메시지 50개만 유지
          if (newMessages.length > 50) {
            newMessages.shift();
          }
     
          return { messages: newMessages };
        }),
    }));
     
    const MessageList = React.memo(() => {
      const messages = useQueueStore((state) => state.messages);
     
      return (
        <ul>
          {messages.map((msg, idx) => (
            <li key={idx}>{msg}</li>
          ))}
        </ul>
      );
    });


6. 결론

WebSocket 대기룸 개발은 실시간성과 안정성이 중요한 기능이었습니다. 특히 메모리 릭 방지 및 상태 관리 최적화를 통해 프론트엔드 성능을 크게 향상시킬 수 있었습니다. 이 경험을 바탕으로 실시간 시스템 개발에 대한 이해를 더 깊게 할 수 있었습니다.