Socket.IO의 정체: Socket.IO와 웹소켓은 동일한 개념일까?

이은지·2024년 2월 18일
8
post-thumbnail

해당 글은 Socket.IO 공식문서 중 다음 두 페이지의 내용을 담고 있습니다. Socket.IO의 정체와 역할(그래서 얘가 대체 뭐하는 앤데?)에 대한 감을 잡고 싶다면 해당 글이 도움이 될거라 생각합니다.

Socket.IO란?

Socket.IO 정체 내가 한 줄 정리 해드림.
Socket.IO란... 웹소켓을 더 안전하고 편리하게 사용할 수 있게 해주는 라이브러리다.

Socket.IO는 웹소켓이라는 통신 프로토콜을 더 쉽게 사용할 수 있도록 사용하기 편리한 인터페이스연결의 안정성을 높이는 부가 기능들을 추가한 라이브러리라고 할 수 있다.

공식문서에서는 Socket.IO를 클라이언트와 서버 간의 저지연의(low-latency), 양방향의, 이벤트 기반의 커뮤니케이션을 제공하는 라이브러리라고 소개하고 있다.

웹소켓이랑 Socket.IO가 같은거야? (웹소켓과 Socket.IO의 차이점)

결론부터 말하자면, 그렇지 않다. 어떤 차이점이 있는지 살펴보자.

1️⃣ Socket.IO는 웹소켓만을 사용하지 않는다.

Socket.IO는 브라우저, 네트워크의 상태에 따라 다음 세 가지 low-level transports 옵션 중 하나를 선택하여 커넥션을 생성한다.

  • HTTP long-polling
  • WebSocket
  • WebTransport: WebSocket의 modern update를 제공하는 API

웹소켓은 Socket.IO가 사용하는 여러 통신 프로토콜 중 하나이다. 즉, Socket.IO와 웹소켓은 완전히 다른 층위에 속한 개념이라고 볼 수 있다. 여러 블로그들에서 '웹소켓의 사용'과 'Socket.IO의 사용'을 동일시하고는 하는데, 이는 부정확한 이해라고 볼 수 있다. 웹소켓을 처음 사용해보는 사람 대부분이 Socket.IO 라이브러리를 사용하는 경우가 많기 때문에 이러한 오해가 발생하는 듯하다.(내 이야기다 😅.)

추가적으로 Socket.IO이 WebSocket을 사용하긴 하지만, 각 패킷에 추가적인 메타데이터를 붙이기 때문에 순수 WebSocket과는 호환이 불가능하다.

2️⃣ Socket.IO는 웹소켓에는 없는 여러 기능들을 제공한다.

다음과 같은 기능들을 제공한다.

  1. HTTP long-polling fallback: 웹소켓 연결이 불가능할 때 HTTP long-polling을 사용한다. 예컨대 유저의 브라우저가 웹소켓을 지원하지 않을 때.

  2. Automatic reconnection: 주기적으로 연결의 상태를 체크한다. 클라이언트의 연결이 갑자기 끊어지면 자동으로 재연결을 시도한다.

  3. Packet buffering: 클라이언트 연결이 끊어지면 패킷들은 자동으로 버퍼에 쌓이고, 재연결 시에 전송된다. Socket이 연결되어 있지 않을 때에 전달된 이벤트는 버퍼에 쌓인다.

  4. Acknowledgements: 이벤트를 전송하고 이벤트 전송에 대한 응답을 받는 편리한 방식을 제공한다.

    // 이벤트를 발송한다.
    socket.emit("hello", "world", (response) => {
      console.log(response); // "got it"
    });
    // 이벤트 발생을 수신하고 핸들링 한다.
    socket.on("hello", (arg, callback) => {
      console.log(arg); // "world"
      callback("got it");
    });
    
    socket.timeout(5000).emit("hello", "world", (err, response) => {
      if (err) {
        // the other side did not acknowledge the event in the given delay
      } else {
        console.log(response); // "got it"
      }
    });
  5. Broadcasting: 연결되어 있는 모든 클라이언트에게 한 번에 이벤트를 전송할 수 있다.

// to all connected clients
io.emit("hello");

// to all connected clients in the "news" room
io.to("news").emit("hello");

Socket.IO의 핵심 엔진: Engine.IO

공식 문서에 뜬금 없이 Engine.IO라는 표현이 등장하는데, 이는 Socket.IO의 내부 엔진이다. Engine.IO는 1️⃣ 데이터의 전송2️⃣ disconnection 감지를 담당하며, Socket.IO는 여기에 몇 가지 부가 기능을 추가한 것이라고 이해하면 된다.

Engine.IO가 데이터를 전송하는 방식

  • 기본적으로 HTTP long-polling을 사용한다. 웹소켓 연결 대신 long-polling을 사용하는 이유는, 웹소켓 연결이 불가능한 케이스들이 존재하기 때문이다. (개인 방화벽, 백신 프로그램, 프록시 등) 웹소켓 연결이 성공한 이후에 HTTP long-polling 연결은 닫히게 된다.
  • 첫 커넥션 생성 시 서버는 클라이언트에게 다음과 같은 데이터를 전송한다.
{
  "sid": "FSDjX-WRwSA4zTZMALqx",// 세션 ID. 이후의 모든 HTTP 요청들의 쿼리 파라미터에 포함된다.
  "upgrades": ["websocket"], // 서버가 지원하는 "better" transports의 리스트
  "pingInterval": 25000, // heartbeat mechanism 에 사용되는 값
  "pingTimeout": 20000
}

