NestJS 파헤치기5 - Websockets-Gateways

재로미·2022년 12월 5일
2

nestjs

목록 보기
5/5
post-thumbnail

Overview: Websockets

지난 포스트들에서는 controller, providers, module 등 NestJS에서 기초적인 개념들을 배웠다면 파헤치기 5편부터는 좀 더 심화되고, 옵셔널하게 사용할 수 있는 개념들을 다루고자 한다.

이번 5편에서는 실시간 서버 api 호출 관련하여 현업에서 많이 사용되고 있는 websocket에 관해 다뤄볼 예정이다.

Websocket 이란?

등장배경

전통적으로 서버와 클라이언트 송수신은 HTTP를 통해서 이루어졌다.
이는 철저히 request/response로 문자메시지 방식처럼 일방적으로 왔다갔다 했으며 서버에서 먼저 request를 보내지 않았다.
90년대까지는 http 요청만으로 웹서비스를 만들었는데 점점 인터넷이 발전하고 다양한 웹기술의 등장, 발전과 더불어 실시간 데이터 처리, 클라이언트-서버 양방향 통신법에 대한 논의가 시작되었다.

개념

웹브라우저는 http 프로토콜로 request/response 가 이루어지는 데 TCP/IP socket처럼 connection이 유지되어 실시간으로 통신을 할 수 없다는 특징이 있다. 그래서 등장한 것이 TCP 기반 양방향 송수신의 ws(websocket) 프로토콜이다. OSI layers에서는 HTTP와 같은 7계층 위치하며, 현재 인터넷 환경(HTML5)에서 많이 사용되고 HTTP와 포트 공유가 가능하며 성능이 매우 좋다는 이점을 갖는다.

Websocket 특징

양방향 송수신 가능(Full duplex)

  • 데이터 송수신을 동시에 처리할 수 있는 통신방법
  • 클라이언트와 서버가 서로 원할 때 데이터를 주고 받을 수 있음
    • ws 프로토콜을 사용하면 웹소켓 포트에 접속해 있는 모든 클라이언트에게 이벤트 방식으로 응답함
  • 통상적인 http 통신은 단방향 통신임

실시간 네트워킹 가능(Real-time Networking)

  • 채팅, 주식, 비디오 등 웹 환경에서 연속된 데이터를 빠르게 노출함
  • 여러 단말기에서 빠르게 데이터 교환할 수 있음

동작방식

  • 요청
    요청
  • 응답
    응답
  • 웹소켓 연결 시
    연결
  • 전체 맥락에서

이를 바탕으로
1. 최초 접속에서만 http 프로토콜 위에서 handshaking 하기 때문에 http header를 사용함
2. 웹소켓을 위한 별도의 포트는 없으며, 기존 포트(http-80, https-443)을 사용
3. 프레임으로 구성된 메시지라는 논리적 단위로 송수신
4. 메시지에 포함될 수 있는 교환 가능한 메시지는 텍스트와 바이너리 뿐

이렇게 웹소켓의 특징을 정리해볼 수 있다. 그러나 웹소켓도 마냥 완벽한 것은 아니고, 한계가 있다.

웹소켓의 한계

앞서 언급했듯 HTML5에 최적화 된 프로토콜이기 때문에 HTML5 이전의 기술로 구현된 서비스에서는 동작하지 않게 된다.

이를 보완하기 위해 nodeJS의 경우 socket.io나 sockJS, 스프링의 경우 STOMP라는 기술을 추가적으로 사용하며, 이를 이용해 해당 브라우저에 맞게 동작할 수 있게 설정할 수 있다.

NestJS에서는 websocket과 socket.io 모두 지원하고 있으며, socket.io에 대해서 좀 더 자세히 살펴보자.

Socket.io

socket.io는 node.js 기반으로 실시간 이벤트 서버를 개발할 수 있는 오픈소스 라이브러리이다. 멀티 디바이스(web, android, ios, windows)를 지원하며, ws를 지원하지 않는 브라우저도 직관적으로 지원한다.

