이번 포스팅에서는 Socket.io의 Namespace개념과 소켓 연결에 정의할 수 있는 몇몇 옵션들에 대해서 알아보자.
Namespace
는 클라이언트와 서버 간의 별도의 소켓통신 채널이다.
단일 소켓에서 여러 Namespace
를 정의할 수 있으며 클라이언트는 특정 Namespace
에 연결하여 해당 Namespace
의 다른 클라이언트에게만 이벤트를 수신하거나 송신할 수 있다.
즉, 네임스페이스는 기본적으로 API의 서로다른 엔드포인트와 비슷한 역할을 하며, 동일한 소켓 내에서 다른 클라이언트 그룹을 위한 독립된 통신 채널을 제공한다.
API명세서 예시 | 소켓 명세서 예시 |
---|
즉, 위 API명세서 예시에서 API콜 용도에 맞춰서 첫 번째 엔드포인트를 users
, channels
로 구분한 것처럼 소켓도 이와같이 하나의 소켓채널을 Namespace
로 구분하여서 하나의 소켓채널을 용도에 맞는 여러 채널로 분리할 수 있다는 것이다.
import { io, Socket } from "socket.io-client";
-------- "firstNamespace"네임스페이스 연결 --------
export const firstNamespace: Socket = io("ws://localhost:5000/firstNamespace", {
autoConnect: false,
});
------- "secondNamespace"네임스페이스 연결 --------
export const secondNamespace: Socket = io(
"ws://localhost:5000/secondNamespace",
{
autoConnect: true,
}
);
위 소켓연결 로직을 보면 하나의 소켓채널(/
)을 2개의 Namespace
로 구분하여서 연결한걸 확인할 수 있다.
firstNamespace
, secondNamespace
는 용도에 맞춰서 해당 Namespace
에만 이벤트를 송,수신 할 수 있다.
예를들면 "실시간 채팅", "실시간 게임"기능이 둘 다 있는 서비스가 있다고 생각해보자.
해당 서비스는 실시간 채팅,게임을 하나의 소켓채널로 이벤트를 송,수신하는게 좋을까?
물론, 상황에 따라서 하나의 소켓 채널로 이벤트를 송,수신하는 경우가 좋을수도 있겠지만 용도에 따라서 게임Namespace
, 채팅Namespace
로 구분해서 사용하는게 소켓을 더 효율적으로 사용할 수 있다.
그럼 왜 더 효율적이라고 하는걸까??
예를 들면, 실시간 게임일 경우 게임을 진행하는 경우에만 해당 게임Namespace
를 연결시켜서 게임을 렌더링 할 데이터를 이벤트를 송,수신 할 수 있다.
즉, 불필요한 연결을 해서 서버에 부담을 주지 않고 필요할 때만 연결을 해서 이벤트를 송,수신 할 수 있다는 장점이 있는 것이다.
이번 예제코드에서는 하나의 소켓채널을 연결하여서 해당 소켓을 2개의 Namespace
로 구분하여서 연결할 것이다.
하나의 Namespace
는 자동연결(autoConnect)옵션을 true로 하고 다른 Namespace
는 자동연결(autoConnect)를 false로 정의할 것이다.
npm i socket.io
, @types/node
, express
, cors
import express, { Application } from "express";
import cors from "cors";
import http from "http";
import { Server, Socket } from "socket.io";
const app: Application = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const server = http.createServer(app);
const io = new Server(server, { //소켓채널 생성
cors: {
origin: "*",
},
});
const firstNamespace = io.of("/firstNamespace"); //firstNamespace 생성
const secondNamespace = io.of("/secondNamespace"); //secondNamespace 생성
firstNamespace.on("connection", (socket: Socket) => { //firstNamespace 이벤트 송,수신 로직
socket.on("message", (data: { message: string }) => {
firstNamespace.emit("message", data);
});
});
secondNamespace.on("connection", (socket: Socket) => { //secondNamespace 이벤트 송,수신 로직
socket.on("message", (data: { message: string }) => {
secondNamespace.emit("message", data);
});
});
server.listen(5000, () => console.log("Server running on port 5000"));
npm i socket.io-client
import "./globals.css";
export const metadata = {
title: "Socket",
description: "Socket study",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="h-svh flex justify-center items-center w-full">
{children}
</body>
</html>
);
}
import FirstChatRoom from "@/components/ChatRoom/FirstChatRoom";
import SecondChatRoom from "@/components/ChatRoom/SecondChatRoom";
export default function page() {
return (
<div className="flex w-full h-full justify-center items-center gap-10">
<FirstChatRoom />
<SecondChatRoom />
</div>
);
}
import { io, Socket } from "socket.io-client";
export const socket: Socket = io("ws://localhost:5000");
export const firstNamespace: Socket = io( //"firstNamespace`네임스페이스 연결
"ws://localhost:5000/firstNamespace", {
autoConnect: false,
forceNew: true,
});
export const secondNamespace: Socket = io( //"secondNamespace"네임스페이스 생성
"ws://localhost:5000/secondNamespace",
{
autoConnect: true,
forceNew: true,
}
);
"use client";
import { useState, useRef, useEffect, FormEvent } from "react";
import { firstNamespace } from "@/util/SocketConfig";
type TChatMessage = {
message: string;
};
export default function FirstChatRoom() {
const [firstChatLog, setFirstChatLog] = useState<TChatMessage[]>([]);
const firstMessageRef = useRef<HTMLInputElement>(null);
const submitFirstMessageHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (firstMessageRef.current?.value) {
firstNamespace.emit("message", {
message: firstMessageRef.current.value,
});
firstMessageRef.current.value = "";
}
};
const getFirstMessagesSocketHandler = (data: TChatMessage) => {
setFirstChatLog((prevChatLog) => [...prevChatLog, data]);
};
useEffect(() => {
firstNamespace.on("message", getFirstMessagesSocketHandler);
return () => {
firstNamespace.off("message", getFirstMessagesSocketHandler);
};
}, []);
return (
<div className="w-3/5 sm:w-2/5 h-3/5 border-[2px] border-black flex flex-col justify-between items-center bg-yellow-200">
<div className="text-center py-2 text-2xl text-white bg-green-500 w-full">
Namespace 1 Chatting
</div>
<ul className="w-full h-full flex flex-col gap-2 px-4 py-2 overflow-y-auto">
{firstChatLog.map((message, index) => (
<li
className="bg-white p-2 break-words whitespace-pre-line w-fit max-w-[60%] rounded-xl shadow-md"
key={index}
>
{message.message}
</li>
))}
</ul>
<form
className="w-full flex border-t-[1px] border-t-black"
onSubmit={submitFirstMessageHandler}
>
<input
type="text"
className="w-5/6 outline-none py-1 px-2 bg-green-900 text-xl text-white"
ref={firstMessageRef}
/>
<button type="submit" className="bg-white w-1/6">
전송
</button>
</form>
</div>
);
}
"use client";
import { useState, useRef, useEffect, FormEvent } from "react";
import { secondNamespace } from "@/util/SocketConfig";
type TChatMessage = {
message: string;
};
export default function SecondChatRoom() {
const [secondChatLog, setSecondChatLog] = useState<TChatMessage[]>([]);
const secondMessageRef = useRef<HTMLInputElement>(null);
const submitSecondMessageHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (secondMessageRef.current?.value) {
secondNamespace.emit("message", {
message: secondMessageRef.current.value,
});
secondMessageRef.current.value = "";
}
};
const getSecondMessagesSocketHandler = (data: TChatMessage) => {
setSecondChatLog((prevChatLog) => [...prevChatLog, data]);
};
useEffect(() => {
secondNamespace.on("message", getSecondMessagesSocketHandler);
return () => {
secondNamespace.off("message", getSecondMessagesSocketHandler);
};
}, []);
return (
<div className="w-3/5 sm:w-2/5 h-3/5 border-[2px] border-black flex flex-col justify-between items-center bg-yellow-200">
<div className="text-center py-2 text-2xl text-white bg-green-500 w-full">
Namespace 2 Chatting
</div>
<ul className="w-full h-full flex flex-col gap-2 px-4 py-2 overflow-y-auto">
{secondChatLog.map((message, index) => (
<li
className="bg-white p-2 break-words whitespace-pre-line w-fit max-w-[60%] rounded-xl shadow-md"
key={index}
>
{message.message}
</li>
))}
</ul>
<form
className="w-full flex border-t-[1px] border-t-black"
onSubmit={submitSecondMessageHandler}
>
<input
type="text"
className="w-5/6 outline-none py-1 px-2 bg-green-900 text-xl text-white"
ref={secondMessageRef}
/>
<button type="submit" className="bg-white w-1/6">
전송
</button>
</form>
</div>
);
}
실행 결과를 보기 전에 예제 코드를 한번 확인해 보면 firstNamespace
는 autoConnect
옵션을 false로 정의함으로 써 자동연결을 하지 않고 secondNamespace
는 autoConnect
옵션을 true로 정의함으로 써 자동연결하여 소켓채널에 연결하도록 하도록 정의되어 있다.
위와같이 Namespace 1 Chatting ===> firstNamespace
는 채팅을 입력해도 소켓이 연결되어 있지 않기 때문에 이벤트 송,수신을 하지 않는다.
하지만 Namespace 2 Chatting ===> secondNamespace
는 autoConnect
옵션을 true로 정의함으로 써 소켓이 연결되어 있어 이벤트 송, 수신을 하는걸 확인할 수 있다.
이와같이 Namespace
를 구분하여서 소켓채널을 분리하여 용도에 맞게 소켓 이벤트 송,수신을 할 수 있다.
import { io, Socket } from "socket.io-client";
export const firstNamespace: Socket = io("ws://localhost:5000/firstNamespace", {
autoConnect: false,
forceNew: true,
});
export const secondNamespace: Socket = io(
"ws://localhost:5000/secondNamespace",
{
autoConnect: true,
forceNew: true,
}
);
위 "SocketConfig.ts"파일에서 각각의 네임스페이스를 연결 할 때 autoConnect
옵션을 활용해서 firstNamespace
는 자동연결을 false로 정의하고 secondNamespace
는 자동연결을 true로 정의한 걸 볼 수 있다.
다만 여기서 두 소켓통신채널의 연결을 각기 다르게 해줄 수 있는 forceNew
옵션을 봐야한다.
Socket.io
는 같은 호스트와 포트에 대해 하나의 Manager
인스턴스를 공유한다.
이 경우, "ws//localhost:5000"이 공통 호스트인 것이다.
Manager
레벨에서 autoConnect
옵션이 true로 정의된다면, 모든 네임스페이스에 대해 자동연결을 시도하게 된다. 만약 여러 네임스페이스를 정의할 때, 가장 먼저 생성된 Socket 인스턴스의 autoConnect
정의로 결정된다.
즉, 위 경우에서 forceNew
옵션을 정의하지 않는다면 firstNamespace
의 autoConnect
옵션의 값으로 모든 네임스페이스의 autoConnect
옵션이 정의된다는 뜻이다.
따라서 이런 경우에는 부가적인 연결제어 혹은 추가적인 옵션을 정의해야 한다.
Manager
생성import { io, Socket, Manager } from "socket.io-client";
const firstManager = new Manager("ws://localhost:5000", { autoConnect: false });
const secondManager = new Manager("ws://localhost:5000", { autoConnect: true });
export const firstNamespace: Socket = firstManager.socket("/firstNamespace");
export const secondNamespace: Socket = secondManager.socket("/secondNamespace");
각 네임스페이스에 대해 별도의 Manager
를 생성하여 독립적으로 제어
import { io, Socket } from "socket.io-client";
export const firstNamespace: Socket = io("ws://localhost:5000/firstNamespace", {
autoConnect: false,
});
export const secondNamespace: Socket = io("ws://localhost:5000/secondNamespace", {
autoConnect: false,
});
firstNamespace.connect();
secondNamespace.connect();
각 네임스페이스를 connect
메서드를 통해서 수동으로 연결
forceNew
옵션 사용import { io, Socket } from "socket.io-client";
export const firstNamespace: Socket = io("ws://localhost:5000/firstNamespace", {
autoConnect: false,
forceNew: true
});
export const secondNamespace: Socket = io("ws://localhost:5000/secondNamespace", {
autoConnect: true,
forceNew: true
});
각 네임스페이스에 대해 새로운 Manager
인스턴스를 강제로 생성
위 3가지 방법중에 하나를 선택하여서 각 네임스페이스 연결을 제어할 수 있다.
그럼 Socket.io
에는 forceNew
옵션 말고 어떤 옵션들이 있을까?
Socket.io
에는 옵션의 용도에 따라서 IO factory Options
, Low-level Engine Options
, Manager Options
, Socket Options
로 구분하여 옵션을 정의할 수 있다.
이번 포스팅에서는 해당 옵션들 전부를 살펴보기 보다는 주로 쓰이는 옵션들에 대해서 간단하게 알아보고 다음 포스팅에서 전체 옵션을 하나한씩 정리해보도록 하겠다..!
connect
메서드를 호출해야 연결이 시작된다.네트워크 불안정이나 서버 다운으로 연결이 끊어졌을 때의 동작을 제어한다.
true로 정의하면 클라이언트는 자동으로 재연결을 시도하여 연결의 연속성을 유지한다.
false로 정의하면 연결 끊김 시 수동으로 재연결을 처리해야 한다.
재연결 시도의 최대 횟수를 정의한다.
특정 숫자로 정의하면, 해당 숫자로 시도 후에 재연결을 포기하고 연결 실패 상태로 남게된다.
클라이언트가 서버와 통신할 때 사용할 수 있는 송,수신 방식을 정의한다.
기본방식은 ["polling", "websocket"]
이며 먼저 폴링으로 연결을 시도하고, 가능한 경우에 WebSocket
으로 업그레이드 한다.
polling
방식을 사용한다.websocket
으로만 정의할 시 polling
단계 없이 바로 "websocket`으로 연결된다.재연결 지연 시간에 무작위성을 추가
0에 가까울수록 일정한 간격으로, 1에 가까울수록 무작위적인 간격으로 재연결을 시도한다.
해당 옵션은 여러 클라이언트가 동시에 재연결을 시도를 방지하는데 사용한다.
초기 연결 혹은 재연결 시도 시 서버의 응답을 기다리는 최대 시간(ms)이다.
이 시간 내에 서버로부터 응답이 없ㄷ으면 연결 시도를 실패로 간주한다.
이번 포스팅에서는 Socket.io
의 Namespace
에 대해서 간단하게 알아보고 예제 코드를 통해서 어떤식으로 코드를 구성하고 어떻게 활용하면 좋은지에 대해서 알아보았다. 웹소켓을 사용해보면 느낄수 있는 점이 생각보다 웹소켓의 확장성이 너무 좋아서 소켓을 한번 활용하면 이것저것 소켓으로 구성하게 된다는 점이다. Namespace
는 이럴 때 용도에 맞게 웹소켓을 연결하거나 혹은 연결시키지 않는 채널을 Namespace
로 활용하면 보다 더 실용적으로 웹소켓을 사용할 수 있다.
다음 포스팅에서는 Socket.io
의 Room
개념에 대해서 알아보고 간단한 예제 코드를 활용하여 어떤식으로 구성해야하는지 알아보자..!
https://roadtos7.github.io/network/2020/01/18/Network-SocketIO_Socket.html