Socket.io
개요
socket.io는 양방향 소통을 위한 js library이다.
지원내역
- 실시간 통신 : 서버, 클라간 양방향 데이터 전송
- 자동폴백 : websocket우선사용
- 브로드 캐스트 : 같은 브로드캐스트 내의 유저에게 정보전달
- 네임스페이스 : 기능별로 채널 분리 가능
- room : 네임스페이스 내에서 그룹 나눔가능
기본 흐름
- 클라이언트가 서버에 socket 연결 요청 =>
socket.emit()
- 서버가 연결 수락 ( connection ) =>
io.on("connection", (socket)=>{})
- 서로 이벤트를 주고 받으며 실시간 통신 =>
socket.on("emit")
- 연결 해제시 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 };
}