NestJS + Websocket으로 채팅만들기 #1
최근 Websocket을 활용하여 사이드프로젝트를 진행한것이 있다.
추후에 오픈소스로 공개 할 예정이지만
kimpga 를 클론코딩하여 만든 https://hsct.io 이다.
Websocket Client를 활용하여 서비스 구성하는것은 너무 쉽고 재미있는 경험이었다.
하지만 아직 Server의 입장에서 Websocket을 다뤄 본 경험이 거의 전무하다 싶다.
빠르게 NestJS를 활용해서 채팅을 만들어 볼까 싶었다.
NestJS로 Websocket 서버 구축하기
만들고 난 지금 보면 너무 간단하지만, 처음 Docs를 보았을때 내가 생각하는 Websocket이 맞나 싶었다.
NestJS Docs에 Websocket 항목이 있는것을 알고 있었으므로 당연히 Websocket 서버일것이라고 생각하고 있었다.
하지만 막상 Docs 제목을 보니 Websocket Gateway라는것이다.
최근 MSA와 Serverless를 공부하고있다보니 Gateway라는 말이 참으로 반가우면서도 이상했다.
내가 생각하던 Websocket은 서버던 유저던 뚫려있던 Pipe로 Message를 Send하면 상대방의 onMessage 함수가 작동되는것이었는데, Gateway라 하면 bypass 해주는 입구역할을 하는것이 아닌가.
사실 Docs만 보고는 완전히 이해하기 어려웠다.
확실한것은 WebSocketGateway 라는 Decorator를 활용한 Class를 만들어 Value, Service , Provider 와 Contact 한다는것 같은데,
이렇게 확실하지 않을때는 예제를 보는것이 직빵이다.
NestJS는 정말 Docs도 너무 친절하고 예제도 너무 친절하다.
정말 한번 보고 이해를 싹 해버렸다.
import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '@nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'ws';
@WebSocketGateway(8080)
export class EventsGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage('events')
onEvent(client: any, data: any): Observable<WsResponse<number>> {
return from([1, 2, 3]).pipe(map(item => ({ event: 'events', data: item })));
}
}
(https://github.com/nestjs/nest/blob/master/sample/16-gateways-ws/src/events/events.gateway.ts)
- @WebSocketGateway(Port) : 해당 데코레이터를 통해 Socket Server를 세팅한다.
- @SubscribeMessage(이름) : event라는 유형의 message를 받게되면 onEvent 함수를 작동시킨다. return 을 통해 응답할 수 있다.
서버 세팅은 사실 이것과
main.ts
import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
app.useWebSocketAdapter(new WsAdapter(app));
이 어댑더 친구만 잘 넣어주면 서버세팅은 완료이다.
<html>
<head>
<script>
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = function() {
console.log('Connected');
socket.send(
JSON.stringify({
event: 'events',
data: 'test',
}),
);
socket.onmessage = function(data) {
console.log(data);
};
};
</script>
</head>
<body></body>
</html>
(https://github.com/nestjs/nest/blob/master/sample/16-gateways-ws/client/index.html)
클라이언트 부분이다.
정말 너무 심플하다.
new WebSocket() 을 통해 @WebSocketGateway(Port)에서 지정한 포트를 통해 연결을 할 수 있고,
send를 통해 통신할 수 있다.
send를 통해 통신할 때 string만 보낼 수 있으며 보낼 때 JSON 으로 stringify 한 object를 보내는데 이때 오브젝트에 들어가는것이 중요하다.
- event : @SubscribeMessage(이름) 서버에서 설정한 이름이 여기에 들어간다. 서버에서 @SubscribeMessage(이름) 를 여러개 설정하면 각각 다르게 작동시킬 수 있다.
- data : 말해뭐해 그냥 보내고자 하는 데이터이다. 너무 쉽다.
WebSocket을 활용하여 단순하게 채팅만 만들어 보려고 했지만 생각보다 더 시간을 들여 디테일하게 작업을 들어가도 될 것 같다고 판단하여, 채팅의 가닥을 잡았다.
해당 형태 구조의 채팅서버를 구축해보고자 마음먹었다.
원래는 Nginx 말고 MSA 형식으로 채팅서버를 구축 해 보려고 했는데, 지금 당장은 채팅서버만 만들 생각이기 때문에 일단은 위와 같은 구조로 구현하고 나중에 응용해보고자 한다.
채팅 서버 뿐만 아니라 Redis 가 들어가있다.
그 이유는 채팅방에 늦게 온 사람또한 기존 정보를 볼 수 있게 하기위해 Redis를 활용하기로 했다.
실 서비스라고 한다면, 주기억장치에 데이터를 저장하여 서버가 종료되었을 시 자료들이 휘발성으로 모두 사라지는 Redis 뒤에 Mysql이나 MongoDB 와 같이 데이터베이스가 뒷바침 되어야 안정적으로 운영되겠지만, 지금은 굳이 필요하지는 않을 것 같다.
지금 구상하는 채팅방은
- 채팅방을 생성할 수 있다.
- 채팅방 목록 페이지에서 본인의 닉네임을 설정하고, 변경은 목록페이지에서만 가능하다.
- 채팅방에 접속하면 이전 채팅글을 볼 수 있고, 100개단위로 이전 채팅글을 불러올 수 있다.
- 글자수 혹은 json size의 제한을 통해 바이너리형태의 직접적인 데이터 전송, xss 등의 보안부분도 추가해준다.
- 닉네임등록시 금지어를 설정하여 비속어 등을 제한한다.
- 사이트를 껐다 킬 경우에도 채팅방에 닉네임은 유지된다.
딱 요만큼만 만들어보자.
제작에 들어가려고 곰곰히 생각해보니 @SubscribeMessage(이름) 에 설정된 함수가 작동될 시
return 의 형태로 message를 전송해준다면 1:1 대화 정도밖에 진행 할 수가 없었다.
내가 구상한 채팅방에서는 유저가 채팅을 보내면 서버가 Broadcast의 형태로 모든 유저에게 해당 내용을 전송해주어 onMessage 함수가 작동될 것이고, 내가 원하는 형태의 대화 목록이 보여지는 것이다.
이러한 형태의 채팅을 구축하려면 유저가 처음에 접속하여 onConnect 되었을 때 해당 소켓에 대한 정보를 Array 혹은 Object 형태로 메모리에 저장해 주고, 접속해있는 어떤 유저가 메시지를 Send 했을 경우 메모리에 저장되어있는 소켓 목록들에게 한번에(일일이) send 를 해주면 된다.
다만 메모리를 인위적으로 컨트롤 해 주기 때문에 onDisconnect 시 메모리에 저장되어있는 소켓 목록에서 지워줘야한다.
삭제를 할 시 삭제 할 소켓 정보를 특정해야하는데, 이 때문에 소켓 Object에 ID를 부여해야한다. 그리고, onConnect 시 메모리에 Key, Value 형태의 Object로 저장을 하면 onDisconnect 발생시 삭제도 용이하다.
마침 NestJS Websocket에는 굉장히 좋은것이 있다.
👉 [NestJS Docs - Websocket - Lifecycle Hook]
이 훅을 사용하여 onConnect, onDisconnect 를 컨트롤 할 수 있다.
사용법은 간단하다.
@WebSocketGateway(5000)
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
client: Record<string, any>;
constructor() {
this.client = {};
}
@WebSocketServer()
server: Server;
public handleConnection(client): void {
console.log('hi');
client['id'] = String(Number(new Date()));
client['nickname'] = '낯선남자' + String(Number(new Date()));
this.client[client['id']] = client;
}
public handleDisconnect(client): void {
console.log('bye', client['id']);
delete this.client[client['id']];
}
@SubscribeMessage('message')
handleMessage(client: any, payload: any): void {
for (const [key, value] of Object.entries(this.client)) {
value.send(
JSON.stringify({
event: 'events',
data: { nickname: client['nickname'], message: payload },
}),
);
}
}
}
이렇게 implement를 통해 내가 원하는 Hook을 사용할 수 있다.
오늘 웹소켓 설치하며 다사다난했던 일만 없었으면 더 좋은 퀄리티의 코딩을 했겠지만,, 이부분은 개선이 필요하다.
이 모든 내용은 Github에 공개 해 두었다.
https://github.com/for2gles/realtime-chat
갑작스러운 도전이었지만, 너무 재미있었다.