본문 바로가기
Sesac 웹 풀스택[새싹X코딩온]/회고록 및 TIL

Socket.io로 채팅방 구현하기(feat. Node.js, Sequelize, React.js)

by 이쟝 2023. 6. 25.
Websocket과 Socket.io의 개념에 대해서 정리
 

Websocket 과 Socket.io

Websocket이 태어난 배경 WebSocket은 HTTP와는 구분되는 별도의 통신 프로토콜이지만 HTTP를 기반으로 동작한다. HTTP는 단방향 통신을 지원하는 프로토콜이므로 서버에서 클라이언트로 데이터를 보내

everysmallstep.tistory.com

Node.js 환경에서 실시간 1:1 채팅을 구현하기 위해서 socket.io를 사용했다.

emit과 on으로만 구현하였고, 수정이나 삭제는 구현하지 않았다. 

 

DB관계
채팅 목록

채팅 시작 전

  • 채팅 목록은 room 테이블과 연결되어 있어서 채팅 페이지에 들어가게 되면 axios 요청을 통해 mysql(DB)에서 데이터를 가져온다.

1. 채팅 시작 시

  • 프론트에서 채팅에 들어온 유저와 방 번호를 emit(서버에 요청)한다. 만약 이 때 채팅했던 데이터가 있다면 axios요청을 통해 DB에서 데이터를 가져온다. 
  • 백에서는 받아서 접속한 유저, 방 번호를 설정한다.
  // front
  
  // 채팅 목록에서 채팅방 클릭 시 DB에서 chat 가져오기
  useEffect(() => {
    axios.get('chat/getData', { params: { id: chatData.id } }).then((res) => {
      res.data.forEach((el, idx) => {
        if (el.userId === user) {
          chat.current.insertAdjacentHTML(
            'beforeend',
            `<div class='${styles.myChat} ${styles[categoryType]}'>` +
              `<span>${el.time}</span>` +
              `<div>` +
              `${el.msg}` +
              '</div>' +
              '</div>'
          );
        } else {
          chat.current.insertAdjacentHTML(
            'beforeend',
            `<div class=` +
              `${styles.otherChat}>` +
              `${el.time}` +
              `<div>` +
              `${el.msg}` +
              '</div>'
          );
        }
      });
    });
  }, []);
  
  // socket으로 연결
  let socket = io.connect('https://bandari.store:5000');
  // 현재 채팅에 들어온 유저와 방 번호
  socket.emit('loginUser', { user: user, roomId: chatData.id });
  
  // back
  // 접속한 유저, 방 번호, 방
    let loginUser = '';
    let roomId = '';
    let rooms = [];
    const io = require('socket.io')(https, {
      cors: {
        orgin: ['https://bandari.store'],
        credentials: true,
      },
    });

    io.on('connection', (socket) => {
      // 방 입장 시 로그인 한 유저와 방이름
      socket.on('loginUser', (data) => {
        loginUser = data.user;
        roomId = data.roomId;

        // rooms에 있으면 roomId 추가 xx
        if (!rooms.includes(roomId)) {
          rooms.push(roomId);
        }
        socket.join(roomId);
      });

2. 채팅 전송 후 표시

  • 프론트에서 데이터를 모아서 서버에 emit(요청)하고, axios 요청으로 chat 테이블에 채팅 내용 저장(post 요청)
  • 백에서는 sendMsg로 받고 다시 백에서 newMsg 데이터 요청해서 채팅 화면 표시(current.insertAdjacentHTML로 실시간 채팅방 구현
  // front
  /*전송이벤트 */
  const btnSend = () => {
    const inputText = inputRef.current.value;
    // 시간
    let hourMin = new Date().toTimeString().split(' ')[0];
    hourMin = hourMin.substring(0, hourMin.lastIndexOf(':'));
    const datas = {
      msg: inputText,
      time: hourMin,
      userId: user,
      roomId: chatData.id,
    };
    socket.emit('sendMsg', datas);
    axios.post('chat/insert', datas);
    inputRef.current.value = '';
    resetScroll();
  };
  
    socket.on('newMsg', (data) => {
    if (user === data.userId) {
      chat.current.insertAdjacentHTML(
        'beforeend',
        `<div class='${styles.myChat} ${styles[categoryType]}'>` +
          `<span>${data.time}</span>` +
          `<div>` +
          `${data.msg}` +
          '</div>' +
          '</div>'
      );
    } else {
      chat.current.insertAdjacentHTML(
        'beforeend',
        `<div class=` +
          `${styles.otherChat}>` +
          `${data.time}` +
          `<div>` +
          `${data.msg}` +
          '</div>'
      );
    }
  
  // back
  // 메시지 데이터
  socket.on('sendMsg', (data) => {
    io.to(roomId).emit('newMsg', data);
  });
  // axios post 요청
  exports.postInsert = async (req, res) => {
    const result = await chat.create(req.body);
    res.send(true);
  };

3. 채팅 나가기 및 채팅 종료하기

  • 채팅 나가기는 x버튼을 클릭해 현재 창에서 채팅 나가기이고, 종료하기는 채팅방의 모든 내용까지 같이 삭제된다.
  • 삭제되면 현재 연결되어 있는 rooms 중에서 연결이 끊긴 방만 나가게 된다
  // front
  /**채팅 방만 나가기 */
  const onClickClose = () => {
    chatRef.current.classList.add(`${styles.transparent}`);
    socket.disconnect();
    setSelectChat(false);
  };
  /** 채팅 종료 버튼 이벤트 : 채팅 삭제 */
  const onClickExit = () => {
    if (window.confirm(
        '채팅을 종료하시겠습니까? 채팅방의 모든 내용이 삭제됩니다.')
    ) {
      axios.delete('room/delete', { data: { id: chatData.id } }).then((res) => {
        if (res.data.result === 1) alert('성공적으로 채팅방에서 나가졌습니다!');
        else alert('이미 채팅방에서 나가졌습니다.');
        socket.disconnect();
        onClickClose();
        window.location.reload();
      });
    }
  
  // back
  // x버튼으로 채팅방 나가기
  socket.on('disconnect', () => {
    rooms.forEach((el, idx) => {
      if (el == roomId) {
        rooms.splice(idx, 1);
      }
    });
    console.log(`${loginUser}가 ${roomId}방을 나갔습니다.`);
    console.log('delete 후의 rooms', rooms);
  });

Server 쪽 채팅 전체 코드

// 접속한 유저, 방 번호, 방
let loginUser = '';
let roomId = '';
let rooms = [];
const io = require('socket.io')(https, {
  cors: {
    orgin: ['https://bandari.store'],
    credentials: true,
  },
});

io.on('connection', (socket) => {
  // 방 입장 시 로그인 한 유저와 방이름
  socket.on('loginUser', (data) => {
    loginUser = data.user;
    roomId = data.roomId;

    // rooms에 있으면 roomId 추가 xx
    if (!rooms.includes(roomId)) {
      rooms.push(roomId);
    }
    socket.join(roomId);
  });

  // 메시지 데이터
  socket.on('sendMsg', (data) => {
    io.to(roomId).emit('newMsg', data);
  });
  // x버튼으로 채팅방 나가기
  socket.on('disconnect', () => {
    rooms.forEach((el, idx) => {
      if (el == roomId) {
        rooms.splice(idx, 1);
      }
    });
    console.log(`${loginUser}가 ${roomId}방을 나갔습니다.`);
    console.log('delete 후의 rooms', rooms);
  });
});

4. 판매완료 시

  • 구매자던 판매자던 판매 완료 버튼을 누르게 되면 판매 테이블(supplies)의 deal 컬럼이 false로 바뀌게 되면서 sold out로 변경되며 다른 유저는 연락할 수 없다.(판매 상세 페이지에서 채팅 버튼 비활성화)


느낀점 🤔

  • 생각보다 socket.io에 있는 다양한 기능들을 써보지 않았어서, 그렇게 백쪽에서는 코드가 길지 않았던 것 같다. 
  • socket.io의 emit과 on으로만으로도 실시간 채팅을 구현할 수 있어서 효율적이었던 것 같다. 
  • socket.io를 사용해 채팅 DB와 실시간 소통을 구현하면서 상대방과 채팅을 할 때마다 DB(Mysql)에 내용을 저장하고, 채팅방에 나갔다가 다시 들어올 때 DB에서 데이터를 다시 가져오게 된다. 그래서 만약 사용자가 많아지게 되면 서버에 부하가 너무 많이 가니까 고민했었다.
    • 고민했었던 것들 
      • 유저들이 나눴던 채팅 데이터들을 임시에 객체에 저장해놨다가, 한 번에 Insert 하기
      • select로 채팅 데이터를 조회할 때 채팅방 ID를 기준으로 chat 테이블을 파티션을 하기
      • 채팅 내용을 DB에 저장하지 않고, 중요한 것은 물품 거래이니까 일정 테이블을 따로 만들어서 그 일정만 저장해서 채팅내용은 휘발성으로 날리기