Socket.io
웹 서비스를 개발하다보면 사용자끼리 1:1 혹은 다수의 인원끼리 소통하기 위해서 실시간 통신이 필요한 경우가 있다.
이 때 WebSocket을 활용하면 실시간 통신을 손쉽게 구현할 수 있는데, 이번 글에서는 WebSocket을 활용하는 socket.io 라이브러리를 Nest.js에서 활용하여 1:1 채팅방을 구현하는 방법을 소개한다.
1. Nest.js 프로젝트 생성 및 초기 설정
1-1. Nest.js 프로젝트 생성
npx @nestjs/cli new chat-app
cd chat-app
1-2. 필수 패키지 설치
- WebSocket 및 Socket.IO 관련 패키지 설치:
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
- JWT 인증 관련 패키지 설치:
npm install @nestjs/jwt passport-jwt
- TypeORM 및 MySQL 설정 (또는 다른 데이터베이스):
npm install @nestjs/typeorm typeorm mysql2
1-3 TypeORM 설정
src/app.module.ts 파일에서 파일에서 TypeORM 설정을 추가하여 데이터베이스 연결을 설정한다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'chat_app',
autoLoadEntities: true,
synchronize: true, // 개발용으로만 사용 (프로덕션에서는 마이그레이션 권장)
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
2. 엔티티 정의
사용자의 데이터를 정의하는 User 엔티티
채팅방의 데이터를 정의하는 ChatRoom 엔티티
메시지 데이터를 정의하는 Message 엔티티
위 3가지를 아래와 같이 작성한다.
2-1 User 엔티티
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: string;
@Column()
username: string;
@Column()
email: string;
}
2-2 ChatRoom 엔티티
import { Entity, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class ChatRoom {
@PrimaryGeneratedColumn()
id: number;
@ManyToMany(() => User)
@JoinTable()
participants: User[];
}
2-3 Message 엔티티
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { ChatRoom } from './chat-room.entity';
@Entity()
export class Message {
@PrimaryGeneratedColumn()
id: number;
@Column()
content: string;
@ManyToOne(() => User)
sender: BaseUser;
@ManyToOne(() => ChatRoom)
room: ChatRoom;
@Column({ default: false })
isRead: boolean;
}
3. 서비스 구현
채팅방의 기능을 동작시키기위해 아래의 3가지 기능을 구현해준다.
- 채팅방 찾기(이미 존재하는 채팅방을 리턴하고 존재하지 않을 경우 새로운 채팅방 생성 후 리턴)
- 메시지 전송
- 채팅 내역 조회(채팅방을 기준으로 메시지 데이터 전체 조회)
3-1 ChatService (대화방 및 메시지 관리)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ChatRoom } from './chat-room.entity';
import { Message } from './message.entity';
import { User } from './user.entity';
@Injectable()
export class ChatService {
constructor(
@InjectRepository(ChatRoom)
private chatRoomRepository: Repository<ChatRoom>,
@InjectRepository(Message)
private messageRepository: Repository<Message>,
) {}
// 채팅방 찾기
async findOrCreateRoom(user1: User, user2: User): Promise<ChatRoom> {
let room = await this.chatRoomRepository.findOne({
where: { participants: [user1, user2] },
});
if (!room) {
room = this.chatRoomRepository.create({ participants: [user1, user2] });
await this.chatRoomRepository.save(room);
}
return room;
}
// 메시지 전송
async sendMessage(sender: User, room: ChatRoom, content: string) {
const message = this.messageRepository.create({ sender, room, content });
await this.messageRepository.save(message);
return message;
}
// 채팅 내역 조회
async getMessages(room: ChatRoom): Promise<Message[]> {
return this.messageRepository.find({ where: { room } });
}
}
4. WebSocket Gateway 구현 (Socket.IO 사용)
GateWay는 클라이언트와 서버가 실시간으로 통신할 수 있게 하는 진입점으로 실시간 데이터 전송이 필요한 시스템에서 많이 사용된다. GateWay의 주요 역할은 아래와 같다.
GateWay의 역할
- 클라이언트 연결 관리: 클라이언트가 연결되면 이를 처리하고, 연결을 해제하거나 재연결할 때도 관리한다.
- 이벤트 처리: 클라이언트에서 특정 이벤트가 발생하면 이를 서버에서 받아 처리할 수 있다. 클라이언트에서 서버로 메시지를 보내면 서버에서 이를 받아 처리하거나, 서버가 클라이언트로 데이터를 보낼 수 있다.
- 실시간 데이터 전송: 클라이언트가 서버와 연결된 동안 실시간으로 데이터를 주고받을 수 있다.
Nest.js에서는 WebSocket 통신을 관리하기 위해 @WebSocketGateway() 데코레이터를 사용하여 Gateway 클래스를 정의한다. 이는 WebSocket 서버의 역할을 하며, 클라이언트와의 소켓 통신을 처리한다.
4-1 ChatGateway
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { ChatService } from './chat.service';
import { JwtService } from '@nestjs/jwt';
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(
private chatService: ChatService,
private jwtService: JwtService,
) {}
async handleConnection(socket: Socket) {
const token = socket.handshake.query.token as string;
const user = this.jwtService.verify(token);
if (!user) {
socket.disconnect();
return;
}
socket.data.user = user;
console.log(`User ${user.username} connected`);
}
async handleDisconnect(socket: Socket) {
const user = socket.data.user;
console.log(`User ${user.username} disconnected`);
}
@SubscribeMessage('joinRoom')
async handleJoinRoom(
@ConnectedSocket() socket: Socket,
@MessageBody() roomId: number,
) {
const user = socket.data.user;
const room = await this.chatService.findRoomById(roomId);
if (!room) {
socket.emit('error', 'Room not found');
return;
}
socket.join(roomId.toString());
console.log(`User ${user.username} joined room ${roomId}`);
}
@SubscribeMessage('sendMessage')
async handleMessage(
@ConnectedSocket() socket: Socket,
@MessageBody() { roomId, content }: { roomId: number; content: string },
) {
const user = socket.data.user;
const room = await this.chatService.findRoomById(roomId);
if (!room) {
socket.emit('error', 'Room not found');
return;
}
const message = await this.chatService.sendMessage(user, room, content);
this.server.to(roomId.toString()).emit('newMessage', {
sender: user.username,
content: message.content,
});
}
}
'Backend > Nest.js' 카테고리의 다른 글
[Nest.js] socket.io 중복 연결 문제 해결 (4) | 2024.10.09 |
---|