개발/Node

# [node] 웹소켓을 사용한 실시간 채팅 구현

ForrestPark 2025. 2. 2. 14:31

chapter 13 웹소켓을 사용한 실시간 채팅 구현

  • 웹소켓은 서버도 클라이언트의 요청없이 응답을 줄 수 있음.
    grk 플랫폼 내부 채팅기능은 어디에서 사용되어야 하는가?
  • project? task? project 내부에서만 채팅이 이루어지도록 해볼까?

13.1 웹소켓 소개

폴링 방식 : 주기적으로 요청을 보내서 응답을 받음
롱폴링 방식 : 클라이언트와 서버간의 커넥션을 유지한 상태로 응답을 주고 받는 방식
요청한 데이터에 변화가 있을떄 응답을 보냄.
롱폴링은 요청을 보낸후 응답 대기 후 응답이 옴ㄴ 바로 다시 요청을 보냄.

웹소켓

  • 하나의 tcp 커넥션으로 서버와 클라이언트간에 양방향 통신을 할 수 있게 만든 프로토콜
  • 대부분의 웹브라우저에서 안정적으로 사용. (IE9 오래된 웹브라우저는 지원불가)
  • 실시간 네트워킹 구현에 용이함.

13.1.1 웹소켓의 동작 방법

핸드 쉐이크 : 서버와 클라이언트가 커넥션을 맺는 과정, 최초 한번만 일어남. HTTP 1.1 프로토콜을 사용하고 헤더에 Upgrade: websocket 과 Connection : Upgrade 를 추가해 웹소켓 프로토콜 사용 하도록 함.

데이터 전송 :

(1) 핸드쉐이크

핸드 쉐이크 요청

GET /chat HTTP/1.1 // 핸드 쉐이킹은 GET 으로 보내야함. 
Host: server.example.com
Upgrade: websocket // 현재 프로토콜에서 다른 프로토콜로 업그레이드하는 규칙
Connection: Upgrade //Upgrade filed 가 있으면 명시
Sec-WebSocket-Key: dgh~ // 클라이언트 키 
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat

// 클라이언트가 요청하는 하위 프로토콜 
Sec-WebSocket-Version

핸드쉐이크 응답

HTTP/1.1 101 Switching Protocols // 프로토콜 전환 되어 연결 잘됨 
Upgrade: websocket
Connection : Upgrade
//클라이언트로부터 받은 키를 사용해 계산된 값
Sec-WebSocket-Accept: ~~
Sec-WebSocket-Protocol: chat
  • 핸드쉐이크 완료시 http 에서 ws 로 https 면 wss 로 변경 됨

(2) 2단계 데이터 전송

  1. 데이터는 message 라 부름.
  2. 메시지는 프레임(frame) 의 모음
  3. 프레임은 바이트의 배열이며 다음과 같은 형태를 가짐
  • 프레임은 헤더와 페이로드(Payload) 로 이루어짐
  • 헤더에는 FIN, RSV1~3, 오프코드(opcode) , 마스크(MASK) ,payload 길이 , 마스킹 키 가 있음.

FIN: 1(마지막), 0(데이터 더있음.)
opcode(payload type):
{
0001: text,
0010 : 바이너리,
1000: 커넥션 끊는다!
1001: ping(클라이언트가 ping 을 날림)
1010: pong(서버가 pong 으로 응답. )
}

(3) 3단계 : 접속을 끊음. opcode: 1000

  • 웹소켓은 데이터를 전송하는것만 지원.
  • 채팅방 만들기, 채팅방 전체 사람들에게 메세지 발송, 접속끊어진경우 자동 연결 등 soket.io 가 이런부분을 지원해줌.

13.3 간단한 채팅 어플리케이션 nest js 만들기

순서

  1. 프로젝트 생성및 패키지 설치
  2. 정적 파일 서비스를 위한 main.ts 설정
  3. socket.io 서버 구동을 위한 게이트 웨이 만들기
  4. 클라이언트 측 코드 작성(index.html)
  5. test

