WebSocket, Socket.IO, SockJS의 차이

프로젝트에서 실시간 양방향 통신을 구현하기 위해 웹소켓을 사용했다.
이 과정에서 웹소켓의 동작 원리를 공부하고, 관련 라이브러리인 Socket.IOSockJS의 차이점을 조사한 내용을 정리해보려 한다.


WebSocket

웹소켓(WebSocket)은 클라이언트와 서버 간의 양방향 통신을 가능하게 하는 프로토콜이다.

기존의 HTTP 요청/응답 방식은 클라이언트가 요청을 보낼 때만 서버가 응답을 줄 수 있는 단방향 통신 구조이지만, 웹소켓은 서버와 클라이언트 간의 실시간 데이터 교환이 가능하며, 서버가 클라이언트로 데이터를 푸시할 수 있는 구조를 지원한다.

웹소켓은 이를 통해 채팅이나 게임같은 실시간 데이터 전송이 필요한 기능을 만들 수 있도록 도와준다.

웹소켓의 주요 특징

양방향 통신

클라이언트와 서버 간에 데이터를 동시에 주고받을 수 있다. 기존의 폴링(Polling) 방식이나 롱 폴링(Long Polling)과 비교해 성능이 우수하다.

연결 유지

HTTP의 요청-응답 모델과 달리, 지속적인 연결로 인해 재연결 비용이 적다.

낮은 오버헤드

데이터 프레임의 크기가 작고, 추가적인 헤더가 거의 없어 네트워크 대역폭 사용량이 적다. REST API와 같은 요청-응답 기반 모델에 비해 효율적이다.

실시간 데이터 전송

주식 시세, 채팅, 게임 등 실시간 데이터가 필요한 애플리케이션에 적합하다.

웹소켓의 매커니즘

1. HTTP Upgrade 요청

웹소켓은 HTTP를 통해 연결을 시작한다. 클라이언트는 서버에 HTTP Upgrade 요청을 보내며, 프로토콜을 웹소켓으로 전환해달라고 한다.

GET /chat HTTP/1.1
Host: host.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

2. HandShake

서버는 101 Switching Protocols 상태 코드를 반환하며 요청을 승인한다.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

3. 웹소켓 통신

핸드셰이크가 완료되면 클라이언트와 서버 간의 연결이 지속적으로 유지된다.

표준 웹소켓 RFC 6455(WebSocket Protocol)을 사용하는 경우 데이터는 프레임(frame) 단위로 전송되며, 각 프레임은 다음과 같은 구조를 가진다.

FIN: 메시지의 마지막 프레임 여부
Opcode: 데이터 유형 (텍스트, 바이너리 등)
Payload Length: 데이터 길이
Payload Data: 실제 데이터 내용

클라이언트와 서버는 별도의 요청 없이 데이터를 주고받을 수 있다.


Socket.io와 SockJS는 뭐지?

웹소켓은 모든 브라우저에서 호환되지 않는다. (물론 대부분의 최신 브라우저에서는 지원한다..!)
특히, 구형 브라우저나 특정 네트워크 환경에서는 웹소켓 연결이 차단될 수 있다.

이러한 문제를 해결하기 위해 Socket.IO와 SockJS를 사용한다.
이 두 라이브러리를 사용하면 웹소켓이 지원되지 않는 환경에서도 폴백 메커니즘(스트리밍, 폴링 등)을 통해 원활히 실시간 통신이 가능하게 구현할 수 있다.


두 라이브러리의 폴백 메커니즘

Socket.IO의 폴백 메커니즘 순서

  • 웹소켓(WebSocket): 기본적으로 가장 먼저 시도
  • XHR 폴링(Polling): 웹소켓이 차단되거나 지원되지 않는 경우 폴링 방식으로 전환

SockJS폴백 메커니즘 순서

  • 웹소켓(WebSocket): 가장 먼저 시도
  • XHR 스트리밍(XHR Streaming): 웹소켓이 불가능한 경우, 클라이언트와 서버 간에 데이터를 스트리밍 형태로 지속적으로 보내는 방식
  • XHR 폴링(XHR Polling): XHR 스트리밍이 불가능한 경우, 마지막 대안으로 폴링 방식을 사용

cf ) XHR 스트리밍과 폴링의 차이점


두 라이브러리의 차이

SockJS는 구형 브라우저에서의 호환성을 보장하기 위해 사용하는 라이브러리이다.
Socket.IO 또한 폴백 메커니즘을 통해 구형 브라우저와 네트워크 환경에서도 안정적으로 동작할 수 있다.
하지만 Socket.IO는 추가적으로 웹소켓 확장 및 고급 기능을 제공한다.

대표적인 고급 기능으로는 다음이 있다.

  • 네임스페이스 지원:
    • 서버의 단일 인스턴스에서 여러 독립적인 네임스페이스를 생성하여 각각의 이벤트와 메시지 흐름을 독립적으로 관리할 수 있다.
  • 방(Room) 기능:
    • 클라이언트를 특정 그룹으로 묶어 같은 그룹에만 메시지를 전송하는 기능.
  • 이벤트 기반 통신:
    • 서버와 클라이언트 간 커스텀 이벤트를 정의하여 효율적인 데이터 흐름을 관리.

복잡하고 유연한 기능이 요구되는 애플리케이션에서는 Socket.IO를 사용하는 것이 적합해보인다. 반면, 단순한 웹소켓 동작을 주로 수행한다면 SockJS가 적합하다.

이는 Socket.IO가 고급 기능을 제공하는 대신, 커스텀 프로토콜과 추가 로직으로 인해 성능 부담이 상대적으로 클 수 있기 때문이다.


내 프로젝트에서는 뭘 쓸까?

내 프로젝트에서는 Socket.IO와 SockJS 모두 사용하지 않기로 했다. 기본 웹소켓STOMP(웹소켓 위에서 동작하는 메시징 프로토콜)를 사용하기로 했다.
이유는 다음과 같다.

  • 백엔드 환경이 스프링을 사용한다.

    • Socket.IO는 기본적으로 Node.js와 잘 맞는 라이브러리지만, 스프링 백엔드에서는 추가적인 설정이 필요하다.
    • 반면, 스프링은 웹소켓 + STOMP 프로토콜을 쉽게 지원하며, 이를 활용하면 Socket.IO의 방 기능과 유사한 로직을 구현할 수 있다.
    • 따라서 굳이 Socket.IO를 사용하지 않기로 했다.
  • SockJS는 폴백 매커니즘을 제공하지만 불필요하다.

    • SockJS는 웹소켓을 지원하지 않는 환경에서도 사용할 수 있도록 폴백 기능(롱 폴링 등)을 제공하지만 현재 사용되는 대부분의 브라우저는 웹소켓을 지원한다.
    • 웹소켓을 지원하지 않는 브라우저(IE 9 이하 등)를 고려할 필요가 없다고 생각해 굳이 SockJS를 사용하여 복잡성을 늘리지 않기로 했다.