[Socket.io] 클라이언트의 실시간 채팅 구현기
https://socket.io/docs/v4/tutorial/introduction
왜 Socket.io인가
기본적으로 WS 모듈과 비교를 해보았을때, room이라는 기능으로 채팅방을 분리하거나, broadcast로 연결된 전체 클라이언트에게 데이터를 보내는 기능들을 더 제공해준다.
또한 WS의 경우 오랜된 브라우저의 경우 지원하지 않는 경우가 있다고 한다. Socket.io는 WS를 기반으로 만들어졌지만 지원이 되지 않는 경우 폴백 처리가 이뤄져 호환성이 더 좋다.
Socket.io는 채팅방에 특화된 라이브러리라는 점에서 각 스트리밍 채널의 다양한 타입의 채팅을 처리해주기에 적절하다고 생각되었다.
소켓 통신과 관련된 node:net과 WS는 이전에 사용해보기도 했기에 Socket.io를 잘 활용해보는 것이 생산성이나 학습 측면에서 좋은 경험이 될 것이라 기대된다.
클라이언트 - 백 연결과정
https://socket.io/how-to/use-with-react
bash
yarn add socket.io-client
각 채팅방 별로 메세지를 분리해서 받는 기능 구현
서버
javascript
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("A user connected:", socket.id);
socket.on("joinRoom", (room) => {
socket.join(room);
console.log(`User ${socket.id} joined room ${room}`);
socket
.to(room)
.emit("receiveMessage", `User ${socket.id} has joined room ${room}`);
});
// 클라이언트가 방을 떠날 때
socket.on("leaveRoom", (room) => {
socket.leave(room);
console.log(`User ${socket.id} left room ${room}`);
});
socket.on("sendMessage", ({ room, message }) => {
console.log(`Message received in room ${room}: ${message}`);
io.to(room).emit("receiveMessage", message);
});
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
});
});
server.listen(8080, () => {
console.log("Server is running on http://localhost:8080");
});
클라이언트 (리액트)
javascript
import { useEffect, useState } from "react";
import "./App.css";
import io from "socket.io-client";
const socket = io("http://localhost:8080");
function App() {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState<string[]>([]);
const [room, setRoom] = useState("");
const [currentRoom, setCurrentRoom] = useState(""); // 현재 참여 중인 방을 추적
useEffect(() => {
socket.on("receiveMessage", (newMessage) => {
setMessages((prevMessages) => [...prevMessages, newMessage]);
});
return () => {
socket.off("receiveMessage");
};
}, []);
const joinRoom = () => {
if (room.trim()) {
// 기존 방이 있으면 나가기
if (currentRoom) {
socket.emit("leaveRoom", currentRoom);
}
// 새로운 방에 참여
socket.emit("joinRoom", room);
setCurrentRoom(room); // 현재 참여 중인 방 업데이트
setMessages([]); // 방이 바뀔 때 메시지 초기화
}
};
const sendMessage = () => {
if (message.trim() && room) {
socket.emit("sendMessage", { room, message });
setMessage("");
}
};
return (
<div>
<h1>Chat Room</h1>
<div>
<label>Select a room: </label>
<select onChange={(e) => setRoom(e.target.value)}>
<option value="">Choose...</option>
<option value="1">Room 1</option>
<option value="2">Room 2</option>
<option value="3">Room 3</option>
</select>
<button onClick={joinRoom}>Join Room</button>
</div>
<div>
{messages.map((msg, index) => (
<p key={index}>{msg}</p>
))}
</div>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
export default App;
치지직의 채팅 데이터
채팅 보내기
json
{
"ver": "3", // 프로토콜 버전 (버전 정보)
"cmd": 3101, // 명령 코드 (이벤트의 종류를 나타냄)
"svcid": "game", // 서비스 ID (어떤 서비스에서 발생한 이벤트인지 식별)
"cid": "N1UlG6", // 클라이언트 ID (이벤트가 발생한 클라이언트의 식별자)
"sid": "VSZR_D50TQJXWM6J1XzLCGHsQikfVFjakLc6LxC5huenPdGXja4pb9Y0CxvLZwZ17wJkDGqcLsWxoGudv0L0Mg--", // 세션 ID (사용자 세션을 고유하게 식별하는 ID)
"retry": false, // 재시도 여부 (해당 이벤트가 재시도된 것인지 여부)
"bdy": { // 본문 데이터 (이벤트의 주요 내용)
"msg": "ㅋㅋㅋㅋ", // 메시지 내용 (사용자가 보낸 텍스트)
"msgTypeCode": 1, // 메시지 타입 코드 (메시지 유형을 나타내는 코드)
"extras": "{\"chatType\":\"STREAMING\",\"osType\":\"PC\",\"extraToken\":\"Cc+YfCOlE4/0odC1UwFeEAfs++s/GzveFyfayz8pWzlbf1d1TKyF9t7bdHSrBwPjnRXlXfCxHlXsjD1TWN3jbQ==\",\"streamingChannelId\":\"19e3b97ca1bca954d1ac84cf6862e0dc\",\"emojis\":{}}", // 추가 정보 (JSON 형식으로 포함된 추가 데이터)
"msgTime": 1731568963648 // 메시지 타임스탬프 (메시지가 발생한 시간, 밀리초 단위)
},
"tid": 3 // 트랜잭션 ID (이벤트를 구별하기 위한 고유 식별자)
}
채팅 받기
json
{
"svcid": "game", // 서비스 ID (예: game, 채팅 서비스 등)
"ver": "1", // 버전 (서비스의 버전 정보)
"bdy": [ // 메시지 본문(body) 배열
{
"svcid": "game", // 서비스 ID (예: game)
"cid": "N1V4Pr", // 채팅 세션 ID 또는 메시지의 고유 ID
"mbrCnt": 2109, // 현재 채팅방에 참여한 사용자 수
"uid": "07c7a5dbffe7fb901cceb68a9a731cd1", // 사용자 고유 ID
"profile": "{ // 사용자 프로필 정보 (JSON 문자열로 저장됨)
\"userIdHash\": \"07c7a5dbffe7fb901cceb68a9a731cd1\", // 사용자 ID 해시 값
\"nickname\": \"USTARD\", // 사용자 닉네임
\"profileImageUrl\": \"\", // 사용자 프로필 이미지 URL (없을 경우 빈 문자열)
\"userRoleCode\": \"common_user\", // 사용자 역할 코드 (예: common_user, 관리자 등)
\"badge\": null, // 사용자 배지 정보 (없을 경우 null)
\"title\": null, // 사용자 제목 (없을 경우 null)
\"verifiedMark\": false, // 사용자 인증 여부 (false: 인증되지 않음)
\"activityBadges\": [], // 활동 배지 정보 (없을 경우 빈 배열)
\"streamingProperty\": { // 스트리밍 관련 사용자 정보
\"nicknameColor\": { // 닉네임 색상 정보
\"colorCode\": \"CC000\" // 색상 코드
},
\"activatedAchievementBadgeIds\": [] // 활성화된 업적 배지 ID들
}
}",
"msg": "잘생겼네", // 채팅 메시지 내용
"msgTypeCode": 1, // 메시지 유형 코드 (예: 1은 일반 텍스트 메시지, 다른 값은 다른 유형)
"msgStatusType": "NORMAL", // 메시지 상태 (예: NORMAL은 일반 메시지, 삭제된 메시지 등)
"extras": "{ // 메시지 관련 추가 정보 (JSON 문자열로 저장됨)
\"chatType\": \"STREAMING\", // 채팅 유형 (예: 스트리밍 채팅)
\"osType\": \"PC\", // 운영체제 유형 (예: PC, 모바일 등)
\"extraToken\": \"HK+d/vpahmIA2m5YJnqtMS/kqPaXti0yEE17mLZGXxyaIy6MzZbzzaf5GJUaavoqq2RSjwfP0d5dp4WgLeAjxg==\", // 추가 인증 토큰 또는 데이터
\"streamingChannelId\": \"8a59b34b46271960c1bf172bb0fac758\", // 스트리밍 채널 ID
\"emojis\": {} // 이모지 정보 (없을 경우 빈 객체)
}",
"ctime": 1731568455198, // 메시지 생성 시간 (타임스탬프, 밀리초 단위)
"utime": 1731568455198, // 메시지 마지막 수정 시간 (타임스탬프, 밀리초 단위)
"msgTid": null, // 메시지의 고유 트랜잭션 ID (없을 경우 null)
"msgTime": 1731568455198 // 메시지 전송 시간 (타임스탬프, 밀리초 단위)
}
],
"cmd": 93101, // 명령 코드 (예: 메시지 전송 명령)
"tid": "38", // 트랜잭션 ID (이 메시지가 속한 트랜잭션의 고유 ID)
"cid": "N1V4Pr" // 채팅 세션 ID 또는 메시지의 고유 ID
}
클라이언트가 필요한 데이터
- cid : 채팅 세션 ID
- streamingChannelId: 스트리밍 채널 ID
- bdy : 메세지 본문
- profile
- uid(userIdHash): 사용자 고유 ID
- userRoleCode : 어떻게 호스트랑 클라이언트의 채팅을 분별할지 논의 필요
- nickname : 사용자 닉네임
- nicknameColor : 사용자 닉네임 색상
- msgTypeCode: 메세지 타입 (우리의 경우에는 일반, 질문, 공지로?)
- msgStatusType: 메세지 상태 (질문 채팅의 경우에는 미답변, 답변완료 이렇게?)
-
ctime msgTime: 메세지 생성 시간 (만약 다시보기에서 채팅 보여주려면 필요)
- profile
- cmd: 명령코드
- 지피티 왈. 채팅 시스템에서 흔히 사용되는 다른 cmd 값들:
1001
: 채팅방 입장1002
: 채팅방 퇴장2001
: 메시지 전송2002
: 메시지 삭제
- 지피티 왈. 채팅 시스템에서 흔히 사용되는 다른 cmd 값들:
클라이언트 설계
채팅 컴포넌트(ChatRoom)에서 소켓 연결을 설정하는 것이 적절하다고 판단.
활용방식 참고
- 연결될 소켓 url을 구하고 (엔드포인트 + 채널 번호)
- createSocket으로 클라이언트 소켓 생성
- Socket.connect
- emit으로 지정된 cmd 전송 (처음 진입시 JOIN_ROOM과 같은 명령어)
- 컴포넌트 state로 socket set
- input 컴포넌트에는 socket 객체도 전달
전역상태로 socket을 가지게 하면 안되나?
없거나 에러가 날때만 socket 재생성 및 conect 되도록..
고려해야할 부분
- 채팅창 닫을시에 chatroom 언마운트가 되면 안됨. 연결은 유지되어야한다.
- 공지가 오면 바로 보여줘야함
- 질문 탭은 어떻게 분리? 걍 클라이언트에서만??
- 가져온 메세지 리스트에서 질문 채팅만 필터링하고 거기서 답변미완료 질문을 또 필터링? 과정이 너무 길다
- 차라리 이벤트 분리를 해서 채팅과 질문탭이 따로 관리되도록하면 더 좋을 것 같기도…………..
- 결론 : 이벤트 설계만 잘해두면 생각보다는 덜 어려운듯
메인 페이지 스트리밍 띄워주기는 SSE가 가장 낫지않을까?
image.png
암튼 데모는 완료~
Subscribe to Liboo.blog
Get the latest posts delivered right to your inbox