13.3.1 socket io 프로젝트 생성

(1) nest cli 를 통한 디렉토리

cd grk/src

nest g module events
nest g gateway events 


npm i @nestjs/websockets @nestjs/platform-socket.io
npm i -D @types/socket.io

(2) html 파일 불러오도록 main ts 설정

main.ts

// Static Asset 설정
import { NestExpressApplication } from '@nestjs/platform-express';
  // HTML form 파일을 제공할 staticAsset 
  app.useStaticAssets(join(__dirname,'..','static'))
  • NestExpressApplication 에는 useStaticAssets 메서드가 있음.
  • static 폴더를 정적 파일 경로로 지정.

(3) index.html 파일 생성

grk/static/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>GRK Research Project Chat Room</title>
    </head>
    <body>
        <div id="chat">GRK Chat</div>
    </body>
</html>

13.3.4 서버측 작업을 위한 게이트 웨이 생성

  • 게이트 웨이 사용시 의존성주입, 데코레이터, 필터, 가드 등의 NestJS 기능을 사용가능,. 프로토콜이 http 라면 컨트롤러로 부터 요청을 받고 프로토콜이 ws 라면 게이트웨이로 부터 요청을 받음. 진입점의 차이가 다르며 사용한느 데코레이터가 달라짐.
  • 게이트웨이는 웹소켓용 컨트롤러..
  • 게이트 웨이를 붙이는 법 @WebSocketGateway() 데코레이터를 클래스에 붙이면 됨.

project 에서 chat 기능 실현

cd src 
nest g gateway project

project.gateway.ts

import { 
  SubscribeMessage, 
  WebSocketGateway,
  WebSocketServer,

} from '@nestjs/websockets';
// socket io 임포트 
import { Server, Socket } from 'socket.io'

@WebSocketGateway()
export class ProjectGateway {

  // 웹 소켓 서버 인ㅅ턴스 선언 
  @WebSocketServer() server : Server

  // message event 구독
  @SubscribeMessage('message')
  handleMessage(socket: Socket, data : any): void {
    // 접속한 클라이언트에게 멧지 전송
    this.server.emit('message', `client-${socket.id.substring(0,4)} : ${data}`,)
  }
}

@WebSocketGateway() : 기본포트 3000을 사용
@WebSocketGateway(port)
@WebSocketGateway(options)
@WebSocketGateway(port,option)

-> 향후 Config 사용해서 웹소켓 포트를 따로 설정해 두도록 하자.

  • @SubscribeMessage('message') : 'message' 라는 이벤트를 구독하는 리스너임.
  • message 이벤트로 데이터 전송시 data 인수에 데이터가 담겨있음.
  • data 는 @MessageBody() , socket 에는 @ConnectedSocket() 데코가 필요함. -> @SubscribeMessage 데코에서는 기본 세팅 됨.
  • 웹 소켓 인스턴스의 emit() 메서드를 사용해 클라이언트 전체에 멤시지를 보냄.
  • 첫째 인수인 message 는 이벤트 명, 두번째인수는 보내주는 데이터
  • socket.io 에서는 모든 클라이언트 인스턴스에 임의의 id 값을 줌.
  • id 에는 무작위 문자열이 저장되어 있음.

13.3.5 게이트웨이를 모듈에 등록.

project.module.ts

// 웹소켓 체팅을 한임포트 
import { ProjectGateway } from './project.gateway';

    providers: [
        ProjectService, ProjectMongoRepository,
        // 웹소켓 게이트 웨이 프로바이더 
        ProjectGateway,

    ],
    exports: [ProjectService],
})
  • 게이트웨이는 컨트롤러 와 같은 개념이지만 게이트웨이는 다른 클래스에서 주입해서 사용할 수있는 프로바이더여서 모듈에 등록해야함

