Skip to main content

NestJS 실시간 채팅 앱 구현

· 5 min read

image

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

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

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의 데모 앱 시연​

git

결론​

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

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