Nest.Js) 중급자를 위한 Gateway/Socket.io 설명회

2023. 2. 8. 03:31JS

본 포스팅은 추상적인 개념을 설명하는 게 아닌 실제 코드나 Nest.js의 데코레이터, Typescript로 작성되는 코드 등 보다 구체화되어 있습니다.

Socket.io를 알아보기에 앞서 WebSocket에 대한 이해를 선행하는 편을 추천드립니다.

 

2023.02.05 - [WEB] - WEB) 초보자를 위한 웹소켓(WebSocket) 설명회

 

WEB) 초보자를 위한 웹소켓(WebSocket) 설명회

우리가 사용하는 다양한 어플리케이션 중에는 카카오톡이나 공유 문서, 게임처럼 실시간으로 화면이 변경되며 데이터가 수정되는 모델들이 존재합니다. 이러한 웹 어플리케이션들은 어떻게 데

youngmon.app

 

실시간 통신을 구현하기 위해 사용할 수 있는 라이브러리는 SockJs, Socket.io 또는 ws(Nest.js 내장)을 고려할 수 있습니다.

본 포스팅에서는 Socket.io 모듈을 사용하는데 이유는 Socket.io를 사용할 경우 WebSocket을 지원하지 않는 브라우저, 프록시가 있더라도 API를 동일하게 유지하면서 HTTP Streaming, Long Poling 등의 방식으로 에뮬레이팅(WebSocket 흉내) 해 주기 때문이며 보다 구현이 쉽다는 점이 있습니다.

 

필요 모듈

socket.io
socket.io

Nest.js에서 사용하기 위해 설치해야 하는 라이브러리는 다음과 같습니다.

npm i @nestjs/websockets \
      @nestjs/platform-socket.io \
      @type/socket.io

 

Gateway란?

Nest.js에서는 웹소켓 커넥션을 생성, 관리하기 위해 Gateway라는 개념을 사용합니다.

게이트웨이는 일반적으로 다른 두 시스템 또는 네트워크를 연결, 제어해 통신을 가능하게 하며, 호환, 보안, 성능 등을 보장하기 위해 사용되는 매개체를 뜻합니다. 예를 들어 인터넷과 내부 망을 연결하는 공유기, MSA 구조에서 복잡한 API를 묶어 외부에서 접근하기 쉽게 해 주고 권한과 로드밸런싱을 담당하는 API Gateway 등이 있습니다.

 

Nest.js에서 사용할 수 있는 웹소켓 구현체로는 ws, socket.io 등이 있습니다. 문제는 socket.io가 단순히 ws를 상속받는 것이 아니라 서로의 인터페이스가 다르다는 문제가 있기 때문에 이러한 저수준의 API를 Gateway라는 클래스로 래핑 해서 고수준의 Gateway API로 변경이 용이한 코드를 작성할 수 있게 합니다.

이러한 설계를 어댑터 패턴이라고 하며, 인터페이스 호환을 통한 의존성 감소를 위해 사용합니다. Gateway로 웹소켓 관련 코드를 작성한 후, Gateway에 적용될 구현체를 변경하는 것으로 코드의 변경을 최소화하면서 어플리케이션을 수정할 수 있습니다.

 

사용해 보기

@WebSocketGateway 데코레이터로 웹소켓 게이트웨이 클래스를 정의하고 경로, 네임스페이스, CORS 등의 옵션을 지정할 수 있습니다. 웹소켓 게이트웨이로 사용할 클래스를 지정했다면, 클래스 내부에서는 크게

  • WebSocketServer : 게이트웨이에서 사용하는 기본 웹소켓 서버 인스턴스를 정의, 연결된 클라이언트 소켓들을 가져올 수 있으며 여러 유용한 기능을 제공
  • SubscribeMessage : 서버에서 메시지를 받을 때 실행할 메소드 정의(.on()의 역할)
  • OnGatewayConnection/Disconnect : 클라이언트가 연결/해제 이벤트 감지를 위한 인터페이스

등등

 

동작 방식

socket.io의 동작을 극도로 단순화시켜 설명하자면 emit()으로 전송하고, on()으로 받습니다.

