[실시간 통신] Polling vs WebSocket

김건휘·2024년 6월 25일
0

✔️들어가며..

이번에 새로 시작하게 된 프로젝트에서 실시간 통신을 이용하여 구현해야하는 기능이 존재하여 이에 대한 공부를 하기 위하여 아티클을 작성하게 되었다. 실시간 통신 방법중에 대표적인 Polling방식과 WebSocket방식에 대하여 알아보고 차이점 및 장단점 그리고 구현 방법에 대해서 알아보는 시간을 갖겠습니다.

시작하기에 앞서 전통적인 HTTP통신 방식에 대한 소개를 하겠습니다.

✔️서버와 유저가 데이터를 주고 받으려면???????

=> 전통적인 방법인 HTTP(Hyper Transfer Protocol)통신 이라는 방법을 사용하여 데이터를 주고 받았다.

1. 클라이언트가 연결을 열고, 서버에 데이터를 요청(Request)한다.
2. 서버는 요청에 대한 응답(클라이언트의 요청에 따른 결과값, Response)을 클라이언트에 다시 보낸다.
3. 클라이언트가 서버에 요청을 보내고, 요청에 대한 응답을 받으면 통신이 종료된다.

이해하기 어렵다면? 문자메세지라고 생각하면 편함.

  • client: 데이터 달라고 요청
  • server: 답장으로 데이터를 보냄

✔️그런데 여기서 고유한 특징이 하나가 있다.

  • server는 절대로 먼저 선톡을 하지 않는다. => 마치 여러분들이 좋아하는 이성처럼
  • 클라이언트에서 먼저 요청을 보내야, 서버는 답장을 보낸다!

✔️Polling(폴링)

클라이언트가 n초 간격으로 request를 서버로 계속 날려서 response를 전달받는 방식

=> 사실 실시간인 것처럼 작동할 뿐, 지속적으로 클라이언트에서 request(요청)을 날려서 마치 실시간인 것처럼 작동하게 하는 방법이다.

✔️장점

  • 보다 구현이 쉬운 편

✔️단점

  • 서버측에서 보낼 내용이 없어도 클라이언트는 알 수 없기 때문에 계속해서 request를 통한 확인이 필요
  • 계속해서 requeset를 날리면 서버의 부담이 증가
  • 실시간인 것처럼 보이는 것일 뿐, 실시간은 아님 => 실시간 보장 x

=> 그런데 요즘은 실시간 주식거래에서 초단위 그래프, 채팅 등등 (찐)실시간 통신이 필수적으로 필요로 하는 기능들이 점차 늘어나고 있다.

✔️한번 생각해보자

실시간으로 정보를 서버에게 받으려면 클라이언트에서 요청을 보내야하는데, 2초마다 요청을 계속해서 보낸다고 생각해보자 => 너무 비효율적
서버에게 요청을 보내지 않아도 서버에서 데이터를 클라이언트로 넘겨주면 효율적이지 않을까? => 그래서 나오게 된게 websocket(웹소켓)!!!

✔️WebSoket

웹소켓은 클리이언트와 서버 간의 지속적인 양방향 연결을 제공하여 실시간 데이터 전송과 이벤트 기반 통신을 가능하게 한다

