[React] SSE 기반 실시간 큐 상태 모니터링 시스템 구축기

Ryomi·2025년 4월 17일
0
post-thumbnail

이번 글에서는 제가 직접 겪은 경험을 중심으로, 실시간 기능 도입 과정에서 느낀 점과 기술적 고민들, 그리고 실제 코드 예시를 공유드리고자 합니다.

👻 실시간 기능을 도입하게 된 배경

프로젝트의 특성상, 여러 사용자가 동시에 같은 페이지를 열어두고 함께 작업하는 상황이 자주 발생합니다.
이때 각 사용자의 행동이 다른 사용자에게 실시간으로 반영되지 않으면, 불필요한 새로고침이나 혼란이 생기게 됩니다.

기존 구조는 데이터 변경 후 수동 새로고침이 필요한 방식이었기 때문에,
이런 실시간 협업 흐름에는 적합하지 않았습니다.

그래서 이번 기회를 통해 실시간 기능을 처음으로 도입하고,
사용자가 직접 새로고침하지 않아도 서버 상태가 자동으로 반영되는 구조로 개선하게 되었습니다.

😵‍💫 기술 선택 배경

💡 SSE란?

SSE (Server-Sent Events) 는
서버가 클라이언트(브라우저)에게 실시간으로 데이터를 푸시(Push) 할 수 있는 기술입니다.
HTTP 프로토콜 위에서 동작하며, 기본적으로 단방향 통신을 지원합니다.

클라이언트가 EventSource를 통해 서버에 연결을 열면,
서버는 필요한 시점마다 event를 전송하여 실시간으로 정보를 전달합니다.

🔁 SSE vs WebSocket 비교

구분SSE (Server-Sent Events)WebSocket
통신 방식단방향 (서버 ➝ 클라이언트)양방향 (서버 ⇄ 클라이언트)
프로토콜HTTP 기반 (text/event-stream)WebSocket 전용 프로토콜 (ws/wss)
연결 관리자동 재연결, 간단한 keep-alive수동 핸들링 필요
사용 난이도상대적으로 단순복잡한 연결 및 핸드셰이크 필요
전송 포맷일반 텍스트 스트림바이너리 / 텍스트 모두 가능

🧭 왜 SSE를 선택했는가?

우리 서비스의 실시간 큐 페이지에서는 서버의 상태를 클라이언트가 실시간으로 받아보는 기능이 핵심이었습니다. 이를 위해 다음과 같은 요구사항을 충족해야 했습니다:

SSE를 선택한 이유는 아래와 같습니다:

✅ 단방향 푸시에 최적화되어 있어 구현이 간단하고 유지보수가 쉬움

✅ HTTP 기반이라 방화벽이나 프록시에서 안정적

✅ 브라우저의 EventSource 객체만으로 빠르게 연결 가능, 재연결도 자동 처리됨

✅ Socket 대비 리소스 사용이 적고, 가볍게 실시간 UI 구성 가능

WebSocket을 선택하지 않은 이유

  • WebSocket과 같은 양방향 통신은 오버헤드가 컸을 것이며, 폴링(Polling)은 실시간성이 떨어져 적합하지 않았습니다. SSE는 이러한 trade-off를 효과적으로 해결한 최적의 선택이었습니다.

🦾 핵심 코드

📡 SSE 연결 및 해제 (startSSE, stopSSE)

let eventSource: EventSource | null = null;

export const startSSE = (queueId: string, onMessage: (data) => void) => {
  eventSource = new EventSource('/api/queue/stream');
  eventSource.onmessage = (event) => {
    const parsed = JSON.parse(event.data);
    onMessage(parsed);
  };
};

export const stopSSE = () => {
  eventSource?.close();
  eventSource = null;
};

실시간 업데이트를 위해 SSE 연결을 설정하고, 페이지를 벗어나거나 편집 모드일 때는 연결을 해제하도록 하였습니다.

💬 개발 중 고려한 점

🧠 SSE 상태에 따른 UI 동기화

