- 구현 문서(백엔드 전 코드 및 설명)
- 추가된 문서
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();