상세 컨텐츠

본문 제목

NestJS를 활용한 실시간 채팅 앱 구축

Dev Type

by ai developer 2024. 3. 18. 22:04

본문

 

서버와 클라이언트 간 통신이 실시간으로 발생해야 하는 경우가 종종 있습니다.

이런 기능은 대표적으로 채팅 앱에 많이 사용되는데요.

NestJS에서 WebSocket을 활용해 실시간 채팅 앱을 만들어 원리를 확인해 봅니다.

실시간 API란 무엇일까요?

실시간 API는 실시간으로 데이터를 교환하는 클라이언트와 서버 간의 통신입니다.

일반적으로 WebSocket을 활용해 구현하는데 WebSocket은 클라이언트와 서버 간 실시간 통신을 가능하게 하는 일종의 프로토콜이라고 보면 됩니다.

WebSocket을 구현하기 위해 프레임워크를 사용할 수 있습니다.(socket.io, ws)

프로젝트 구성

프로젝트 구조를 이야기 하자면, 아래와 같습니다.

- 클라이언트: ReactJS Wep App

- 서버: NestJS Realtime App

 

우선 프로젝트 폴더부터 생성하고,

mkdir nest-chat-app

 

프로젝트 폴더에 진입 후 NestJS로 서버 폴더를 설치합니다.

nest new server

 

서버 폴더에 진입 후 NestJS에서 WebSocket 작업을 시작하기 위해 패키지를 설치합니다.

npm i --save @nestjs/websockets @nestjs/platform-socket.io

 

서버는 얼추 설치가 마무리 됐고, 다시 프로젝트 폴더로 나온 뒤 이제 React 프로젝트를 생성합니다.

npx create-react-app client

 

클라이언트 폴더로 진입 후 React에서도 필요한 패키지를 설치합니다.

npm i --save socket.io-client

 

이렇게 프로젝트 기본 구성을 마쳤습니다!

이제 본격적으로 서버를 만들어 볼까요🔥

서버 구성

NestJS에서 WebSocket을 관리하려면 Gateway를 사용해야 하고 이것을 이용하면 간단히 @WebSocketGateway() 데코레이터를 이용해 간단히 설정이 가능해집니다.

그럼 이제 ChatGateway를 생성해 볼까요!

nest generate gateway chat

 

이렇게 하면 자동으로 chat 폴더에 chat.gateway.ts, chat.gateway.spec.ts 파일들를 자동으로 생성해 줍니다.

 

그리고 아래와 같이 ChatGateway 클래스에 @WebSocketGateway() 데코레이터가 추가되고 handleMessage 기능을 통해 메시지를 구독할 수 있게 됩니다. 이렇게 되면 클라이언트로부터 데이터를 전달하는 동안 들어오는 메시지를 처리할 수 있게 됩니다.

import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';

 @WebSocketGateway()
 export class ChatGateway {
   @SubscribeMessage('message') // subscribe to message events
   handleMessage(client: any, payload: any): string {
     return 'Hello world!'; // return the text
   }
 }

 

handleMessage 기능은 2개의 파라미터가 있는데, client: 플랫폼 별 소켓 인스턴스, payload: 클라이언트로부터 수신된 데이터 이렇게 2개가 있습니다.

 

클라이언트 측에서 메시지를 보내려면 어떻게 해야 하나요? socket.io-client 패키지를 활용해 이것이 가능합니다. 또한 아래 두번째 줄과 같이 서버에서 보낸 결과 데이터를 다룰 수도 있습니다.

socket.emit('events', { name: 'Nest' });
socket.emit('events', { name: 'Nest' }, (data) => console.log(data));

 

단순히 서버에서 클라이언트로 데이터를 반환하는 예를 보았는데, 그것이 아닌 메시지를 subscribe 중인 모든 고객에게 메시지를 broadcast 해 보겠습니다.