websocket server는 client와 서버 간에 http protocol로 커넥션을 초기에 맺고 ws-websocket protocol로 upgrade한 후 서로에게 heartbeat를 주기적으로 발생시켜 커넥션이 유지되고 있는지 체크하며 네트워크를 유지하는 방식이다.

개념

Namespace, room, and event
도식
socket io에서 트래픽을 격리하여 구분할 때 사용되는 단위이다. event는 명칭 그대로 송/수신하는 이벤트의 이름을 말한다.

트래픽격리 구분없이 이벤트를 송/수신하면 이벤트 리스너를 등록하여 이벤트를 처리하는 코드가 존재하지 않더라도 접속한 모든 client에 전송 및 수신을 하게 된다.

과도하게 많거나 설계가 중구난방이면 불필요한 트래픽이 발생하게 되고 서버 자체의 성능도 저하되기 때문에 적절한 설계로 구분할 필요가 있다.
특징은 다음과 같다.

  • 기본 네임스페이스는 / 이다
  • 방은 네임스페이스의 하위개념이다
  • 같은 네임스페이스, 같은 방 안에서만 소통할 수 있다

Public & Private & Broadcasting
socket io에서 이벤트를 송수신하는 방식을 말한다.

  • public : 이벤트를 송신하게 되면 송신한 자와 수신받는 자 모두 이벤트를 수신받음
  • broadcasting : 이벤트 송신자는 제외하고 수신자만 이벤트를 수신받음
  • private : 특정 client에게만 이벤트를 전송함

Cluster
nodejs는 기본적으로 싱글 프로세스로 동작하며 서버 CPU core 수만큼 process 생성하여 multi process로 구동하기 위해서는 cluster를 이용하여 process를 생성해야함

Master & worker
nodejs cluster를 이용해서 process를 생성하면 실제 일을 수행하는 process를 worker라고 하며 worker들을 제어하는 역할을 하는 process를 master라고 부른다.

socket.io와 ws의 차이

NestJS는 두 기능 다 지원한다고 하였는데, 각 모듈은 서로 미세한 차이가 있다. 그 차이는 다음과 같다.

1. socket.io는 추가적인 설치를 해야 함

  • ws는 new WebSocket(’서버주소/네임스페이스’)로 객체 바로 생성 가능
  • socket.io는 js 파일을 추가해야함

2. ws는 string 형으로 데이터를 전송함

  • 소켓io는 event 명과 데이터를 명확하게 구분하고 모든 자료형을 주고 받을 수 있음

3. ws에는 room이 없다.

  • namespace는 ws에서도 지원하지만 room과 broadcast 같은 소켓io 기능을 ws에선 직접 만들어야 할 수 있음

4. 자신을 제외한 사용자에게 데이터를 보내려면

  • socketIO에서는 서버.broadcast.emit(’이벤트명’, 데이터)
  • ws에서는 for 반복으로 클라이언트.send(데이터) 보내야함

5. socket.io는 연결이 끊어져도 주기적으로 연결을 시도함

  • 같은 기능을 구현하려면 ws는 추가적인 코드를 필요로함

6. socketIO에서는 to, of 같은 특정 클라이언트를 지정하는 기능도 제공함

Simple example

웹소켓과 socketIO에 대한 기본적인 개념과 특징은 알았으니 직접 실습하면서 이해해보자. 간단한 채팅앱을 만들어보겠다.

~$ nest new ws-chat-sample

nestJS 애플리케이션을 하나 만들고, 이 애플리케이션 내에 아래 라이브러리들을 설치하여 준다.

~$ yarn add @nestjs/websockets @nestjs/platform-socket.io
~$ yarn add -D @types/socket.io

우리가 만들 app tree는 다음과 같다.

socket-client
 |-chat-socket.js			   # client 단에서 웹소켓 연결에 필요한 js
 |-index.html				   # 화면에서 서버와 chat할 간단한 화면