emit의 인자로 보내지는 값은 이벤트의 이름(네임스페이스)과 자바스크립트 구조체인 데이터 페이로드(옵션), 이벤트 전송 이후 실행될 콜백 함수(옵션)입니다.

예를 들어

@WebSocketServer()
private server;	//	웹소켓에 사용될 서버 인스턴스 주입

this.server.emit("abc", {
	data: "asd",
});	// 해당 네임스페이스 서버의 "abc" 이벤트에 data를 전달

해당 서버 함수에서는 "abc" 이벤트에 "asd"라는 문자열을 data라는 이름으로 전달합니다.

@SubscribeMessage("abc")	//	socket.on("abc", (payload) => {});
handlerF(@MessageBody() payload) {};	//	Nest.js

이후 "abc" 이벤트를 기다리고 있던 handlerF 메소드가 실행되며 필요에 따라 함께 넘어온 payload 객체를 이용해 비즈니스 로직을 수행할 수 있습니다.

 

예제

클라이언트 코드(.html)

<!DOCTYPE html>
<html>
<head>
  <title>Socket.io Example</title>
  <script src="https://cdn.socket.io/4.5.4/socket.io.min.js" integrity="sha384-/KNQL8Nu5gCHLqwqfQjA689Hhoqgi2S84SNUxC3roTe4EhJ9AfLkp8QiQcU8AMzI" crossorigin="anonymous"></script>
</head>
<body>
  <h1 id = "title"></h1>
  <ul id = "msg-list">
  </ul>
  <div>
	  <input id="msg" type="text" />
	  <button onclick="handleSend()">Send</button>
  </div>
  <script src="./index.js"></script>
</body>
</html>

클라이언트 코드(.js)

const name = prompt('name?');
document.getElementById("title").textContent = 'USER : ' + name;
const socket = io('http://localhost:3001/chat?name=' + name, { transports: ['websocket'] });
const message = document.getElementById('msg');
const messages = document.getElementById('msg-list');

const handleSend = () => {
  socket.emit('message', { data: msg.value });
};

socket.on('pub-message', ( data ) => {
	console.log(data);
  handlePrintMessage(data);
});

handlePrintMessage = (message) => {
  messages.appendChild(buildMessage(message));
};

const buildMessage = (message) => {
  const li = document.createElement('li');
  li.appendChild(document.createTextNode(message));
  return li;
};

 

서버 비즈니스 로직

  async handleConnection(sock) {
	sock.name = sock.handshake.query.name;
	this.logger.verbose(`${sock.name} is Connected.`);
	this.server.emit('pub-message', `${sock.name} is entered!`);
  }

  async handleDisconnect(sock) {
	this.logger.verbose(`${sock.name} is Disconnected.`);
	this.server.emit('pub-message', `${sock.name} is leaves!`);
  }
  @SubscribeMessage('message')
  handleMessage(@ConnectedSocket() sock,
				@MessageBody() payload: any) {
	this.logger.log(this.handleMessage.name + " " + sock.name + " : " + payload.data);
    this.server.emit('pub-message', `${sock.name} : ${payload.data}`);
  }

비즈니스 로직에 대해 간단히 설명하자면

  1. 클라이언트와 소켓 커넥션이 생기면 서버 전체에 특정 클라이언트가 들어왔다고 공지
  2. 클라이언트가 특정 데이터를 서버에 보내면 서버에서 전체 사용자들에게 pub
  3. 클라이언트가 연결이 종료되면 전체 서버에 공지
반응형

실제 구동

실제 구동 영상

예제 코드를 작성하면서 프론트엔드 프레임워크를 하나 정도 배우고 싶은 생각이 들었습니다.

일단 서버사이드부터 잘해야 할 텐데 말이죠.

 

아마도 요건 시리즈물로 두 편 정도 더 나올 것 같습니다. 웹소켓 재밌네요

 

이론보다 구체적인 프레임 워크와 코드로 설명하는 비중이 있기에 중급자라고 적었습니다만, 제가 초급자이기 때문에 찐 중급자 분들이 보시기에 미흡한 부분이 넘쳐날 수 있습니다. 틀린 부분이 있다면 지적 부탁드립니다🧚‍♂️

반응형

'JS' 카테고리의 다른 글

Node.Js란?  (2) 2023.02.01