import {
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  MessageBody,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';

import { AddMessageDto } from './dto/add-message.dto';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  private logger = new Logger('ChatGateway');

  @SubscribeMessage('chat') // subscribe to chat event messages
  handleMessage(@MessageBody() payload: AddMessageDto): AddMessageDto {
    this.logger.log(`Message received: ${payload.author} - ${payload.body}`);
    this.server.emit('chat', payload); // broadbast a message to all clients
    return payload; // return the same payload data
  }
}

 

위와 같이 데코레이터 @WebSocketServer()를 이용해 broadcast를 실행합니다.(chat을 구독중인 모두에게)

그리고 NestJS에서 제공하는 2개의 수명주기를 사용해 편리하게 소켓 연결 시기를 알 수 있습니다.

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  ...
  ...
 
  // it will be handled when a client connects to the server
  handleConnection(socket: Socket) {
    this.logger.log(`Socket connected: ${socket.id}`);
  }

  // it will be handled when a client disconnects from the server
  handleDisconnect(socket: Socket) {
    this.logger.log(`Socket disconnected: ${socket.id}`);
  }
}

 

위와 같이 OnGatewayConnection, OnGatewayDisconnect 2개에서 제공하는 handleConnection, handleDisconnect를 활용해 연결 시와 연결 끊김 시의 기능을 구현할 수 있습니다. 그리고 원래는 신뢰할 수 있는 주소에만 적용해야 하지만 테스트 중이니 cors를 모두 통과하도록 안전하지 않은 구성을 진행했습니다.

 

그럼 서버는 이제 마무리 했고 웹을 구현해 볼 차례입니다.

웹 구성

클라이언트에서 서버와 실시간으로 메시지를 보내고 받는 방법을 구현하기 위해 chat.tsx 파일에 만들 예정입니다.

import { useState, useEffect } from "react";
import { io } from "socket.io-client";

const SystemMessage = {
  id: 1,
  body: "Welcome to the Nest Chat app",
  author: "Bot",
};

// create a new socket instance with localhost URL
const socket = io('http://localhost:4000', { autoConnect: false });

export function Chat({ currentUser, onLogout }) {
  const [inputValue, setInputValue] = useState("");
  const [messages, setMessages] = useState([SystemMessage]);

  useEffect(() => {
    socket.connect(); // connect to socket

    socket.on("connect", () => { // fire when we have connection
      console.log("Socket connected");
    });

    socket.on("disconnect", () => { // fire when socked is disconnected
      console.log("Socket disconnected");
    });

    // listen chat event messages
    socket.on("chat", (newMessage) => {
      console.log("New message added", newMessage);
      setMessages((previousMessages) => [...previousMessages, newMessage]);
    });

    // remove all event listeners
    return () => {
      socket.off("connect");
      socket.off("disconnect");
      socket.off("chat");
    };
  }, []);

  const handleSendMessage = (e) => {
    if (e.key !== "Enter" || inputValue.trim().length === 0) return;

    // send a message to the server
    socket.emit("chat", { author: currentUser, body: inputValue.trim() });
    setInputValue("");
  };

  const handleLogout = () => {
    socket.disconnect(); // disconnect when we do logout
    onLogout();
  };

  return (
    <div className="chat">
      <div className="chat-header">
        <span>Nest Chat App</span>
        <button className="button" onClick={handleLogout}>
          Logout
        </button>
      </div>
      <div className="chat-message-list">
        {messages.map((message, idx) => (
          <div
            key={idx}
            className={`chat-message ${
              currentUser === message.author ? "outgoing" : ""
            }`}
          >
            <div className="chat-message-wrapper">
              <span className="chat-message-author">{message.author}</span>
              <div className="chat-message-bubble">
                <span className="chat-message-body">{message.body}</span>
              </div>
            </div>
          </div>
        ))}
      </div>
      <div className="chat-composer">
        <input
          className="chat-composer-input"
          placeholder="Type message here"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={handleSendMessage}
        />
      </div>
    </div>
  );
}

 

이제 웹 클라이언트를 모두 만들었으니 실제 화면을 공유해 봅니다.

NestJS - ReactJS의 데모 앱 시연

 

 

결론

모든 코드를 확인하려면 여기에서 확인하세요!

도움이 되었기를 바랍니다!

300x250

관련글 더보기

댓글 영역