
먼저 socket.io를 설치하고 서버와 클라이언트를 위한 소켓 통신 환경을 설정한다.
pnpm install socket.io
서버 코드 예제에서는 사용자 연결 상태 관리, 메시지 수신/전송 등을 처리합니다.
socket 파일을 작성해 준다.
- fetchSockets: 현재 연결된 클라이언트 수
- io.in("room1").socketsJoin("room2"): room1에 있는 클라이언트를 room2로 이동시킨다.
- io.in("room1").disconnectSockets(): room1 소켓 연결 해제한다.
- await io.in("room1").fetchSockets(): room1에 연결된 클라이언트 수
=> 예제 코드에는 room없이 구현할 생각.
// socket.ts
import { Server } from "socket.io";
const origin = "*";
export class ChatSocket {
private mSocket: Server;
constructor(http: any) {
this.mSocket = new Server(http, {
cors: { origin, credentials: false },
});
}
connect() {
this.mSocket.on("connect", async (socket) => {
const userInfo = async () => {
const sockets = await this.mSocket.fetchSockets();
return sockets.map(({ id }) => ({ socketId: id }));
};
socket.emit("welcome", {
socketId: socket.id,
users: userInfo(),
clientsCount: (await userInfo()).length,
});
socket.on(
"incoming-user-name",
async ({ userName }: { userName: string }) => {
socket.emit("incoming", {
clientsCount: (await userInfo()).length,
userName,
});
socket.broadcast.emit("incoming", {
clientsCount: (await userInfo()).length,
userName,
});
}
);
socket.on(
"send-message",
({ id, message }: { id: string; message: string }) => {
socket.emit("receive-message", { id, message });
socket.broadcast.emit("receive-message", { id, message });
}
);
socket.on("disconnect", async () => {
socket.broadcast.emit("leave-user", {
clientsCount: (await userInfo()).length,
});
});
});
this.mSocket.listen(3000);
}
}
Express와 Socket 서버를 통합하는 코드.
// main.ts
const express = require("express");
const cors = require("cors");
const http = require("http");
import { ChatSocket } from "./socket";
const port = 80;
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({ origin: "*" }));
app.use("/", express.static("public"));
const server = http.createServer({}, app);
server.listen(port, () => console.log(`listening at port ${port}`));
const socket = new ChatSocket(server);
socket.connect();
pnpm install socket.io-client
import { io, Socket } from "socket.io-client"
const SOCKET_SERVER_URL = "http://localhost:3000"
let socket: Socket | null = null
// 소켓 초기화 함수
export const initializeSocket = () => {
if (!socket) {
socket = io(SOCKET_SERVER_URL, {
withCredentials: false, // 쿠키나 인증 정보 전송 필요 시 설정
autoConnect: true, // 자동 연결 방지 (원할 때 연결 가능)
})
// 연결 이벤트 리스너 추가
socket.on("connect", () => {
console.log("Socket connected:", socket?.id)
})
// 에러 이벤트 리스너 추가
socket.on("connect_error", (error) => {
console.error("Connection error:", error)
})
// 연결 해제 이벤트 리스너
socket.on("disconnect", () => {
console.log("Socket disconnected")
})
}
}
// 소켓 연결 함수
export const connectSocket = () => {
if (!socket) initializeSocket()
socket?.connect()
}
// 소켓 연결 해제 함수
export const disconnectSocket = () => {
socket?.disconnect()
}
// 소켓 이벤트 수신 함수
export const onMessage = (event: string, callback: (data: any) => void) => {
socket?.on(event, callback)
}
// 소켓 이벤트 전송 함수
export const emitMessage = (event: string, data: any) => {
socket?.emit(event, data)
}
connectSocket()
onMessage("incoming", ({ clientsCount, userName }) => {
setUserList({ userCount: clientsCount })
setChatList((prev) => [
...prev,
{ id: "", message: `[ ${userName} ]등장 !` },
])
})
emitMessage("incoming-user-name", { userName })
onMessage("leave-user", ({ clientsCount }) => {
setUserList({ userCount: clientsCount })
})
onMessage("welcome", ({ clientsCount }) => {
setUserList({ userCount: clientsCount })
})
onMessage("receive-message", ({ id, message }) => {
setChatList((prev) => [...prev, { id, message }])
})
socket에 있는 함수로 on , emit을 호출해 준다.
연결이 되면
server에서 welcome 송신 => 현 코드에서 현재 연결된 클라이언트 수를 전송합니다.
client에서 welcome 수신 => 현 코드에서는 setUserList에 총 연결 수를 저장합니다.
client에서 incoming-user-name 송신 => userName을 서버에 전송
server에서 incoming-user-name 수신 =>
client에서 userName을 수신한 뒤,
emit과 broadcast로 연결된 모든 클라이언트에게 userName을 전송합니다.
=> [userName] 등장 메시지 띄우는 등 활용
userName을 socket.data.userName으로 저장할 수 있습니다.
client에서 incoming 수신 => userName을 활용한 UI 로직
서버 (Server)
클라이언트 (Client)
서버의 broadcast를 활용할 때는 클라이언트의 데이터 일관성을 고려해야 한다. 예를 들어, 서버와 클라이언트 간 연결 오류가 발생하면 서버와 클라이언트의 데이터가 불일치할 수 있으므로, 가능한 서버에서 직접 UI 로직을 제어하는 것이 좋다.
server에서 broadcast를 쓸 때, server의 호출에 의해 client 데이터를 핸들링하는게 좋다.
예를 들면, 채팅 메시지를 보낼 때, 채팅 state에 내가 보낸 메시지를 업데이트하고 서버에
보내서 broadcast를 하게 되면 socket에 error가 났을 경우, 서버의 데이터와 client의 데이터가 달라진다.
예를 들면,
// server
socket.emit('receive-message',{userName:'test-user',message:'hello'})
socket.broadcast.emit('receive-message',{userName:'test-user',message:'hello'})
으로 전송을 해준 뒤,
// client
socket.on('receive-message',({userName:'test-user',message:'hello'})=>{
// 채팅 UI 로직
})