Socket.io활용해 채팅 만들기

👀·2024년 11월 9일

1. node.js socket서버 만들기

1.1 Socket.IO 설치 및 기본 설정

먼저 socket.io를 설치하고 서버와 클라이언트를 위한 소켓 통신 환경을 설정한다.

pnpm install socket.io

1.2 서버 코드 작성 (socket.ts)

서버 코드 예제에서는 사용자 연결 상태 관리, 메시지 수신/전송 등을 처리합니다.
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);
  }
}

1.3 Express 서버와 Socket 서버 연결 (main.ts)

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();

주요 기능 요약

  • 사용자 연결 상태 및 정보 관리: 연결된 모든 사용자의 소켓 ID 정보를 fetchSockets()를 통해 조회합니다.
  • 환영 메시지 및 클라이언트 수 전송: 새로운 사용자에게 환영 메시지와 현재 연결된 클라이언트 수를 전달합니다.
  • 메시지 수신/전송: send-message 이벤트를 통해 메시지를 수신하고, 클라이언트에게 전송합니다.
  • 연결 해제 알림: disconnect 이벤트 발생 시 다른 클라이언트에게 현재 클라이언트 수를 알려줍니다.
  • Express 설정: JSON 요청과 정적 파일을 처리할 수 있도록 설정.
  • 서버 인스턴스 생성 및 연결: Express 서버를 생성하고, 이 서버를 기반으로 Socket.IO 서버를 실행합니다.

2. 클라이언트 연동

2.1 Socket.IO 클라이언트 설치 및 소켓 초기화

pnpm install socket.io-client

2.2 클라이언트 소켓 정의 및 함수 구성

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)
}

2.3 클라이언트 Hook 예제

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 로직

주요 함수 설명

  • initializeSocket: 소켓 연결을 초기화하고, 연결 상태와 오류를 콘솔에 표시합니다.
  • connectSocket 및 disconnectSocket: 소켓 연결을 수동으로 제어할 수 있도록 합니다.
  • onMessage 및 emitMessage: 서버와의 이벤트 수신과 전송을 관리하는 함수입니다.
  • connectSocket(): 소켓 연결을 활성화합니다.
  • incoming 이벤트: 새로운 사용자가 접속 시, userName을 포함한 메시지를 채팅창에 추가합니다.
  • incoming-user-name 송신: 사용자 이름을 서버로 전송합니다.
  • receive-message 수신: 서버에서 받은 메시지를 채팅 목록에 추가합니다.

3. 서버와 클라이언트 간의 데이터 흐름

서버와 클라이언트가 이벤트를 주고받으며 소켓 연결 상태를 관리하고, 사용자와 메시지 정보를 전달하는 방식입니다.

이벤트 흐름 요약

서버 (Server)

  • emit: 특정 클라이언트에게만 이벤트를 전송합니다.
  • broadcast.emit: 현재 소켓 클라이언트를 제외한 모든 클라이언트에게 메시지를 전송합니다.
  • on: 클라이언트에서 송신된 이벤트를 수신합니다.

클라이언트 (Client)

  • emit: 서버에 이벤트를 전송합니다.
  • on: 서버에서 송신된 이벤트를 수신합니다.

정리

서버의 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  로직 
    })

socket.io 공식문서
전체 코드

0개의 댓글