Back/Node.js

[Nodejs] Express Typescript SocketIo

여러 챗봇과 실시간 채팅을 만들어야 했다.

기획이 굉장히 납득가지 않았지만, 기획자는 기획자의 일이 있는거니까 일단 이해하고 만들었다..

 

socket.model.ts

const socketConnect = (httpServer: any) => {
  const ioConnect = new socketIo.Server(httpServer, {
    cors: {
      origin: '*',
      methods: ['GET', 'POST'],
      credentials: true,
    },
  });

  const io = ioConnect.of('/socket/chat');
  return io;
};

export = socketConnect;

소켓을 여는 함수를 만든다.

 

 

app.ts

import express from 'express';
import { createServer } from 'http';

const app = express();
const httpServer = createServer(app);

const io = socketConnect(httpServer);

const socketCheckInterceptor: RequestHandler = async (req, res, next) => {
  const originUrl = req.originalUrl;
  const originUrlSplit = originUrl.split('/');
  if (특정 조건) res.locals.socketIo = io;
  if (특정 조건) res.locals.socketIo = io;

  return next();
};

app.use(socketCheckInterceptor);
app.use('/', routes);

httpServer.listen(process.env.PORT, () => {
  console.log(`🛡️ Server listening on port ${procee.env.PORT}`);
});

만든 함수를 app에서 불러오고 socket을 res.locals에 특정 상황일 때만 집어넣어주고 리턴시킨다.

 

 

socket.module.ts

const socketIoModule = {
  emit: async (io: any, room: string, emit: string, content: { [key: string]: any }) => {
    io.in(room).socketsJoin(room);
    io.to(room).emit(emit, content);
  },
  count: async (io: any, room: string) => {
    const count = io.engine.clientsCount;
    return count;
  },
  leave: async (io: any, room: string) => {
    return true;
  },
};

export = socketIoModule

쓸 것들만 대강 모듈화 시켜놓았다.

실제로 쓸 때는 이것보단 세분화 해놨다.

chat.controller.ts

const createBattleChatController: RequestHandler = async (req, res, next) => {
  try {
    const memberInfo: MemberInfo = res.locals.member;
    const { battleInviteCode, message, imageYn, socketId } = req.body;

    const result = await createBattleChatService(
      battleInviteCode,
      message,
      imageYn,
      memberInfo.member_idx,
      memberInfo.profile_img,
      memberInfo.member_name,
      res.locals.socketIo,
      socketId,
    );

    if (!result) res.locals.responseResult = { msg: 'noneInviteCode', status: 'err' };
    else res.locals.responseResult = { msg: 'success' };

    return next();
  } catch (err) {
    res.locals.errorContent = err;
    return errorGlobalFilter(req, res, next);
  }
};

app에서 받아온 io를 이용해 서비스로 넘겼다..

chat.service.ts

const chatService = {
  createChat: async (
    roomName: string,
    message: string,
    io: any,
    socketId: string,
  ) => {
    const sockets = await io.in(roomName).fetchSockets();
    if (sockets.length < 1) return false;
    await io.in(socketId).socketsJoin(roomName);
    await io.to(roomName).emit('emitName', content);
    await createChatMessageToDynamoDb(battleInviteCode, message, imageYn, memberIdx, 1);
    return true;
  }},

실제 프로덕트에선 이 안에서 분기처리가 어..엄청나게.. 많다.. 눈물없인 볼 수가 없다..

일단 만든 채팅은 aws dynamodb에 집어 넣었다..

그리고 전체 접속 중 클라이언트는 알 수 있어도, 특정 room에 접속 중인 클라이언트의 수를 파악하는게, 어떤 레퍼런스를 봐도 명쾌하게 떨어지는 것이 없었다..

공식문서를 뒤져도 안나왔다..

여튼 io.in(방이름).fetchSockets() 의 길이로 해당 방에 접속 중인 소켓id를 파악할 수가 있었다..

여기서 문제는 방을 나가도 소켓이 연결되어 있는 경우가 있었는데,


소켓을 끄거나 백그라운드로 가거나 등, 소켓을 활용하지 않을 때

 await io.in(socketId).socketsLeave(RoomName);


이렇게 방을 나가게 해주면 정확하게 파악할 수 있다.

사이드에서 몇번 써봤긴 했지만,

 

실프로덕트에서 챗봇이랑 채팅 알람 등 여러 상황에 맞게끔 구현해내는게 쉽지 않았다.

살려줘

 

Introduction | Socket.IO

What Socket.IO is

socket.io