src
 |- main.ts                    # Nest 앱의 실행파일
 |- app.module.ts              # 실행파일에서 등록, 사용하는 root 모듈파일
 |- app.controller.ts          # Nest 앱의 root 기본 컨트롤러
 |- chat.gateway.ts            # Nest 앱의 root 기본 비즈니스 서비스

먼저 chat.gateway.ts 파일을 생성하고 다음과 같이 class를 구성한다.

// chat.gateway.ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';

import { Server } from 'socket.io';

@WebSocketGateway() // 안에 port와 namespace를 속성으로 넣어줄 수 있다.
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  // handleMessage(client, data): void {} // client 직접적으로 사용하고 싶거나 decorator 사용 안 원하면 이렇게도 가능
  handleMessage(@MessageBody() message: string): void {
    this.server.emit('message', message);
  }
}

위 코드는 ChatGateway 라는 웹소켓 게이트웨이 클래스에서 웹소켓 서버 인스턴스인 server를 public 속성값으로 갖고,
'message'라는 이벤트로 들어오는 string의 메시지를 치리하는 handleMessage라는 메서드를 통해 클라이언트에서 들어오는 data를 emit 할 것을 주문하는 코드이다.
이를 통해 클라이언트의 데이터는 서버로 들어와서 얘를 클라이언트로 다시 뿌려주는 역할을 할 수 있게 된다. 이렇게 구현한 gateway를 모듈 프로바이더로 등록하자.

// app.module.ts
import { AppController } from './app.controller';
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  controllers: [AppController],
  providers: [ChatGateway],
})

export class AppModule {}

그 다음, 통신할 클라이언트를 간단하게 만들어주기 위해 socket-client라는 폴더를 만들고 여기에 index.html을 다음과 같이 만들어준다.

<!DOCTYPE html>
<html class="no-js" lang="">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>

  <body>
    <div>
      <ul id="messages"></ul>
    </div>

    <div>
      <input id="message" type="text" />
      <button onclick="handleSubmitNewMessage()">Submit</button>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.0/socket.io.js"></script>
    <script src="./chat-socket.js"></script>
  </body>
</html>

이 index 파일이 불러오는 자바스크립트 파일인 chat-socket.js는 아래와 같다.

const socket = io('http://localhost:3000', { transports: ['websocket'] }); // 서버주소가 http 프로토콜임을 유의

const message = document.getElementById('message');
const messages = document.getElementById('messages');

const handleSubmitNewMessage = () => {
  socket.emit('message', { data: message.value }); // 클라이언트에서 서버로 이벤트를 발생시킨다
};

// socket.on은 클라이언트단에서 발생한 이벤트를 선택적으로 캐치하여 이벤트 핸들러를 등록함
socket.on('message', ({ data }) => {
  handleNewMessage(data);
});

handleNewMessage = (message) => {
  messages.appendChild(buildNewMessage(message));
};

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

클라이언트에서 socket io의 io 객체를 사용하여 backend 주소를 첫번째 인자로, 두번째 인자로 option을 넘겨주고 있다. 이제 백엔드를 실행하고 index.html을 두개 열어보면 다음과 같이 동작함을 확인할 수 있다.
왼쪽
오른쪽

간단하게만 socket.io를 다루어보았는데 이 외에 of, to 등을 사용할 일이 있을 것이다. 추후 실습에서는 이를 다뤄보도록 하겠다.

참고 자료 출처

profile
정확하고 체계적인 지식을 가진 개발자 뿐만 아니라, 가진 지식을 사람들과 함께 나눌 수 있는 계발자가 되고 싶습니다

1개의 댓글

comment-user-thumbnail
2023년 11월 29일

app.module.ts에서 따로 gateway를 호출 안해도 되는건가요? 루트로 들어가면 어차피 실행이 되니까?
아니면 app.controller.ts에서 호출해야되나요? 그리고 또 port설정해서 서버에서 소켓을 여는게 있던데 main.ts에서 적혀져있는 3000번 포트랑 동일하게 해야하나요?

답글 달기