[React][Node.js] 실시간 알림 시스템 구축 (feat. WebSocket, SSE)

[React][Node.js] 실시간 알림 시스템 구축 (feat. WebSocket, SSE)

개발중에 DB의 값이 변하면 실시간으로 웹페이지에 값이 변하는 기능을 구현을 하고있었다.

(물론 새로고침 없이)

리액트 쿼리를 사용하기 등 여러 방법을 생각해보다가 어쨋든간에 프론트에서 백으로 get이든 post든 때려서 값을 갱신해야된다.
그러기 위해선 어떤 방법이 있을까.. 고민해보았다

그래서 이번 글에서는 React랑 Node.js 써서 데이터베이스의 값이 바뀔 때마다 프론트엔드에 바로바로 알려주는 시스템 만드는 법을 알아보겠다 🚀


왜 실시간 알림이 필요할까? 🤔

refetchInterval 써서 주기적으로 데이터 가져오는 거? 그거 좀 별로야... 😓 서버에 계속 물어보니까 불필요한 요청도 많고, 로그도 쌓이고...
너무 1차원적이다

구현 목표

  1. 백엔드: WebSocket이나 SSE 서버를 만든다.
  2. 데이터베이스: 뭐 바뀌면 (INSERT, UPDATE, DELETE) 백엔드에 알려준다!
  3. 백엔드: 데이터베이스한테 소식 들으면? 연결된 클라이언트(프론트엔드)에게 "야! 뭐 바뀌었다!" 하고 알려준다.
  4. 프론트엔드: 알림 받으면? React Query 캐시를 싹 갈아엎거나(invalidateQueries), 아니면 새 데이터로 바로 업데이트(setQueryData) 한다.

장점:

  • 실시간 업데이트: 데이터 바뀌는 순간 바로바로 화면에 반영! 👍
  • 효율적인 네트워크 사용: 필요한 때만 통신하니까 깔끔! ✨

단점:

  • refetchInterval 보다는 구현이 조금 복잡하다... (그도 그럴만한게 refetchInterval: ms만 쓰면 끝이다)

코드 레벨

1. 백엔드 (Node.js) - WebSocket

WebSocket 서버 구축하는 예시다 (SSE도 비슷하게 할 수 있다)

// 필요한 모듈 설치: npm install ws mysql2
const WebSocket = require('ws');
const mysql = require('mysql2');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
  console.log('Client connected!');

  ws.on('close', () => {
    console.log('Client disconnected!');
  });
});

// 데이터베이스 연결 설정
async function connectDB() {
    const connection = await mysql.createConnection({
      host: 'localhost',
      user: 'your_user',
      password: 'your_password',
      database: 'your_database',
    });

    // 변경 감지를 위한 트리거 설정 (MariaDB/MySQL)
    const triggerQuery = `
    CREATE TRIGGER IF NOT EXISTS equip_change_trigger
    AFTER INSERT OR UPDATE OR DELETE ON equip
    FOR EACH ROW
    BEGIN
       -- 간단한 알림을 위해 변경 타입과 ID만 전송
       -- 프로시저를 생성하여 호출
      CALL notify_equip_change(IF(NEW.id IS NULL, OLD.id, NEW.id), IF(NEW.id IS NULL, 'DELETE', IF(OLD.id IS NULL, 'INSERT', 'UPDATE')));
    END;
    `;

    // 프로시저 생성
     const procedureQuery = `
    CREATE PROCEDURE IF NOT EXISTS notify_equip_change(IN equipId INT, IN changeType VARCHAR(10))
    BEGIN
      SET @message = CONCAT('{"type": "EQUIP_CHANGED", "payload": {"id": ', equipId, ', "changeType": "', changeType, '"}}');
      
      -- WebSocket을 통해 클라이언트에 메시지 전송
      -- (실제로는 별도의 테이블에 메시지를 저장하고, 주기적으로 또는 long-polling 등으로 처리하는 것이 일반적)
    END;
    `;

    await connection.execute(triggerQuery);
    await connection.execute(procedureQuery);

    // 실시간 알림을 위한 간이 polling (더 나은 방법: Debezium, mysql-events 등)
    setInterval(async () => {
        const [rows] = await connection.execute('SELECT 1 FROM equip LIMIT 1'); // 간단한 쿼리, 최적화 필요
        if(rows.length > 0)
        {
            wss.clients.forEach(client => {
                if (client.readyState === WebSocket.OPEN) {
                    client.send(JSON.stringify({
                        type: 'EQUIP_CHANGED',
                        payload: {
                        }
                    }));
                }
            });
        }

    }, 1000); // 1초마다 확인 (조정 가능)

    return connection;
}

