ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React/Node.js] Chat App, Typing Indicator - 채팅 앱에서 대화 상대가 텍스트를 입력 중일 경우 표시해주는 기능 구현하기
    Frontend 2019. 10. 6. 20:16

     

     

     

    대화 상대방이 텍스트를 입력 중일 경우 상대방 화면에 표시해주는 기능을 처음으로 구현해보았는데

    별거 아닌 기능이지만 막상 만들려니까 어떻게 해야될지 감이 안잡혀서 고생했다ㅋㅋ

     

    일단 구현하는 로직은 이렇다.

     

     

     

    1.

    사용자가 input박스에 텍스트를 칠 때마다 서버에 startTyping 이벤트를 보내는데,

    startTyping이벤트를 거는 동시에 setTimeout()으로 일정시간 뒤에 stopTyping이벤트가 실행되도록 한다.

     

    2.

    그런데 input의 onChange 이벤트가 계속된다면 clearTimeout()을 이용하여

    setTimeout으로 걸어놓은 stopTyping 이벤트들을 계속해서 clear시켜준다.

     

    3.

    그러다가 input에 change 이벤트가 더 이상 일어나지 않으면

    그 때서야 비로소 setTimeout으로 걸어놓은 stopTyping이벤트가

    일정시간 있다가 trigger되어 typing indicator를 화면에서 숨긴다.

     

     

    import { connect } from 'react-redux';
    import socketIOClient from 'socket.io-client';
    import App from '../components/App';
    import * as action from '../actions/actions';
    
    const SERVER_URL = 'http://localhost:8080/';
    const socket = socketIOClient(SERVER_URL);
    
    const subscribeSocket = (dispatch) => {
      socket.on('startTyping', () => {
        dispatch(action.startTyping());
      });
    
      socket.on('stopTyping', () => {
        dispatch(action.stopTyping());
      });
    
      socket.on('sendTextMessage', ({ chat }) => {
        dispatch(action.sendNewTextSuccess(chat));
      });
    };
    
    const mapStateToProps = state => {
      return state;
    };
    
    const mapDispatchToProps = dispatch => {
      subscribeSocket(dispatch);
    
      return {
        handleTextSending: (text, socketId) => {
          if (!text.trim()) {
            return;
          }
          socket.emit('sendTextMessage', text, socketId);
        },
        handleTypingStart: () => {
          socket.emit('startTyping');
        },
        handleTypingStop: () => {
          socket.emit('stopTyping');
        }
      };
    };
    
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(App);
    

     

    React의 container에서 서버로 startTyping 이벤트와 stopTyping 이벤트를 emit하는 함수를 만들어준다.

    그리고 서버로부터 같은 이름의 이벤트를 수신할 수 있도록 등록해준다.

     

     

     

    const totalRoomList = {};
    
    module.exports = (io) => {
      io.on('connection', (socket) => {
        socket.on('startTyping', () => {
          const roomKey = totalRoomList[socket.id];
    
          socket.broadcast.to(roomKey).emit('startTyping');
        });
    
        socket.on('stopTyping', () => {
          const roomKey = totalRoomList[socket.id];
    
          socket.broadcast.to(roomKey).emit('stopTyping');
        });
      });
    };
    

     

    서버에서는 startTyping 이벤트를 받아서 해당 socket.id가 속한 방 번호를 알아낸 후,

    해당 방에 broadcast로 startTyping 이벤트를 emit한다.

     

    broadcast를 이용하면 자기 자신을 제외한 다른 client들에게만 메시지를 보낼 수 있다.

     

     

     

    import React, { useEffect } from 'react';
    import { Redirect } from 'react-router-dom';
    import { debounce } from 'lodash';
    import './css/chatroom.scss';
    
    const ChatRoom = (props) => {
      const {
        roomConnection,
        textSending,
        handleTextSending,
        handleTypingStart,
        handleTypingStop
      } = props;
    
      const START_INTERVAL = 300;
      const WAIT_INTERVAL = 800;
      const ENTER_KEY = 13;
    
      let textInput = React.createRef();
      let stopTypingTimer = null;
    
      const triggerChange = () => {
        if (textInput && textInput.current) {
          handleTypingStop();
          clearTimeout(stopTypingTimer);
        }
      };
    
      const debouncedTypingStart = debounce(handleTypingStart, START_INTERVAL, {
        leading: true,
        trailing: false
      });
    
      const handleKeyDown = (e) => {
        if (e.keyCode === ENTER_KEY) {
          triggerChange();
        }
      };
    
      const handleChange = (e) => {
        clearTimeout(stopTypingTimer);
        debouncedTypingStart();
    
        stopTypingTimer = setTimeout(triggerChange, WAIT_INTERVAL);
      };
    
      return (
        <div className="chat-container">
          <ul className="text-list">
            {textSending.chats.length > 0 && (
              textSending.chats.map((chat, index) => {
                const isMyText = roomConnection.info.id === chat.id;
    
                return (
                  <li key={chat.id + index} className={isMyText ? 'me' : 'partner'}>
                    <div className="nickname">
                      {isMyText ? roomConnection.info.name : roomMatch.partner.name}
                    </div>
                    <div className="chat-content">
                      {chat.text}
                    </div>
                  </li>
                );
              })
            )}
          </ul>
          <form
            className="chat-form"
            onSubmit={e => {
              e.preventDefault();
    
              handleTextSending(textInput.current.value, roomConnection.info.id);
              textInput.current.value = '';
              textInput.current.focus();
            }}
          >
            {textSending.isTyping && (
              <div className="info-type">
                <p className="info-message">
                  {roomMatch.partner.name} is typing...
                </p>
              </div>
            )}
            <input
              type="text"
              className="input-chat"
              ref={textInput}
              onChange={handleChange}
              onKeyDown={handleKeyDown}
            />
            <button
              type="submit"
              className="btn-send"
            >
              SEND
            </button>
          </form>
        </div>
      );
    };
    
    export default ChatRoom;
    

     

    이제 해당 text input에 onChange 이벤트로 handleChange 이벤트를 등록한다.

    handleChange 이벤트를 보면 기존에 clearTimeout으로 걸어둔 stopTyping 이벤트를 clear하고

    startTyping이벤트와 stopTyping이벤트를 다시 걸어준다.

     

    나는 startTyping이벤트도 lodash의 debounce 함수를 이용해서 이벤트 발생 횟수를 줄여주었다.

    debounce를 걸지 않으면 서버쪽에서 너무 많은 startTyping 이벤트가 감지된다.

    그리고 stopTyping 이벤트는 800밀리초 뒤에 실행되도록 등록해준다.

     

    이렇게하면 내가 input에 입력을 시작한 시점으로부터 300밀리초 뒤부터 startTyping 이벤트가 300밀리초 간격으로 실행된다.

    그 간격으로 서버에 요청이 가서 typing indicator를 화면에 보여주도록 state를 관리하고

    input에 입력을 멈추면 그 시점으로부터 800밀리초 뒤에 state를 변경하는 stopTyping 이벤트를 발생시켜 화면에서 typing indicator를 숨긴다.

     

    그리고 text input에 onKeyDown 이벤트도 걸어서 엔터키를 누르는 경우 무조건 timer를 clear하도록 지정해준다.

     

     

     

     

    참고 자료:

     

    https://www.pubnub.com/blog/react-chat-typing-indicators/

     

    React JS Chat Tutorial: Typing Indicators (4/4) | PubNub

    How to add typing indicators in a React chat application to show when users are currently typing.

    www.pubnub.com

     

    반응형

    COMMENT