[WebSocket] Socket.io의 Namespace 및 옵션 < 4 >

exceed_96·2024년 8월 13일
0

WebSocket

목록 보기
5/5
post-thumbnail

이번 포스팅에서는 Socket.io의 Namespace개념과 소켓 연결에 정의할 수 있는 몇몇 옵션들에 대해서 알아보자.



1. Namespace란?

Namespace는 클라이언트와 서버 간의 별도의 소켓통신 채널이다.

단일 소켓에서 여러 Namespace를 정의할 수 있으며 클라이언트는 특정 Namespace에 연결하여 해당 Namespace의 다른 클라이언트에게만 이벤트를 수신하거나 송신할 수 있다.

즉, 네임스페이스는 기본적으로 API의 서로다른 엔드포인트와 비슷한 역할을 하며, 동일한 소켓 내에서 다른 클라이언트 그룹을 위한 독립된 통신 채널을 제공한다.

API명세서 예시 소켓 명세서 예시

즉, 위 API명세서 예시에서 API콜 용도에 맞춰서 첫 번째 엔드포인트를 users, channels로 구분한 것처럼 소켓도 이와같이 하나의 소켓채널을 Namespace로 구분하여서 하나의 소켓채널을 용도에 맞는 여러 채널로 분리할 수 있다는 것이다.



2. 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를 연결시켜서 게임을 렌더링 할 데이터를 이벤트를 송,수신 할 수 있다.

즉, 불필요한 연결을 해서 서버에 부담을 주지 않고 필요할 때만 연결을 해서 이벤트를 송,수신 할 수 있다는 장점이 있는 것이다.



3. Namespace 예제 코드

이번 예제코드에서는 하나의 소켓채널을 연결하여서 해당 소켓을 2개의 Namespace로 구분하여서 연결할 것이다.

하나의 Namespace는 자동연결(autoConnect)옵션을 true로 하고 다른 Namespace는 자동연결(autoConnect)를 false로 정의할 것이다.

3.1 서버 (Express)

설치 패키지

npm i socket.io, @types/node, express, cors

백엔드 디렉토리 구조


src/server.ts

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


3.2 클라이언트(NextJS)


설치패키지

npm i socket.io-client


프론트 디렉토리 구조


src/app/layout.tsx

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

src/app/page.tsx

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

src/util/SocketConfig.ts

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

src/components/ChatRoom/FirstChatRoom.tsx

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

src/components/ChatRoom/SecondChatRoom.tsx

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


4. 예제 코드 실행 결과

실행 결과를 보기 전에 예제 코드를 한번 확인해 보면 firstNamespaceautoConnect옵션을 false로 정의함으로 써 자동연결을 하지 않고 secondNamespaceautoConnect옵션을 true로 정의함으로 써 자동연결하여 소켓채널에 연결하도록 하도록 정의되어 있다.

위와같이 Namespace 1 Chatting ===> firstNamespace는 채팅을 입력해도 소켓이 연결되어 있지 않기 때문에 이벤트 송,수신을 하지 않는다.

하지만 Namespace 2 Chatting ===> secondNamespaceautoConnect옵션을 true로 정의함으로 써 소켓이 연결되어 있어 이벤트 송, 수신을 하는걸 확인할 수 있다.

이와같이 Namespace를 구분하여서 소켓채널을 분리하여 용도에 맞게 소켓 이벤트 송,수신을 할 수 있다.



5. 예제 코드 뜯어보기

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옵션을 정의하지 않는다면 firstNamespaceautoConnect옵션의 값으로 모든 네임스페이스의 autoConnect옵션이 정의된다는 뜻이다.

따라서 이런 경우에는 부가적인 연결제어 혹은 추가적인 옵션을 정의해야 한다.


1. 명시적 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를 생성하여 독립적으로 제어


2. 수동으로 연결제어

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메서드를 통해서 수동으로 연결


3. 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옵션 말고 어떤 옵션들이 있을까?



6. Socket.io 옵션들

Socket.io에는 옵션의 용도에 따라서 IO factory Options, Low-level Engine Options, Manager Options, Socket Options로 구분하여 옵션을 정의할 수 있다.

이번 포스팅에서는 해당 옵션들 전부를 살펴보기 보다는 주로 쓰이는 옵션들에 대해서 간단하게 알아보고 다음 포스팅에서 전체 옵션을 하나한씩 정리해보도록 하겠다..!

autoConnect(true)

  • socket인스턴스 생성 시 자동으로 서버에 연결을 시도할지 여부를 결정하는 옵션이다.
  • false로 정의하면 명시적으로 connect메서드를 호출해야 연결이 시작된다.

reconnection(true)

  • 네트워크 불안정이나 서버 다운으로 연결이 끊어졌을 때의 동작을 제어한다.

  • true로 정의하면 클라이언트는 자동으로 재연결을 시도하여 연결의 연속성을 유지한다.

  • false로 정의하면 연결 끊김 시 수동으로 재연결을 처리해야 한다.


reconnectionAttempts(Infinity)

  • 재연결 시도의 최대 횟수를 정의한다.

  • 특정 숫자로 정의하면, 해당 숫자로 시도 후에 재연결을 포기하고 연결 실패 상태로 남게된다.


TransPorts(["polling", "websocket"])

  • 클라이언트가 서버와 통신할 때 사용할 수 있는 송,수신 방식을 정의한다.

  • 기본방식은 ["polling", "websocket"]이며 먼저 폴링으로 연결을 시도하고, 가능한 경우에 WebSocket으로 업그레이드 한다.

polling(Long Polling)

  • HTTP통신 프로토콜의 polling방식을 사용한다.

websocket

  • 실시간, 양방향 통신을 제공하는 방식이다.
  • websocket으로만 정의할 시 polling단계 없이 바로 "websocket`으로 연결된다.

randomizationFactor(0.5)

  • 재연결 지연 시간에 무작위성을 추가

  • 0에 가까울수록 일정한 간격으로, 1에 가까울수록 무작위적인 간격으로 재연결을 시도한다.

  • 해당 옵션은 여러 클라이언트가 동시에 재연결을 시도를 방지하는데 사용한다.


timeout(20000)

  • 초기 연결 혹은 재연결 시도 시 서버의 응답을 기다리는 최대 시간(ms)이다.

  • 이 시간 내에 서버로부터 응답이 없ㄷ으면 연결 시도를 실패로 간주한다.



7. 마치며

이번 포스팅에서는 Socket.ioNamespace에 대해서 간단하게 알아보고 예제 코드를 통해서 어떤식으로 코드를 구성하고 어떻게 활용하면 좋은지에 대해서 알아보았다. 웹소켓을 사용해보면 느낄수 있는 점이 생각보다 웹소켓의 확장성이 너무 좋아서 소켓을 한번 활용하면 이것저것 소켓으로 구성하게 된다는 점이다. Namespace는 이럴 때 용도에 맞게 웹소켓을 연결하거나 혹은 연결시키지 않는 채널을 Namespace로 활용하면 보다 더 실용적으로 웹소켓을 사용할 수 있다.

다음 포스팅에서는 Socket.ioRoom개념에 대해서 알아보고 간단한 예제 코드를 활용하여 어떤식으로 구성해야하는지 알아보자..!



8. Reference

https://roadtos7.github.io/network/2020/01/18/Network-SocketIO_Socket.html

profile
개발진행형

0개의 댓글