Engine.IO의 disconnection 감지

네트워크의 연결이 끊기면 이를 빠르게 감지할 수 있어야 한다. 유저에게 현재 메시지의 송/수신이 불가능한 상태임을 인지시키거나, 재연결을 시도하는 등의 대응을 해야 하기 때문이다.

Engine.IO는 다음 네 가지 경우에 '연결이 끊겼다' 라고 간주한다. 즉, Engine.IO에서는 disconnection을 다음과 같이 정의한다.

  • 한 번의 HTTP 요청이 실패
  • WebSocket 연결이 닫혔을 때
  • socket.disconnect() 가 호출되었을 때
  • heartbeat mechanism
    • 서버는 pingInterval 마다 PING 패킷을 보낸다. 클라이언트는 pingTimeout 안에 PONG 패킷을 보내야 한다. pingTimeout 안에 PONG 패킷을 돌려받지 못하면, 서버는 연결이 끊어졌다고 간주한다. 반대로 클라이언트 입장에서 일정 시간 동안(pingInterval+pingTimeout) PING 패킷을 받지 못하면 연결이 끊어졌다고 간주한다

기본적인 사용법

채팅 기능을 기준으로 간략하게 사용법을 설명해보겠다.

socket 인스턴스 생성하기

우선, socket 인스턴스를 생성해야 한다.

import { io } from "socket.io-client";

const socket = io("https://server-domain.com");

나의 경우 전역에 파일을 하나 만들어 인스턴스를 생성한 다음, 컴포넌트에서 이를 import하여 사용했다.

연결 상태 확인하기/표시하기

Socket.IO는 이벤트 기반의 라이브러리다. 이 표현이 조금 생소할 수 있는데, 실제 사용 코드를 보면 쉽게 이해할 수 있다. 네트워크가 연결되었을 때 화면에 초록불을 표시하고 싶다고 해보자. 이렇게 코드를 작성하면 된다.


import socket from './socket.ts'

export default function App() {
  const [isConnected, setIsConnected] = useState(false)

  useEffect(() => {
    socket.on("connect"), () => {
      setIsConnected(true)
    }
  }, [])
  
  return (
    <div>{isConnected ? <div>connected</div> : null} </div>
  )
}

즉 네트워크가 연결되었을 때 socket 인스턴스는 "connect" 라는 이름의 이벤트를 발생시킨다. 이 이벤트에 대한 리스너를 등록하고, (socket.on("connect")) 리스너에 원하는 콜백 함수를 전달하면 된다.

참고로 socket 인스턴스는 다음과 같은 이벤트들을 발생 시킨다.

  • connect
    • Engine.IO에 의해 서버에 연결 -> Socket이 handshake 패킷을 전송 -> 커넥션 생성 -> "connect" 이벤트 발생
    • 연결이 끊겼다가 재연결될 때에도 "connect" 이벤트가 발생한다.
  • connect_error
    • low-level connection이 생성될 수 없을 때(HTTP long-polling, WebSocket)
    • 서버에 의해 연결이 거부됐을 때 발생한다.
  • disconnect: 연결이 되어 있다가 끊겼을 때 발생
    • 명시적으로 연결을 끊은 경우 (socket.disconnect()) 이 경우에는 재연결을 자동으로 시도하지 않는다.
      • io server disconnect
      • io client disconnect
    • 그 외. 재연결을 자동으로 시도한다.
      • transport close
      • transport error
      • ping timeout

socket 인스턴스의 라이프사이클 도식을 보면 좀 더 명확히 이해할 수 있다.

메시지 주고 받기

위에서 살펴본 이벤트는, Socket.IO가 알아서 발생시켜주는 이벤트라고 할 수 있다. 메시지를 주고 받기 위해선 커스텀 이벤트를 발생시켜야 한다.

서버와 메시지를 주고 받기 위한 이벤트를 정의해야 한다. 이벤트명은 자유이다. 가령 message 라는 이름의 이벤트를 정의할 수 있다. 그리고 이벤트 emit 시 넘겨줄 인자를 함께 정의해야 한다. 아래는 예시 코드다.


// 메시지 발송: 이벤트 emit

const sendMessage = () => {
  socket.emit('message',{name, message})
}

// 메시지 수신: 이벤트 listen

useEffect(() => {
  socket.on('message',({name,message})=>{
    setChat([...chat,{name,message}]) // 화면에 렌더링하는 chat state에 새 메시지를 추가한다.
  })
},[])

정리

이번 글에서는 Socket.IO의 정체와 동작 방식, 기본적인 사용법에 대해 알아보았다. 다음 글에서는 나에게 큰 재미와 고통을 주었던😇 채팅에서의 에러 핸들링과 연결 상태 관리를 구현한 과정에 대해 기술해보도록 하겠다.

0개의 댓글