Socket.io

개요

socket.io는 양방향 소통을 위한 js library이다.

지원내역

  • 실시간 통신 : 서버, 클라간 양방향 데이터 전송
  • 자동폴백 : websocket우선사용
  • 브로드 캐스트 : 같은 브로드캐스트 내의 유저에게 정보전달
  • 네임스페이스 : 기능별로 채널 분리 가능
  • room : 네임스페이스 내에서 그룹 나눔가능

기본 흐름

  1. 클라이언트가 서버에 socket 연결 요청 => socket.emit()
  2. 서버가 연결 수락 ( connection ) => io.on("connection", (socket)=>{})
  3. 서로 이벤트를 주고 받으며 실시간 통신 => socket.on("emit")
  4. 연결 해제시 disconnect

네임스페이스 vs room

네임스페이스 :

  • 서버내 기능을 논리적으로 구분하기 위한 통로
  • 큰 카테고리를 담당함. ex) 채팅기능, 영상기능, 뉴스기능 등 여러 기능을 수행할 때

room

  • 네임스페이스 내부의 작은 그룹

코드

socket 백엔드 서버( express.js )

const express = require("express");
const http = require("http");
const { Server: SocketIOServer } = require("socket.io");

const app = express();
const server = http.createServer(app);

// Socket.IO 서버 설정
let io;
const rooms = {};

// Socket.IO 초기화
io = new SocketIOServer(server, {
  path: "/socket", // 소켓 경로 설정
  cors: {
    origin: "*", // CORS 허용 (필요에 따라 설정)
    methods: ["GET", "POST"],
  },
});


// 소켓 연결 처리
io.on("connection", (socket) => {
  console.log("A user connected:", socket.id);

  // 방 목록 요청 처리
  socket.on("getRoomList", () => {
    const roomList = Object.keys(rooms);
    socket.emit("roomList", roomList);
  });

  // 방에 참가
  socket.on("joinRoom", ({ chatId }) => {
    socket.join(chatId);

    // 해당 방에 아무도 없으면 방장으로 설정
    if (!rooms[chatId]) {
      rooms[chatId] = {
        owner: socket.id,
        users: new Set([socket.id]),
        elements: [],
      };
      socket.emit("setOwner", true); // 클라이언트에 방장임을 알림
    } else {
      rooms[chatId].users.add(socket.id);
      socket.emit("setOwner", false); // 클라이언트에 방장이 아님을 알림
      socket.emit("initializeDrawing", rooms[chatId].elements);
    }
    console.log(`User ${socket.id} joined room ${chatId}`);
  });

  socket.emit("initializeDrawing", () => {
    console.log("initializeDrawing emit");
  });
  // 드로잉 초기화
  socket.on("initializeDrawing", () => {
    console.log("initializeDrawing on");
    socket.emit("initializeDrawing", rooms[chatId].elements);
  });

  // 메시지 전송
  socket.on("chatMessage", ({ chatId, message }) => {
    io.to(chatId).emit("chatMessage", message);
  });

  // 방장이 그린 요소를 브로드캐스트
  socket.on("drawingUpdate", ({ chatId, elements }) => {
    if (rooms[chatId]?.owner === socket.id) {
      rooms[chatId].elements = elements;
      socket.to(chatId).emit("drawingUpdate", elements);
    }
  });

  // 유저가 나갔을 때 처리
  socket.on("disconnect", () => {
    console.log("A user disconnected:", socket.id);

    // 방에서 나가고 방이 빈 경우 폐기
    Object.keys(rooms).forEach((chatId) => {
      rooms[chatId]?.users.delete(socket.id);

      if (rooms[chatId]?.users.size === 0) {
        console.log(`Deleting empty room ${chatId}`);
        delete rooms[chatId];
      }
    });
  });
});

// Express 기본 라우트 설정
app.get("/", (req, res) => {
  res.send("Socket.IO chat server is running");
});

// 서버 실행
const PORT = 5000;
server.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

프론트 화면

page.tsx

"use client";
import { useGetRoomList } from "@/hooks/useSocket";
import {
  Button,
  Flex,
  Heading,
  Section,
  Separator,
  Text,
} from "@radix-ui/themes";
import { useRouter } from "next/navigation";
import { v4 as uuidv4 } from "uuid"; // UUID를 사용해 랜덤 chatId 생성
import styles from "./styles.module.scss";
import { useThrottle } from "@/hooks/useThrottle";
import { MdOutlineRefresh } from "react-icons/md";
import { stackRouterPush } from "@/utils/stackRouter";

export default function ChatPage() {
  const router = useRouter();
  const { rooms, refresh } = useGetRoomList();

  const refreshHandler = useThrottle(refresh, 1000);

  const createRoom = () => {
    const chatId = uuidv4(); // 랜덤 chatId 생성
    stackRouterPush(router, `/chat-detail/${chatId}`);
  };

  const joinRoom = (chatId: string) => {
    stackRouterPush(router, `/chat-detail/${chatId}`);
  };

  return (
    <div>
      <Section className={styles.chatCreateWrapper}>
        <Heading as="h2">chat방 생성</Heading>
        <Text>chat방을 만들고 사용자에게 url을 공유하세요</Text>
        <Button onClick={createRoom}>Create Room</Button>
        <Separator orientation="horizontal" size="4" />
      </Section>

      <Section className={styles.chatListWrapper}>
        <Flex direction={"row"} justify={"between"}>
          <Heading as="h2">room List</Heading>
          <MdOutlineRefresh
            size={26}
            onClick={refreshHandler}
            className={styles.refreshIcon}
          />
        </Flex>
        <Flex direction={"column"} gap="1">
          {rooms.map((item) => (
            <Button
              key={item}
              className={styles.chatRoom}
              onClick={() => joinRoom(item)}
            >
              {item}
            </Button>
          ))}
        </Flex>
        <Separator orientation="horizontal" size="4" />
      </Section>
    </div>
  );
}

front hooks

// hooks/useSocket.js
import { useEffect, useRef, useState } from "react";
import io, { Socket } from "socket.io-client";

export function useSocket(chatId: string) {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isOwner, setIsOwner] = useState(false);

  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_CHATSOCKET_URL!, {
      path: "/socket",
    });

    socket.emit("joinRoom", { chatId });

    socket.on("setOwner", (isOwner) => {
      setIsOwner(isOwner);
    });

    socket.on("roomClosed", () => {
      alert("The room was closed by the owner.");
      // 추가적으로 리디렉션 로직도 여기에 포함 가능
    });

    setSocket(socket);

    return () => {
      socket.disconnect();
    };
  }, [chatId]);

  return { socket, isOwner };
}

export function useGetRoomList() {
  const [rooms, setRooms] = useState([]);
  const socketRef = useRef<Socket | null>(null);

  // refresh 
  const refresh = () => {
    if (socketRef.current) {
      socketRef.current.emit("getRoomList");
    }
  };

  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_CHATSOCKET_URL!, {
      path: "/socket",
    });
    socketRef.current = socket;
    // 방 목록 요청
    socket.emit("getRoomList");

    // 서버에서 받은 방 목록 처리
    socket.on("roomList", (roomList) => {
      setRooms(roomList);
    });

    // 컴포넌트 언마운트 시 소켓 이벤트 정리
    return () => {
      socket.off("roomList");
    };
  }, []);

  return { rooms, refresh };
}