WebSocket 실시간 채팅 구현 가이드

📦 1. 패키지 설치

백엔드 (DaMoono-Backend)

npm install socket.io

프론트엔드 (DaMoono-Frontend)

npm install socket.io-client

🏗️ 2. 시스템 구조도

┌─────────────────────────────────────────────────────────────┐
│                        사용자 브라우저                          │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  /chat/consult (ChatConsultPage.tsx)                 │   │
│  │  - 상담 요청                                           │   │
│  │  - 메시지 전송/수신                                     │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            │
                            │ Socket.IO
                            │ (WebSocket)
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    백엔드 서버 (Node.js)                       │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Socket.IO Server (index.ts)                         │   │
│  │  - 세션 관리 (Map)                                     │   │
│  │  - 메시지 중계                                          │   │
│  │  - 이벤트 처리                                          │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            │
                            │ Socket.IO
                            │ (WebSocket)
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                       상담사 브라우저                           │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  /chat/admin (ChatAdminPage.tsx)                     │   │
│  │  - 세션 목록 조회                                       │   │
│  │  - 세션 참여                                           │   │
│  │  - 메시지 전송/수신                                     │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘


🔧 3. 백엔드 구현

📄 DaMoono-Backend/src/index.ts

import { Server } from 'socket.io';
import { createServer } from 'http';

// HTTP 서버 생성
const httpServer = createServer(app);

// Socket.IO 서버 설정
const io = new Server(httpServer, {
  cors: {
    origin: '<http://localhost:5173>',
    methods: ['GET', 'POST'],
    credentials: true,
  },
});

// 세션 저장소 (메모리)
const consultSessions = new Map();

// Socket 이벤트 처리
io.on('connection', (socket) => {
  console.log('클라이언트 연결:', socket.id);

  // 1. 사용자가 상담 시작
  socket.on('start-consult', (userId) => {
    const sessionId = `session-${Date.now()}`;
    consultSessions.set(sessionId, {
      userId,
      userSocket: socket.id,
      consultantSocket: null,
      status: 'waiting',
      createdAt: new Date(),
    });
    socket.join(sessionId); // Room 참여
    socket.emit('session-created', sessionId);
    io.emit('sessions-updated', getWaitingSessions());
  });

  // 2. 대기 중인 세션 목록 요청
  socket.on('get-waiting-sessions', () => {
    socket.emit('waiting-sessions', getWaitingSessions());
  });

  // 3. 상담사가 세션 참여
  socket.on('consultant-join', (sessionId) => {
    const session = consultSessions.get(sessionId);
    if (session) {
      session.consultantSocket = socket.id;
      session.status = 'connected';
      socket.join(sessionId);
      io.to(sessionId).emit('consultant-connected');
      io.emit('sessions-updated', getWaitingSessions());
    }
  });

  // 4. 메시지 전송
  socket.on('send-message', ({ sessionId, message, sender }) => {
    io.to(sessionId).emit('receive-message', {
      message,
      sender,
      timestamp: new Date(),
    });
  });

  // 5. 상담 종료
  socket.on('end-consult', (sessionId) => {
    io.to(sessionId).emit('consult-ended');
    consultSessions.delete(sessionId);
    io.emit('sessions-updated', getWaitingSessions());
  });

  socket.on('disconnect', () => {
    console.log('연결 해제:', socket.id);
  });
});

// 대기 중인 세션 필터링
function getWaitingSessions() {
  const waiting = [];
  for (const [sessionId, session] of consultSessions.entries()) {
    if (session.status === 'waiting') {
      waiting.push({
        sessionId,
        userId: session.userId,
        createdAt: session.createdAt,
      });
    }
  }
  return waiting;
}

// HTTP 서버 시작 (app.listen 대신)
httpServer.listen(PORT, () => {
  console.log('Socket.IO 서버 실행 중');
});


🎨 4. 프론트엔드 구현

📄 DaMoono-Frontend/src/services/socketService.ts

import { io, Socket } from 'socket.io-client';

class SocketService {
  private socket: Socket | null = null;
  private sessionId: string | null = null;

  // 서버 연결
  connect() {
    this.socket = io('<http://localhost:3000>');
    return this.socket;
  }

  // 연결 해제
  disconnect() {
    this.socket?.disconnect();
  }

  // === 사용자 기능 ===
  startConsult(userId: string) {
    this.socket?.emit('start-consult', userId);
  }

  // === 상담사 기능 ===
  getWaitingSessions() {
    this.socket?.emit('get-waiting-sessions');
  }

  joinSession(sessionId: string) {
    this.sessionId = sessionId;
    this.socket?.emit('consultant-join', sessionId);
  }

  // === 공통 기능 ===
  sendMessage(message: string, sender: 'user' | 'consultant') {
    if (this.sessionId) {
      this.socket?.emit('send-message', {
        sessionId: this.sessionId,
        message,
        sender,
      });
    }
  }

  endConsult() {
    if (this.sessionId) {
      this.socket?.emit('end-consult', this.sessionId);
    }
  }

  // === 이벤트 리스너 ===
  onSessionCreated(callback: (sessionId: string) => void) {
    this.socket?.on('session-created', (sessionId) => {
      this.sessionId = sessionId;
      callback(sessionId);
    });
  }

  onWaitingSessions(callback: (sessions: any[]) => void) {
    this.socket?.on('waiting-sessions', callback);
  }

  onSessionsUpdated(callback: (sessions: any[]) => void) {
    this.socket?.on('sessions-updated', callback);
  }

  onConsultantConnected(callback: () => void) {
    this.socket?.on('consultant-connected', callback);
  }

  onMessage(callback: (data: any) => void) {
    this.socket?.on('receive-message', callback);
  }

  onConsultEnded(callback: () => void) {
    this.socket?.on('consult-ended', callback);
  }
}

export default new SocketService();