개인 프로젝트이자 단체 프로젝트
wemakers에 채팅방을 구현해보자
app에 socketAdapter를 입혀주는것으로 socket의 초기 세팅을 시작한다
... 생략
app.useWebSocketAdapter(new IoAdapter(app));
await app.listen(configService.get('PORT'));
... 생략
gateway 생성과 모듈 설치는 NestJs 공식홈페이지를 참고했다
- yarn add --save @nestjs/websockets @nestjs/platform-socket.io
- yarn nest g gateway ${게이트웨이 이름}
본인은 현재 프로젝트에서 prisma orm을 사용하고 있다
따라서 prisma 사용하지 않는다면 prisma 코드는 제외하고 참고하면 된다
@WebSocketGateway(8001, namespace: 'websocket' })
export class EventsGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
prisma = new PrismaClient();
constructor(
private user: UsersRepository,
private socketRepository: SocketRepository
) {}
@WebSocketServer()
server: Server;
logger = new Logger('GateWay');
async afterInit() {
try{
await this.socketRepository.deleteNobodyRoom()
}catch(err){
throw new HttpException(err.message, 500)
}
this.logger.debug('웹소켓 서버 초기화 ✅');
}
async handleDisconnect(client: Socket) {
const numId = Number(client.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(numId);
this.logger.debug(`${nickName} is discsonnected...`);
}
async handleConnection(client: Socket) {
const numId = Number(client.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(numId);
this.server.emit('msgToClient', {
nickName,
text: `${nickName} is connect!`,
});
}
@SubscribeMessage('msgToServer')
async handleMessage(
@ConnectedSocket() socket: Socket,
@MessageBody() { roomName, message }: MessagePayload,
) {
const chatUserId = Number(socket.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
socket.broadcast
.to(roomName)
.emit('msgToReciver', { nickName, message });
this.logger.debug('message 전송 완료')
return { nickName , message, };
}
@SubscribeMessage('create-room')
async handleCreateRoom(
@ConnectedSocket() socket: Socket,
@MessageBody() roomName: string,
) {
const chatUserId = Number(socket.handshake.query.id);
const invitedUserId = Number(socket.handshake.query.inviteId)
try {
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
const { nickName: invitedUserNickname } = await this.user.findUserByIdOrWhere(invitedUserId);
const {roomInfo} = await this.socketRepository.detailRoomInfo({accountId: chatUserId, invitedUserId: invitedUserId})
if (roomInfo) {
throw new HttpException(exceptionMessagesSocket.THIS_ROOM_ALREADY_EXISTS, 400)
}
await this.socketRepository.createRoomWithUsers({
roomName,
accountId: chatUserId,
invitedUserId
})
socket.join(roomName)
this.server.emit('createRoom', `${nickName}님이 ${invitedUserNickname}을 초대하였습니다`);
this.logger.debug(`${nickName} create ${roomName} room`);
} catch(err){
throw new HttpException(err.message, 400)
}
}
@SubscribeMessage('join-room')
async handleJoinRoom(
@ConnectedSocket() socket: Socket,
@MessageBody() roomName: string,
) {
const chatUserId = Number(socket.handshake.query.id);
try{
const exRoom = await this.socketRepository.chatRoomWithAccount({
roomName,
accountId: chatUserId
})
if(!exRoom){
throw new HttpException(exceptionMessagesSocket.THIS_ROOM_DOES_NOT_EXISTS, 400)
}
}catch(err){
throw new HttpException(err.message, 400)
}
socket.join(roomName);
this.server.to(roomName).emit('joinMessage', `$${roomName}에 입장햇습니다`);
}
@SubscribeMessage('leave-room')
async leaveRoom(
roomName: string,
@ConnectedSocket() socket: Socket,
) {
try{
const chatUserId = Number(socket.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
const { roomInfo } = await this.socketRepository.chatRoomWithAccount({
roomName,
accountId: chatUserId
})
if(!roomInfo){
throw new HttpException(exceptionMessagesSocket.THIS_ROOM_DOES_NOT_EXISTS, 400)
}
socket.leave(roomName)
this.socketRepository.disconnectSocketWithRoom({
roomId: roomInfo.id,
accountId: chatUserId
})
this.server.to(roomName).emit('leaveRoomMessage', `${nickName}님이 ${roomName}에서 퇴장하셨습니다`)
}catch(err){
throw new HttpException(err.message, 400)
}
}
진짜 진행하고 있는 프로젝트의 코드를 가져오다보니 코드가 굉장히 길다..
위에서부터 천천히 하나하나 뜯어보자
@WebSocketGateway(8001, namespace: 'websocket' })
export class EventsGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
@WebSocketGateway(PORT, namespace: 'websocket' })
OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
@WebSocketServer()
server: Server;
logger = new Logger('GateWay');
async afterInit() {
try{
await this.socketRepository.deleteNobodyRoom()
}catch(err){
throw new HttpException(err.message, 500)
}
this.logger.debug('웹소켓 서버 초기화 ✅');
}
// sokcet repository
async deleteNobodyRoom(){
const isExistsRoom = await this.prisma.chatRoom.findMany({
include: {
ChatRoomInfo: true
}
})
isExistsRoom.map(async (roomInfo) => {
if(roomInfo.ChatRoomInfo.length === 0){
await this.prisma.chatRoom.deleteMany({
where: {
id: roomInfo.id
}
})
}
})
}
init 부분은 서버와 동시에 실행된다 본인은 해당 동작이 실행되며
아무도 없는 Room (미리 설명하자면 채팅방 같은 개념이다)을 삭제하기로 했다
따라서 해당 코드를 사용했는데 prisma에서 분명히 connection 로우가
존재하는지 안 하는지 알 수 있는 코드가 있을 것 같은데 아직 못 찾아서
이런식으로 코드를 짜 놨지만 수정해야 될 것 같다 ..
외에는 딱히 설명할 코드가 없기 때문에 넘어가겠다
async handleConnection(client: Socket) {
const numId = Number(client.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(numId);
this.server.emit('msgToClient', {
nickName,
text: `${nickName} is connect!`,
});
}
socket 서버에 연결된 경우이다 나는 해당 유저가 누군지 알고 싶었기 때문에
유저의 닉네임을 찾아주었다

exampleListner로 data를 넘기겠다는 의미이다 postma에서 connection 해보면 이런 데이터가 넘어온다

async handleDisconnect(client: Socket) {
const numId = Number(client.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(numId);
this.logger.debug(`${nickName} is discsonnected...`);
}
connection과 크게 다르지 않기 때문에 참고하면 될 것 같다
@SubscribeMessage('list-room')
async listRoom(
@ConnectedSocket() socket: Socket,
){
const chatUserId = Number(socket.handshake.query.id);
const roomInfo = await this.socketRepository.listAccountRoom(chatUserId)
return roomInfo;
}
// socket repository
async listAccountRoom(accountId: number): Promise<chatRoomInfoWithConnection[]>{
const rooms = await this.prisma.chatUserInfo.findMany({
where: {
accountId: accountId
}
})
const response = rooms.map((roomInfo) => {
return {
roomInfo
}
})
return response
}
Room을 보여주는것마찬가지로 postman으로 조회해보자
현재 내 db에 3번 id를 갖은 user는 어떠한 채팅방에도 속해있지 않다
따라서 빈배열이 나타나고 있지만 입장한 경우에는 해당 방이 나타날것이다

코드 순서상 create Room이 먼저 오면 안 되지만 다른 API의 이해도를 높이고자
create room API를 먼저 땡겨서 설명
@SubscribeMessage('create-room')
async handleCreateRoom(
@ConnectedSocket() socket: Socket,
@MessageBody() roomName: string,
) {
const chatUserId = Number(socket.handshake.query.id);
const invitedUserId = Number(socket.handshake.query.inviteId)
try {
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
const { nickName: invitedUserNickname } = await this.user.findUserByIdOrWhere(invitedUserId);
const {roomInfo} = await this.socketRepository.detailRoomInfo({accountId: chatUserId, invitedUserId: invitedUserId})
if (roomInfo) {
throw new HttpException(exceptionMessagesSocket.THIS_ROOM_ALREADY_EXISTS, 400)
}
await this.socketRepository.createRoomWithUsers({
roomName,
accountId: chatUserId,
invitedUserId
})
socket.join(roomName)
this.server.emit('createRoom', `${nickName}님이 ${invitedUserNickname}을 초대하였습니다`);
this.logger.debug(`${nickName} create ${roomName} room`);
} catch(err){
throw new HttpException(err.message, 400)
}
}
// socket repository
async detailRoomInfo({ accountId, invitedUserId }): Promise<chatRoomInfoWithConnection>{
const roomInfo = await this.prisma.chatUserInfo.findFirst({
where: {
AND: [
{accountId: accountId}, { accountId: invitedUserId}
]
}
})
return { roomInfo }
}
async createRoomWithUsers({roomName, accountId, invitedUserId}){
const { id:chatRoomId } = await this.prisma.chatRoom.create({
data: {
roomName,
ChatRoomInfo: {
create: [
{
isAccept: true,
Account: {
connect:
{ id: accountId }
},
},
],
},
},
})
대부분 socket에 대한 코드는 설명을 했기 때문에
해당 부분은 API의 대한 소개를 하겠다 이 API만 확인하면
나머지 부분의 이해도 확실히 높아질것이다
- chatUserId => 현재 소켓 서버의 접속한 유저의 id
- invitedUserId => 채팅방에 초대할 유저의 id
- roomInfo => 나와 상대방이 함께 존재하는 채팅방이 있는경우 exception
3-1. 진행중인 프로젝트는 나와 상대방이 존재하는 채팅방은 한개만 존재할 수 있다 (detailRoomInfo 함수)- 상대방과 나를 외래키로 갖고있는
Room생성 (createRoomWithUsers 함수)- socket.join(roomName)
5-1. 해당 코드로 room으로 join을 시킬수 있음 (채팅방에 입장)- createRoom이라는 listner로 해당 메세지를 발송
- debug 로깅
해당 api를 postman으로 확인해보자 (방의 이름은 text로 전송하면 된다)

DB를 확인해보자

Room과 User 모두 정상적으로 들어간것을 확인할 수 있다
아까 진행했던 roomList를 한번 받아보자

성공적으로 room list에 방금 생성한 방이 존재하는것을 알 수 있다
@SubscribeMessage('msgToServer')
async handleMessage(
@ConnectedSocket() socket: Socket,
@MessageBody() { roomName, message }: MessagePayload,
) {
const chatUserId = Number(socket.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
socket.broadcast
.to(roomName)
.emit('msgToReciver', { nickName, message });
this.logger.debug('message 전송 완료')
return { nickName , message, };
}
room을 생성했다면 해당 방에 메세지를 전송해보자
- msgToServer ( EventName)
- socket.broadcast.to(방이름).emit('listenrName', data)
2-1. 해당 코드로 원하는 방에 원하는 데이터를 전송할 수 있다
그럼 postman으로 확인해보자

JSON형식으로 전송하여 roomName과 message을 작성하여 전송했다
정상적으로 값을 받아오는것을 볼 수 있다
@SubscribeMessage('leave-room')
async leaveRoom(
roomName: string,
@ConnectedSocket() socket: Socket,
) {
try{
const chatUserId = Number(socket.handshake.query.id);
const { nickName } = await this.user.findUserByIdOrWhere(chatUserId);
const { roomInfo } = await this.socketRepository.chatRoomWithAccount({
roomName,
accountId: chatUserId
})
if(!roomInfo){
throw new HttpException(exceptionMessagesSocket.THIS_ROOM_DOES_NOT_EXISTS, 400)
}
socket.leave(roomName)
this.socketRepository.disconnectSocketWithRoom({
roomId: roomInfo.id,
accountId: chatUserId
})
this.server.to(roomName).emit('leaveRoomMessage', `${nickName}님이 ${roomName}에서 퇴장하셨습니다`)
}catch(err){
throw new HttpException(err.message, 400)
}
}
// socket repository
async chatRoomWithAccount({roomName, accountId}): Promise<chatRoomInfo>{
const roomInfo = await this.prisma.chatRoom.findFirst({
where: {
roomName,
ChatRoomInfo: {
some:
{ accountId },
},
},
})
return { roomInfo }
async disconnectSocketWithRoom({roomId, accountId}): Promise<void>{
await this.prisma.chatUserInfo.deleteMany({
where: { chatRoomId: roomId, accountId: accountId },
});
}
1.leave-room(eventName)
2. roomInfo : 자신이 속해있는 room인지 조회 (chatRoomWithAccount)
3. socket.leave(roomName)
3-1. 해당 코드로 원하는 방에서 socket을 퇴장시킬수 있음
4. DB에 해당 room에서 user delete (disconnectSocketWithRoom)
5. leaveRoomMessage에서 해당 방에 채팅방 퇴장 알림
해당 로직을 postman에서 확인해보자

아쉽게도 본인은 방에서 제외가 됐기 때문에
this.server.to(roomName).emit('leaveRoomMessage', `${nickName}님이 ${roomName}에서 퇴장하셨습니다`)
의 메시지를 보지는 못한다 하지만 그렇다면 db를 조회해보자

성공적으로 room에서 user 1번의 데이터가 삭제된것을 볼 수 있다
블로그를 쓰다 보니까 코드에서 try catch 부분도 이상하고
중간중간 안 감싸 준 애들도 보인다.. 뭐 socket에 대한 포스팅이니까
거기 까지 점검은 안 해줘도 되겠지만 얼른 돌아가서 리팩토링을 좀 해야 될 것 같다