polling(HTTP 방식)은 문자,websocket은 전화라고 생각하면 이해하기 편함


  1. websocket 열기 : 이 과정을 Handshake라고 하며, 클라이언트와 서버간의 HTTP 요청/ 응답 교환으로 구성
    => 이 이후로부터는 HTTP가 아닌 WebSocket 프로토콜을 사용하여 통신(ws:// or wsss://)
  2. websocket을 통한 데이터 전송 : 성공적인 Handshake 이후, 클라이언트와 서버는 지속적으로 메시지를 교환할 수 있다(양방향 통신)
  3. websocket 닫기 : 목적을 달성하면 클라이언트와 서버 모두 종료 메시지를 전송 할 수 있다

✔️WebSocket이 필요한 경우

  • 실시간 양방향 데이터 통신이 필요한 경우
  • 많은 수의 동시 접속자를 수용해야하는 경우
  • 브라우저에서 TCP 기반의 통신으로 확장해야 하는 경우
  • 개발자에게 사용하기 쉬운 API가 필요한 경우

✔️장점

  • 양방향성 통신으로 HTTP의 한계를 극복할 수 있다.
  • 폴링 방식은 주기적으로 HTTP 요청이 발생하고 연결이 끊어지지만, 웹 소켓은 클라이언트와 서버가 한 번의 요청으로 지속적으로 연결된다.
  • 실시간 서비스에 유용하다.
  • HTTP 방식과 달리 불필요한 요청 / 응답 헤더 데이터가 존재하지 않다.

✔️단점

  • 폴링 방식 보다 구현이 어렵다

✔️Polling vs WebSocket 요약

  • Polling은 클라이언트가 일정 주기마다 서버에 요청을 보내 새로운 변경사항이 있는지 확인한다. 이 방식은 서버측에 변화가 없더라도 일정 주기마다 요청을 보내 확인을 해야 하므로 불필요한 트래픽이 낭비된다. 그러나 WebSocket 방식은 한 번의 요청으로 서버와 브라우저 간의 실시간 양방향 통신환경을 가질 수 있어, 클라이언트의 요청 없이 서버가 먼저 데이터를 전송할 수 있다.
  • 네트워크 탭을 확인해보면 폴링 방식은 주기적으로 클라이언트에서 서버로 요청을 보내지만 소켓은 로컬 호스트를 요청한 것과 웹 소켓을 요청한 것 두 번뿐이다.

✔️그럼 작심 서비스는?? 실시간성이 필요한 기능이 무엇 무엇이 있는지 알아보자.

아직 와이어 프레임이 확정은 아니지만

  • ✔️친구 접속 중 상태 표시
    => 이게 제일 중요해 보임

  • ✔️친구 요청
    => 굳이? 실시간성 보장하지 않더라도 재접속 했을 때, 요청 와있으면 친구 요청 뜰 수 있게 하면 쫌 그런가? 암튼 실시간성 보장해주면 좋아보인다

  • ✔️친구 집중 시간 표시

  • ✔️친구가 접속중인 사이트 표시
    => 이 기능은 익스텐션으로 구현해야 할 듯! but, websocket을 사용해야한다는 점은 공통점이다.

작심 서비스에 실시간성을 통한 기능이 정말 많아 보인다..... 할수있다!

✔️리액트에서 WebSocket 사용하기 예시코드(message 주고받기)

import React, { useState, useEffect } from 'react';

const WebSocketComponent = () => {
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket('ws://your-websocket-server-url');

    socket.onopen = () => {
      console.log('WebSocket connection established');
    };

    socket.onmessage = (event) => {
      const newMessage = event.data;
      setMessages((prevMessages) => [...prevMessages, newMessage]);
    };

    socket.onclose = () => {
      console.log('WebSocket connection closed');
    };

    socket.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    // Cleanup on unmount
    return () => {
      socket.close();
    };
  }, []);

  const sendMessage = () => {
    const socket = new WebSocket('ws://your-websocket-server-url');
    socket.send(message);
    setMessage('');
  };

  return (
    <div>
      <h1>WebSocket Chat</h1>
      <div>
        <input 
          type="text" 
          value={message} 
          onChange={(e) => setMessage(e.target.value)} 
        />
        <button onClick={sendMessage}>Send</button>
      </div>
      <div>
        {messages.map((msg, index) => (
          <div key={index}>{msg}</div>
        ))}
      </div>
    </div>
  );
};

export default WebSocketComponent;

1. WebSocket 설정 및 연결
우선 WebSocket 서버에 연결하는 코드를 작성해야 한다. 이를 위해 useEffect 훅을 사용하여 컴포넌트가 마운트될 때 WebSocket을 설정하고, 언마운트될 때 연결을 정리하도록 한다.

2. WebSocket 연결 및 메시지 전송
메시지를 서버로 전송하기 위한 sendMessage 함수를 작성.

3. WebSocket을 통한 메시지 처리
서버로부터 수신한 메시지를 처리하기 위해 onmessage 핸들러를 사용한다. 이 핸들러는 수신한 메시지를 messages 상태에 추가하는 역할을 한다.

cf) useRef를 사용하여 컴포넌트가 리렌더링되더라도 동일한 WebSocket 객체를 참조하게 해주어서 WebSocket 연결을 효율적으로 관리하는 방법도 있음.

✔️요약

  1. useEffect를 사용하여 컴포넌트가 마운트될 때 WebSocket 연결을 설정하고, 언마운트될 때 연결을 정리.
  2. WebSocket의 onmessage 핸들러를 사용하여 서버로부터 수신한 메시지를 처리.
  3. sendMessage 함수 => 사용자가 입력한 메시지를 서버로 전송.

✔️socket에서 사용되는 4개의 이벤트

  • open – 커넥션이 제대로 만들어졌을 때 발생함
  • message – 데이터를 수신하였을 때 발생함
  • error – 에러가 생겼을 때 발생함
  • close – 커넥션이 종료되었을 때 발생함

✔️친구 접속 중 상태 표시 코드 예시

import { useEffect, useState } from 'react';

interface Props {
    // 필요한 Props 타입 정의
}

const WebSocketComponent = (props: Props) => {
    const [isConnected, setIsConnected] = useState<boolean>(false);
    const [friendsStatus, setFriendsStatus] = useState<Record<string, boolean>>({});

    useEffect(() => {
        const userId = 'yourUserId'; // 현재 사용자의 ID
        const ws = new WebSocket(`ws://localhost:8080?userId=${userId}`); // 백엔드 url

        ws.onopen = () => {
            setIsConnected(true);
        };

        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            setFriendsStatus((prevStatus) => ({
                ...prevStatus,
                [data.userId]: data.isOnline,
            }));
        };

        ws.onclose = () => {
            setIsConnected(false);
        };

        return () => {
            ws.close();
        };
    }, []);

    return (
        <div>
            <h1>Friends Status</h1>
            {Object.entries(friendsStatus).map(([id, isOnline]) => (
                <div key={id}>
                    {id}: {isOnline ? 'Online' : 'Offline'}
                </div>
            ))}
        </div>
    );
};

