ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node.js] 웹 소켓으로 실시간 랜덤 채팅 구현 중 메시지 중복 버그 해결과정 (WebSocket Random Chat - clients rendering duplicate message)
    Backend 2019. 10. 5. 23:18

    정말 오랜만에 블로그 글을 작성하는 것 같다.

     

    바닐라코딩에서 서버쪽 공부를 시작하면서 할 것도 엄청 많고 과제에 허덕이다보니 도저히 블로그 글을 쓸 여유가 안생겼었다.

    이번에 웹소켓을 이용해서 랜덤 채팅을 구현했는데 처음에 이런 저런 버그들로 엄청 고생했어서 만들고 나니까 너무 뿌듯하다ㅜㅜ

     

     

     

     

    실시간으로 소통하는 채팅을 만들기 위해 웹 소켓(WebSocket)을 사용하여 구현하였다.

     

    MDN 문서를 보면 웹 소켓은 클라이언트(브라우저)와 서버 사이에서 상호 작용 가능한 통신 세션을 설정할 수 있게 하는 기술이라고 되어있다. 그래서 개발자는 웹 소켓 API를 통해 서버로 메시지를 보내고 서버의 응답을 위해 서버를 폴링하지 않고도 이벤트 중심 응답을 받는 것이 가능하다고 되어있다.

     

    여기서 폴링(Poling)이란 일정한 시간마다 클라이언트가 서버로 동기적으로 요청을 하여 사용 가능한 정보가 있는지 알아내는 것을 말한다. 그 요청은 주기적인 간격으로 이루어지며 클라이언트는 정보가 있든 없든 항상 응답을 받는다. 그러니까 즉 클라이언트에서 항상 주기적인 간격으로 서버에 "나한테 줄 새로운 정보 있니?"하고 물어보고 서버는 클라이언트에게 줄 정보가 있으면 정보를 전송하고 정보가 없으면 부정적인 응답을 보내는 것을 말한다.

     

    그러니까 종합해보면 웹 소켓은 서버에 메시지를 보내면 서버에 폴링을 하지 않아도 서버로부터 이벤트 중심의 응답을 받는 것이 가능한 기술이다.

     

    Web Socket을 사용하는 여러 가지 라이브러리나 툴들이 있는데 나는 Socket.IO를 사용하여 구현하였다.

    서버는 express에 socket.io를 설치하여 구축했고 클라이언트는 react에 socket.io-client를 설치해서 구축했다.

     

     

     

    하여튼 서론이 길었는데 오늘의 포스팅 주제는 구현하는 법에 대한 것이 아니라

    구현하면서 엄청 고생했던 버그와 그 버그를 어떻게 고쳤는지에 대한 것이다.

     

     


    Problem

    [ 채팅 메시지를 전송할 때마다 중복 메시지가 출력되는 현상 ]

     

    그러니까 맨 처음 채팅창에 메시지를 입력하고 전송하면 화면에 똑같은 메시지가 2개가 출력되는 것이다.

    처음엔 2개, 그 다음번엔 4개, 그 다음은 8개, ... 나중에는 나는 분명히 메시지를 하나만 보냈는데

    똑같은 메시지가 화면에 30개씩 출력되는 현상이 발생했다.

     

    stackoverflow에서도 엄청 검색하고 내 코드를 암만 봐도 도저히 뭐가 문제인지 모르겠어서

    일단 이 현상이 서버에서 잘못되서 일어나는 문제인지, 클라이언트 쪽의 문제인지 살펴보았다.

     

    const handleTextSending = (text, socketId) => {
      if (!text.trim()) {
        return;
      }
      socket.emit('sendTextMessage', text, socketId);
    };

     

    우선 채팅창의 form 태그에 위와 같은 함수가 submit이벤트로 등록되어있었다.

    그래서 채팅 내용과 state에 저장된 socket id를 받아서 server쪽에 'sendTextMessage' 이벤트를 emit한다.

     

     

     

    socket.on('sendTextMessage', (text, socketId) => {
      const roomKey = totalRoomList[socket.id];
      const newChat = {
        id: socketId,
        text
      };
    
      io.sockets.in(roomKey).emit('sendTextMessage', {
        chat: newChat
      });
    });

     

    그러면 서버에서 'sendTextMessage'이벤트를 수신하여

    채팅 방에 들어와있는 모든 클라이언트에게 채팅 메시지를 전달해준다.

     

    이 부분에서 console.log()로 출력해보니

    내가 메시지를 보내는 개수만큼 정확히 출력되었다.

    그러니까 서버쪽에는 아무 문제가 없었다.

    클라이언트1이 1개의 메시지를 보내면 정확히 하나의 메시지를 수신하여

    클라이언트1과 클라이언트2에 매시지를 전송해주었다.

     

    그러니까 클라이언트에서 메시지를 받아서 렌더하는 과정에서 문제가 발생했다는 뜻이다.

     

     

     

    socket.on('sendTextMessage', ({ chat }) => {
      dispatch(action.sendNewTextSuccess(chat));
    });

     

    그리하여 다시 클라이언트 쪽 코드를 살펴보았다.

    리액트 리덕스를 사용하여 container에서

    'sendTextMessage' 이벤트를 수신하는 함수를 만들어

    App 컴포넌트에 prop으로 전달한다.

     

    sendTextMessage이벤트를 등록하는 함수를 보면

    sendNewTextSuccess 액션 dispatch되고 있다.

     

    그래서 채팅 메시지를 보낼 때마다 sendNewTextSuccess 액션이 몇 번 발생하는지 살펴보았더니

    역시나 문제는 여기였다. 액션이 2번, 4번, 8번, ... 계속 중복된 액션이 여러 번 발생하였다.

     

    스택오버플로우를 계속 검색해서 알아낸 내 코드의 문제점은

    바로 socket.on('sendTextMessge');가 어디서 어떻게 실행되고 있느냐와 관련된 것이었다.

     

     

     

    useEffect(() => {
      handleTextReceiving();
    }, [ textSending.chats, handleTextReceiving ]);
    

     

     

    그러니까 저 버그가 발생했을 무렵의 나의 코드를 보면 

    socket.on('sendTextMessge');이 handleTextReceiving이라는 이름의 함수로 

    App 컴포넌트에서 ChatRoom 컴포넌트로 전달되었는데

    handleTextReceiving함수는 chats 배열이 업데이트될 때마다 계속해서 다시 실행된다.

     

    그러니까 이미 client쪽 socket에 'sendTextMessage'라는 이벤트 리스너가 등록되었는데

    배열이 업데이트될 때마다 계속해서 추가로 리스너가 등록되는 것이다.

     

    그렇게 되면 그 리스너들이 전부 서버로부터 데이터를 받아서 렌더해주니까

    중복된 데이터가 렌더되었던 것이다... 하하ㅏㅏ...ㅜㅜ...

     

     

     

    How to solve

    [ 이벤트 리스너가 중복되어 등록되지 않도록 한다 ]

     

    useEffect(() => {
      socket.on('sendTextMessage', ({ chat }) => {
        dispatch(action.sendNewTextSuccess(chat));
      });
    }, []);
    

     

    해결하는 방법은 매우 매우 간단한데...

    같은 이벤트 리스너는 딱 한 번만 등록하도록 만들어주기만 하면 된다!

     

    처음에는 off()함수로 useEffect()내의 return문에

    실행한 이벤트 리스너를 제거하는 함수를 넣었다.

     

    그러면 이벤트를 등록하고 바로 제거되는 것이 반복되니 중복 문제가 일단은 해결된다.

    그런데 어쨌든 이벤트 등록을 딱 한 번만 등록하는 것이

    근본적인 해결책이므로 로직을 전반적으로 약간씩 수정하였다.

     

    즉, socket.on()으로 이벤트를 등록하는 부분과 chat 배열을 업데이트하는 로직을 분리하여

    sendTextMessage 이벤트를 등록하는 함수는 useEffect 두 번째 인자로 빈 배열을 주어서 딱 한 번만 실행되도록 만들었다.

     

     

     

     

    두 번째 방법은 App 컴포넌트에 굳이 prop으로 전달하지 않고

    App 컨테이너의 mapDispatchToProps 함수 내에서 딱 한 번만 실행하는 것이다.

     

    const subscribeSocket = (dispatch) => {
      socket.on('initialConnect', (data) => {
        dispatch(action.connectChatSuccess(data));
      });
    
      socket.on('completeMatch', (roomData) => {
        if (!roomData.matched) {
          dispatch(action.matchPartnerPending());
        } else {
          dispatch(action.matchPartnerSuccess(roomData));
        }
      });
    
      socket.on('partnerDisconnection', () => {
        socket.emit('leaveRoom');
        dispatch(action.clearChatTexts());
        dispatch(action.disconnectChatSuccess());
      });
    
      socket.on('startTyping', () => {
        dispatch(action.startTyping());
      });
    
      socket.on('stopTyping', () => {
        dispatch(action.stopTyping());
      });
    
      socket.on('connect_error', (err) => {
        dispatch(action.connectChatFailure(err));
      });
    
      socket.on('error', (err) => {
        dispatch(action.connectChatFailure(err));
      });
    
      socket.on('sendTextMessage', ({ chat }) => {
        dispatch(action.sendNewTextSuccess(chat));
      });
    
      socket.on('disconnect', () => {
        dispatch(action.connectChatFailure());
      });
    };

     

    const mapDispatchToProps = dispatch => {
      subscribeSocket(dispatch);
    
      return {
      };
    };

     

    이런 식으로 이벤트 등록하는 함수를 모두 모아서 mapDispatchToProps내에서 딱 한 번만 실행한다.

    그럼 매우 깔끔하게 정리된다(!)

     

     

     

     

     

    하... 이 버그를 해결하기까지 진짜 몇 시간을 고생했는지 모르겠다ㅜㅜㅋㅋ

    stackoverflow에서 나랑 비슷한 경험을 한 사람들이 쓴 질문글이 엄청 많이 나오는 것을 보고 그나마 위안을 얻었다ㅋㅋ

     

    이벤트 함수를 등록할 때 같은 이벤트를 여러 번 중복해서 등록하지 않도록 주의해야한다는 것을 다시금 깨달았다.

    그리고 이벤트를 등록했다가 필요없어지면 꼭 리스너를 제거해주던지 클리어해주는 작업을 잊지말자...!!!

     

     

     

     


     

     

     

     

    https://socket.io

     

    Socket.IO

    SOCKET.IO 2.0 IS HERE FEATURING THE FASTEST AND MOST RELIABLE REAL-TIME ENGINE ~/Projects/tweets/index.js var io = require('

    socket.io

     

    https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

     

    The WebSocket API (WebSockets)

    The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without havi

    developer.mozilla.org

     

    반응형

    COMMENT