사용자가 현재 보고 있는 페이지 상태와 서버의 상태가 일치하도록 하는 것이 가장 핵심이었습니다.
SSE 이벤트를 그대로 반영하지 않고, 현재 UI 상태와 병합한 뒤 적용하도록 처리하였습니다.

// 병합 함수 만들기
const mergeData = (serverData: any[], uiState: any[]) => {
  return serverData.map((item) => {
    const local = uiState.find((u) => u.id === item.id);
    return {
      ...item,
      checked: local?.checked || false, // UI 선택 상태 유지
    };
  });
};

✏️ 편집 도중에는 실시간 업데이트 차단

편집 중일 때 외부 상태 변화가 UI를 덮어쓰지 않도록, SSE를 끊고, 저장 후 다시 연결하는 흐름을 설계하였습니다.
이 과정에서 사용자 의도를 최우선으로 반영하는 UX를 구성하려 했습니다.

// useQueueEdit.ts
import { useState } from 'react';
import { startSSE, stopSSE } from './sse';

export const useQueueEdit = (queueId: string, getUIState: () => any[], setData: (data: any[]) => void) => {
  const [isEditing, setIsEditing] = useState(false);

  const startEdit = () => {
    stopSSE(); // 실시간 끊기
    setIsEditing(true);
  };

  const saveEdit = async () => {
    await saveQueue(); // 저장 API 호출
    setIsEditing(false);
    startSSE(queueId, setData, getUIState); // 저장 후 실시간 다시 연결
  };

  return {
    isEditing,
    startEdit,
    saveEdit,
  };
};

const saveQueue = async () => {
  const res = await fetch('/api/queue/save', { method: 'POST' });
  if (!res.ok) throw new Error('저장 실패');
};

🍞 사용자 경험을 고려한 토스트 및 인터랙션

큐를 저장하거나, 순서를 변경했을 때, 각 작업의 피드백을 빠르게 받을 수 있도록
toast 메시지를 출력하여 사용자 경험을 높였습니다.

// queueActions.ts
import { toast } from 'sonner';

export const saveQueue = async () => {
  try {
    await fetch('/api/queue/save', { method: 'POST' });
    toast.success('✅ 큐가 성공적으로 저장되었습니다.');
  } catch {
    toast.error('❌ 저장 중 오류가 발생했습니다.');
  }
};

export const reorderQueue = async (ids: string[]) => {
  try {
    await fetch('/api/queue/reorder', {
      method: 'POST',
      body: JSON.stringify({ order: ids }),
      headers: { 'Content-Type': 'application/json' },
    });
    toast.success('🔃 순서가 저장되었습니다.');
  } catch {
    toast.error('💥 순서 저장 중 오류가 발생했습니다.');
  }
};

🔍 아쉬운 점과 개선 방향

⚡ 퍼포먼스 최적화 (불필요한 렌더링 최소화)

SSE 수신 이벤트가 잦다 보니, 렌더링이 과도하게 일어나는 구간이 있었습니다.
이후 memo, useMemo, React Window 등을 도입하여 리렌더링을 줄였습니다.

📦 대용량 데이터 처리 성능

초기 로딩 시 데이터 양이 많아 느려지는 현상이 있었고,
이후 서버측 페이징 및 클라이언트 가상 스크롤 전략을 병행하여 해결했습니다.

🧹 코드의 관심사 분리

초기에는 SSE 로직, 테이블 렌더링, 상태 관리가 섞여 있었습니다.
이후 useSSE, useQueueTable, useEditMode 등의 커스텀 훅으로 분리하여
테스트성과 유지보수성을 높였습니다.

마무리 ~

실시간 시스템 개발은 상태 동기화, 성능 최적화, 그리고 사용자 경험을 모두 고려해야 하는 작업이었습니다.
이 과정에서 React의 상태 관리와 SSE의 활용에 대해 더 깊이 이해할 수 있었고, 실시간 기능을 구현할 때의 고민과 선택의 중요성을 실감했습니다 !!

profile
making a list, checking it twice 🐥

0개의 댓글