connectDB().then((connection)=>{
    console.log("db connect ok");
}).catch((err) => {
    console.error('Database connection error:', err);
});

설명:

  • ws 모듈로 WebSocket 서버를 만들었다.
  • mysql2 모듈로 MariaDB(MySQL) 데이터베이스에 연결했다.
  • equip 테이블에 트리거(equip_change_trigger)를 생성해서 INSERT, UPDATE, DELETE 이벤트 발생 시 notify_equip_change프로시저를 호출한다.
    • 트리거는 변경된 행의 ID와 변경 유형(INSERT, UPDATE, DELETE)을 프로시저에 전달합니다.
  • notify_equip_change 프로시저에서 변경알림 메시지를 생성한다.
  • setInterval을 써서 주기적으로 데이터베이스 바뀌었나 확인하고, 바뀌었으면 연결된 WebSocket 클라이언트에게 알림을 보낸다.
    • 주의: 여기서는 간단하게 폴링(polling) 방식을 썼다. 실제 프로덕션 환경에서는 Debezium, mysql-events 같은 전문적인 CDC(Change Data Capture) 도구를 쓰는 게 더 효율적이고 안정적이다. 아니면, 별도 테이블에 변경 이벤트 기록하고, 주기적으로 확인하는 방법도 고려해 볼 수 있다.
  • 여기선 소스로 한번에 구현했지만 보통 SQL 툴로 DB 작업을 하고 비즈니스 로직만 백엔드에서 수행하면 된다.

2. 프론트엔드 (React)

// 필요한 모듈 설치: npm install react-query
import { useEffect } from 'react';
import { useQueryClient } from 'react-query';

function EquipList() {
  const queryClient = useQueryClient();

  useEffect(() => {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onopen = () => {
      console.log('WebSocket connected!');
    };

    ws.onmessage = event => {
      const message = JSON.parse(event.data);

      if (message.type === 'EQUIP_CHANGED') {
        // 방법 1: 캐시 무효화 (데이터 다시 가져오기)
        queryClient.invalidateQueries({ queryKey: ['equipList'] });

        // 방법 2: 캐시 직접 업데이트 (더 효율적)
        // queryClient.setQueryData(['equipList'], (oldData) => {
        //   // message.payload에 변경된 데이터가 있다고 가정
        //   return updateData(oldData, message.payload); // updateData 함수는 직접 구현
        // });
      }
    };

    ws.onclose = () => {
      console.log('WebSocket disconnected!');
    };

    return () => {
      ws.close();
    };
  }, [queryClient]);

  // ... (equipList 쿼리 사용 부분) ...

  return (
    <div>
      {/* ... 장비 목록 표시 ... */}
    </div>
  );
}

설명:

  • useEffect 훅 안에서 WebSocket 연결을 만들고, 메시지를 처리한다.
  • EQUIP_CHANGED 메시지를 받으면, React Query 캐시를 업데이트한다.
    • invalidateQueries: 'equipList' 쿼리를 무효화해서 데이터 다시 가져오게 한다.
    • setQueryData: 캐시 데이터를 직접 업데이트한다 (이게 더 효율적이다). updateData 함수는 직접 구현해야 한다 (예: 기존 데이터에서 변경된 항목 찾아서 바꾸기).

이 예시는 기본적인 실시간 알림 시스템 구축 방법을 보여준다. 실제 애플리케이션에서는 에러 처리, 인증, 보안 등 추가로 고려해야 할 것들이 있을 수 있다.

댓글