export default WebSocketComponent;

✔️하나씩 살펴보자

const [isConnected, setIsConnected] = useState<boolean>(false);
const [friendsStatus, setFriendsStatus] = useState<Record<string, boolean>>({});
  • isConnected는 웹소켓이 연결되어 있는지를 나타내는 상태이다.
  • friendsStatus는 친구들의 접속 상태를 저장하는 객체, 객체의 키는 친구의 userId이고, 값은 온라인 여부를 나타내는 boolean 값이다.
useEffect(() => {
    const userId = 'yourUserId'; // 현재 사용자의 ID
    const ws = new WebSocket(`ws://localhost:8080?userId=${userId}`); // 백엔드 url

    ws.onopen = () => {
        setIsConnected(true);
    };

    ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        setFriendsStatus((prevStatus) => ({
            ...prevStatus,
            [data.userId]: data.isOnline,
        }));
    };

    ws.onclose = () => {
        setIsConnected(false);
    };

    return () => {
        ws.close();
    };
}, []);

useEffect 훅을 사용하여 컴포넌트가 마운트될 때 웹소켓 서버에 연결하고, 언마운트될 때 연결을 종료한다.

  • const userId = 'yourUserId'; 부분에서 현재 사용자의 ID를 설정.
  • new WebSocket(ws://localhost:8080?userId=${userId}); 부분에서 웹소켓 서버에 연결합니다. 왜? ws://를 사용하냐 간단하게 보안에 더 좋다고 생각하면 된다.
  • ws.onopen 이벤트 핸들러는 웹소켓이 성공적으로 연결되었을 때 호출되며, isConnected 상태를 true로 설정한다.
  • ws.onmessage 이벤트 핸들러는 서버로부터 메시지를 수신했을 때 호출되며, 수신한 데이터를 파싱하여 friendsStatus 상태를 업데이트한다.
  • ws.onclose 이벤트 핸들러는 웹소켓 연결이 종료되었을 때 호출되며, isConnected 상태를 false로 설정합니다.
  • return () => { ws.close(); }; 부분에서 컴포넌트가 언마운트될 때 웹소켓 연결을 종료한다.

아티클을 작성하면서 문득 든 생각!

"친구가 접속해있는지 표시해주는 기능은 양방향 통신이 필요한게 아니라, 서버에서 보내주는 데이터를 클라이언트에서 표시해주기만 하면 되는데 굳이 websocket을 사용해야 될까?"

그래서 찾아보았다

✔️실시간 업데이트에 사용되는 또다른 기술

  1. SSE (Server-Sent Events): SSE는 서버에서 클라이언트로 단방향 이벤트 스트림을 제공. WebSocket과 비슷하게 실시간 업데이트가 가능하지만, 양방향 통신은 지원하지 않는다. 주로 서버에서 클라이언트로 자주 업데이트를 푸시할 필요가 있는 상황에서 사용된다
    => 친구의 접속 상태와 같은 실시간 업데이트에 적합해 보임.(양방향 통신이 필요 없으니까)

  2. React-Query 및 SWR: React-Query와 SWR은 데이터 페칭 라이브러리로, 실시간성을 제공하는 데는 제한적. 주기적으로 데이터를 새로 고침(polling)하여 업데이트를 받을 수 있지만, WebSocket이나 SSE만큼 즉각적이지 않음. 주기적 업데이트가 허용되는 상황에서는 사용할 수 있지만, 실시간성이 중요한 경우에는 덜 적합.

✔️SSE 방식을 사용하여 친구 접속 중 표시 기능 구현 예시(참고만)

import { useState, useEffect } from 'react';

interface FriendStatus {
  friendId: number;
  online: boolean;
}

const FriendStatus = (): JSX.Element => {
  const [friendStatus, setFriendStatus] = useState<FriendStatus | null>(null);

  useEffect(() => {
    const eventSource = new EventSource('http://localhost:3000/events');

    eventSource.onmessage = (event) => {
      const data: FriendStatus = JSON.parse(event.data);
      setFriendStatus(data);
    };

    eventSource.onerror = (error) => {
      console.error('EventSource failed: ', error);
      eventSource.close();
    };

    return () => {
      eventSource.close();
    };
  }, []);

  if (!friendStatus) {
    return <div>Loading friend status...</div>;
  }

  return (
    <div>
      <p>Friend ID: {friendStatus.friendId}</p>
      <p>Status: {friendStatus.online ? 'Online' : 'Offline'}</p>
    </div>
  );
};

export default FriendStatus;

=> 구조나 사용 방식도 socket과 유사하다!

✔️그런데

작심 서비스의 친구 접속 중 상태 표시, 친구 집중 시간 표시는 단순히 사용자에게 데이터를 띄워주는 것이 아니라. 내 정보도 내 친구가 알 수 있어야하기 때문에, 내 상태와 정보도 서버로 보내주어야한다. 즉, 양방향 통신websocket을 사용하는게 맞다!

profile
공유할 때 행복을 느끼는 프론트엔드 개발자

0개의 댓글

관련 채용 정보