13.3.6 클라이언트를 위한 index.html

  • 표준 프로토콜에서 브라우저 자체 웹소켓 지원.
  • socket.io 는 브라우저에서 지원 하지 않으므로 클라이언트에서 socket.io를 사용하도록 라이브러리 설정 필요

src/static/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>GRK Research Project Chat Room</title>
    </head>
    <body>
        <h2> Simple chat</h2>
        <div id="chat">GRK Chat</div>

        <input type="text" id="message" placeholder="input your message">
        <button onclick="sendMessage()">전송</button>


    </body>
    <!-- jquery 로드 -->
    <script src = "http://code.jquery.com/jquery-3.6.1.slim.js"></script>
    <!-- socket.io 클라이언트 로드  -->
    <script src="http://localhost:3000/socket.io/socket.io.js"></script>
    <script>
        // socket.io 인스턴스 생성 
        const socket =io('http://localhost:3000/project/chat')
        // 전송 버튼 클릭 시 입력된 글을 message 이벤트로 보냄 
        function sendMessage() {
            const message = $('#message').val()
            socket.emit('message', message)

            $('#message').val('') // 입력창 비우기
        }
        socket.on('connect', () => {
            console.log('connected')
        })

        socket.on('message',(message) => {
            $('#chat').append(`<div>${message}</div>`)
        })

    </script>
</html>

이후 프로젝트 모듈에 게이트웨이를 프로바이더에 추가해야하고 appmodule 에서 project 모듈을 추가해야함. !!

결과

13.4 채팅방기능이 있는 채팅 어플리케이션

  • 네임스페이스는 네임스페이스로 지정된 곳에만 이벤트를 발생시킴
  • 멀티플랙싱이라고도 함.
  • 슬랙의 워크스페이스나 게임 채널과 비슷함.
  • 네임 스페이스 안에 룸 생성가능
  • 네임스페이스와 룸을 같이 사용하여 더 정교하게 메세지 송수신 제어가능

13.4.1 네임스페이스 사용

(1) name space

project.gateway.ts


import { 
  SubscribeMessage, 
  WebSocketGateway,
  WebSocketServer,

} from '@nestjs/websockets';
// socket io 임포트 
import { Server, Socket } from 'socket.io'

@WebSocketGateway({namespace:'project/chat'})
export class ProjectGateway {
  // 웹 소켓 서버 인ㅅ턴스 선언 
  @WebSocketServer() server : Server


  // message event 구독
  @SubscribeMessage('message')
  handleMessage(socket: Socket, data : any): void {
    // 접속한 클라이언트에게 멧지 전송
    // console.log("hi")
    this.server.emit('message', `User-${socket.id.substring(0,4)} : ${data}`,)
  }
}

13.4.2 닉네임 추가

(1) 웹페이지 진입시 입력한 닉네임을 사용하도록 변경

=> 향후 username 으로 채팅하도록 변경 필요

index.html

 <script>
        // socket.io 인스턴스 생성 
        const socket =io('http://localhost:3000/project/chat')

        const nickname = prompt('input your nickname')


        // 전송 버튼 클릭 시 입력된 글을 message 이벤트로 보냄 
        function sendMessage() {
            // message 창에 입력된 값 할당
            const message = $('#message').val()

            $('#chat').append(`<div> 나 : ${message}</div>`)

            // 채팅 소켓의 message 로 방출 
            socket.emit('message', {message, nickname})

            $('#message').val('') // 입력창 비우기
        }
        socket.on('connect', () => {
            console.log('connected')
        })

        socket.on('message',(message) => {
            $('#chat').append(`<div>${message}</div>`)
        })

    </script>

project.gateway.ts

// message event 구독
  @SubscribeMessage('message')
  handleMessage(socket: Socket, data : any): void {
    // 접속한 클라이언트에게 멧지 전송
    // this.server.emit('message', `User-${socket.id.substring(0,4)} : ${data}`,)
    const { message, nickname} =data
    socket.broadcast.emit('message',`${nickname}: ${message}`)
  }
}