ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Polling 방식을 이용해 간단한 실시간(?) 채팅 구현하기
    Today I Learned 2022. 12. 15. 23:54

     

    운동 모집 게시글의 참가자들이 참여할 수 있는 실시간 채팅 페이지 구현에 필요한 정보들을 학습하고 있다.

     

    클라이언트와 서버가 상호 연결을 유지한 채로 데이터를 주고받을 수 있는 WebSocket 방식이 등장하기 이전에는 HTTP 방식을 이용해 마치 실시간으로 통신하는 것처럼 보이도록 하는 Polling 방식을 사용했다고 한다.

     

     

    Polling 방식은 클라이언트가 서버에게 일정한 주기로 요청을 보내 응답을 받아오는 식으로 이루어진다. 실시간으로 발생하는 이벤트가 언제 발생할지 예측할 수 없기 때문에 지속적으로 서버에 요청을 보내야 하는 만큼 클라이언트의 수가 많아지면 요청을 처리하는 서버의 부담이 커지고, 요청을 보내는 주기를 늘릴수록 실시간성이 떨어진다는 문제점이 있다. 하지만 HTTP 통신을 이용하기 때문에 구현이 간단하다는 점이 있다.

     

    오늘은 실험적으로 Polling 방식을 이용한 최소한의 동작을 수행하는 실시간 채팅을 구현해보았다. 클라이언트는 React, 서버는 Spring Boot를 이용해 구현을 진행했다.

     

    클라이언트는 일정한 주기로 서버에 메시지 목록을 받아오는 API 요청을 보내 응답을 받도록 했다. 응답에 새로운 메시지가 있다면 useState에 저장하고 있는 기존의 메시지 목록 상태를 응답으로 받아온 목록으로 대체하고 컴포넌트를 리렌더링하도록 했다. 채팅 목록 API 요청은 useInterval Hook 안에서 수행하도록 했는데, useInterval을 사용할 경우 컴포넌트가 unmount될 때 clearInterval을 자동으로 수행하므로 setInterval 사용 시 발생할 수 있는 리렌더링 이전의 state 값을 참조해 잘못된 값을 가져오는 문제를 방지할 수 있다.

     

    채팅 메시지 전송은 각 클라이언트에서 입력 필드에 메시지를 입력한 뒤 전송 버튼을 눌러 이루어진다. 요청을 수신하는 서버에 클라이언트를 식별할 수 있는 숫자 값을 입력한 메시지에 포함하여 서버에 전송한다.

     

    // App.jsx
    
    import { useState } from 'react';
    import axios from 'axios';
    import useInterval from 'use-interval';
    
    const apiBaseUrl = 'http://localhost:8001';
    
    const userIndex = Math.ceil(Math.random() * 100);
    
    export default function ChattingPage() {
      const [inputMessage, setInputMessage] = useState('');
      const [chattingLogs, setChattingLogs] = useState([]);
    
      useInterval(async () => {
        const { data } = await axios.get(`${apiBaseUrl}/chat`);
    
        const isChanged = data.messages.length !== chattingLogs.length;
        if (isChanged) {
          setChattingLogs(data.messages);
        }
      }, 1000);
    
      const handleChangeInput = (event) => {
        const { value } = event.target;
        setInputMessage(value);
      };
    
      const handleClickSendMessage = async () => {
        const messageToSend = {
          message: `사용자 ${userIndex}: ${inputMessage}`,
        };
        await axios.post(`${apiBaseUrl}/chat`, messageToSend);
      };
    
      return (
        <div>
          <p>
            사용자 id:
            {' '}
            {userIndex}
          </p>
          <label htmlFor="input-message">
            메시지 입력
          </label>
          <input
            id="input-message"
            type="text"
            value={inputMessage}
            onChange={(event) => handleChangeInput(event)}
          />
          <button
            type="button"
            onClick={handleClickSendMessage}
          >
            전송
          </button>
          {chattingLogs.length === 0 ? (
            <p>전송된 메시지가 없습니다.</p>
          ) : (
            <ul>
              {chattingLogs.map((message) => (
                <li key={message}>
                  {message}
                </li>
              ))}
            </ul>
          )}
        </div>
      );
    }

     

    백엔드 서버에서는 메시지 입력 요청이 들어올 경우 메시지 리스트에 메시지 내용을 저장하고, 메시지 목록 반환 요청이 들어올 경우 메시지 목록을 반환하는 단순한 구조이다.

     

    // controllers/ChattingController.java
    
    @RestController
    @CrossOrigin
    @RequestMapping("/chat")
    public class ChattingController {
        private final ChattingService chattingService;
    
        public ChattingController(ChattingService chattingService) {
            this.chattingService = chattingService;
        }
    
        @GetMapping
        public MessagesResponseDto message() {
            System.out.println("요청이 들어옵니다...");
    
            return chattingService.getMessages();
        }
    
        @PostMapping
        @ResponseStatus(HttpStatus.CREATED)
        public void input(
            @RequestBody MessageRequestDto messageRequestDto
        ) {
            chattingService.inputMessage(messageRequestDto.getMessage());
        }
    }
    // services/ChattingService.java
    
    @Service
    public class ChattingService {
        List<String> messages = new ArrayList<>();
    
        public MessagesResponseDto getMessages() {
            return new MessagesResponseDto(messages);
        }
    
        public void inputMessage(String message) {
            messages.add(message);
        }
    }

     

    동작하는 화면은 다음과 같다.

     

     

    특별히 기술적인 내용이 추가되는 것 없이도 HTTP만을 이용해 실시간과 유사한 서비스를 어렵지 않게 구현해볼 수 있었으나, 서버에서 요청을 받을 때마다 콘솔에 메시지를 출력하게 하니 클라이언트의 Interval이 돌 때마다 요청이 들어와 접속한 클라이언트가 늘어날수록 로그가 무서운 속도로 늘어나는 것을 확인할 수 있었다.

     

    그 외에 비록 간단하게 구현한 것이었지만, 중간에 사용자가 추가로 접속해 클라이언트의 수가 늘었을 때 추가로 접속한 사용자도 접속 이전의 메시지들을 확인할 수 있었다는 점이 아쉬웠다. 새로 입력받은 메시지와 목록으로 저장할 메시지를 분리시켜서 메시지가 새로 들어왔을 때 접속자들은 새로 입력받은 메시지를 가져가고, 모든 접속자들이 들어왔던 메시지들을 가져가고 나면 메시지를 목록으로 옮기는 방식을 고려했지만, 어떤 사용자가 언제 접속했는지 서버 입장에서 알아낼 방법이 마땅히 생각나지 않아 보류했다.

     

    내일은 WebSocket에 대해 좀 더 자세한 학습을 진행한 뒤, WebSocket을 이용해 실시간 채팅 구현을 진행하는 실험을 진행해보도록 하자.

     

     

    References

    Polling 방식

    - https://velog.io/@hahan/Polling-Long-Polling-Streaming

     

    useInterval

    - https://mingule.tistory.com/65

     

     

     

     

     

    댓글

Designed by Tistory.