Websocket과 Socket.io 라이브러리 전격 비교

Yunhye Park·2024년 5월 5일
0

Back To Basic

목록 보기
7/10
post-thumbnail

며칠 전 기술면접에서 io 라이브러리를 사용했을 때의 장점에 관한 질문을 받았다. 얼핏 기억은 나는데 확실히 정리가 안 되는 바람에 스스로 답변이 아쉬웠다.

하여, 이번 기회에 웹소켓과 io 라이브러리 모두를 정리해보고자 한다.

Websocket

웹소켓은 실시간 양방향(클라이언트-서버) 통신 기술이다. 즉 서버와 클라이언트가 실시간으로 데이터를 주고받을 수 있는 통신 채널을 열어주는 격이다. 에러 때문에 통신이 끊기거나 강제로 종료하지 않는 이상 한번 연결을 하면 영구적으로 통신이 유지된다.

  • Websocket Protocol
    : TCP 연결 기반의 실시간 양방향(서버-클라이언트) 프로토콜
  • Websocket API
    : 웹소켓 프로토콜을 편리하게 사용할 수 있도록 해주는 프로그래밍 인터페이스

대부분의 브라우저나 웹소켓을 활용한 라이브러리/프레임워크는 프로토콜을 자동으로 처리해주기 때문에 개발자로서 실질적으로 접하는 건 API를 통한 통신이다.

동작 원리 및 사용법

웹소켓은 서버와 클라이언트가 통신을 연결하고, 데이터를 주고받고, 작업이 끝났으면 연결을 끊는 흐름으로 이루어진다.

1. 웹소켓 연결 (Opening Handshake)

연결의 시작은 언제나 클라이언트다. 클라이언트가 서버에 'HTTP를 Websocket 프로토콜로 업그레이드 해달라'는 GET 요청을 보낸다.

앞서 프로토콜은 API를 사용하면 딱히 볼 일은 없다고 언급했지만, 통신 방식에 대한 이해를 하는 데엔 도움이 되는 것 같아 예시를 덧붙여 본다.

GET ws://localhost:3000/ HTTP/1.1
Host: localhost: 3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: zy6Dy9mSAIM7GJZNf9rI1A==
  • Sec-WebSocket-Version은 13 버전만 허용된다.

  • Sec-WebSocket-Key는 클라이언트가 서버에 보내는 일종의 확인 key이다. 웹소켓 프로토콜로 통신을 주고받으려면, HTTP 프로토콜을 비롯한 다른 요청에 서버가 응답해서는 안 된다.

  • 서버가 응답 코드를 반환해야 연결이 완료되는데, 이때 Sec-WebSocket-Accept으로 클라이언트의 확인 key에 대한 답도 전달한다.

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: EDJa7WCAQQzMCYNJM42Syuo9SqQ=
Upgrade: websocket

API를 사용하면 프로토콜은 알아서 처리되니까 new 생성자 함수로 소켓을 만들어 open해주면 된다.

메서드로는 onopen, send, onmessage, onclose 가 있다. 각 명칭에서 보이듯 소켓을 연결할 때, 서버에 데이터를 전달할 때, 서버로부터 데이터를 받을 때, 소켓 통신을 끊을 때 사용한다.

// 소켓 연결 후 데이터 전달
const socket = new Websocket('ws://localhost:3000');

socket.onopen = (e) => {
	console.log('소켓 연결');
};

http와 https의 차이와 마찬가지로, 암호화된 주소를 사용할 경우 ws 대신에 wss를 사용한다.

2. 데이터 주고받기

웹소켓은 비동기 방식의 이벤트 중심 프로그래밍이다. 즉 소켓이 오픈(연결)되어있는 동안 클라이언트와 서버는 데이터를 주고받거나 연결 상태 변경할 때 이벤트를 활용한다.

socket.onmessage = (e) => {
  	console.log(JSON.parse(e));
	socket.send(JSON.stringify({'userId': 'hello1234'}));
};

JSON으로 send하는 이유?
웹소켓에서의 message는 string이나 binary 형식이어야 한다. 주로 객체 데이터를 주고 받으니, 문자열로 직렬화(stringify) 및 역직렬화(parse)가 가능한 JSON이 유용하다.

3. 연결 종료

한 번 오픈한 소켓은 연결을 끊기 전까지 지속된다. 더이상 통신이 필요 없는 경우에 연결을 종료해준다.

socket.onclose = (e) => {
	console.log('웹소켓 연결 종료');
};

참고. addEventListener를 사용하는 경우

// 웹소켓 연결(핸드셰이킹) 후 서버에 데이터 전송
socket.addEventListener('open', (e) => {
	socket.send(JSON.stringify({'userId': 'hello1234'}));
});

// 서버로부터 메시지 받기
socket.addEventListener('message', (e) => {
	console.log(JSON.parse(e));
});

// 웹소켓 연결 종료
socket.addEventListener('close', (e) => {
	console.log('웹소켓 연결 종료');
});

// 에러 캐치
socket.addEventListener('error', (e) => {
  console.log('웹소켓 에러: ', e);
});

Websocket의 특징

  • HTML5 웹 표준 기술
    ➡️ 클라이언트를 위한 별도의 모듈 설치 불필요
    (하지만 다양한 메시징 기술을 사용하려면 라이브러리 필요)

  • 멀티플렉싱이 내장되지 않아 브로드캐스팅 X
    : 브로드캐스팅은 연결된 모든 사용자에게 메시지를 보내는 것을 말한다. 기본 웹소켓은 브로드캐스팅이 없기 때문에 서버에서 여러 클라이언트에게 동시에 메시지를 보낼 때 각 연결을 개별적으로 해야 한다. 이런 상황이 많아진다면 서버 과부하가 생길 수 있다.

    멀티플렉싱
    하나의 연결을 여러 명의 사용자 혹은 여러 서비스로 분할하는 것.

  • fallback 옵션 X
    : 통신이 연결되지 않았을 경우를 대비할 옵션이 주어지지 않는다. 그래서 연결되지 않으면 자동 복구를 시도하지 않는다.

  • 상태 없는(Stateless) 프로토콜
    클라이언트와 서버가 연결된 때에 상태 정보를 서버에 저장하지 않는다. 각 클라이언트의 요청을 서버에서 독립적으로 처리하기에 클라이언트의 이전 요청과 상태 정보에 대한 의존성이 없다.

가벼운 메시징이라면 기본 웹소켓 API로 구현할 수도 있긴 하지만, 조금 더 풍부한 기능을 위해서는 웹소켓 라이브러리를 활용하는 게 유용하다. 대표적으로 io 라이브러리가 있다.

socket.io 라이브러리

  • Javascipt 기반의 Websocket 라이브러리

웹소켓을 활용한 라이브러리이기 때문에 동작 원리는 크게 다를 게 없다. 다만 웹소켓을 보완한 몇 가지 특징이 있다.

socket.io 특징

  • 내장된 멀티플렉싱으로 브로드캐스팅 O
    : 서버에서 여러 클라이언트에게 동시에 메시지를 보낼 때 각 연결을 개별적으로 하지 않고 단 하나의 연결만으로 여러 작업을 처리한다. 그래서 접속한 사용자 모두에게 동시에 메시지를 전송할 수 있다.

  • 네임스페이스(namespace)와 룸(Room)
    : 네임스페이스는 소켓 통신을 분리하는 고유한 식별자다. 룸은 네임스페이스 내에서 클라이언트를 그룹화하고 관리한다. 예를 들어, 여러 클라이언트를 하나로 그룹화하여 특정 사용자(들)에게만 메세지 전송할 때 사용할 수 있다. 하나의 네임스페이스는 여러 룸을 가질 수 있다.

  • fallback 옵션 O
    : 개발자가 지정한 연결 방식에 따라 자동으로 재연결을 시도한다. 연결하기 전 대기 시간, 시도 횟수 등을 설정할 수 있다.

  • HTTP Polling으로 먼저 소켓 연결 설정
    : 이후, 가능하다면 이를 Websocket으로 업그레이드한다. 즉 Websocket으로 업그레이드 되지 않더라도 HTTP 폴링으로 연결을 미리 시도하기 때문에 연결 오류에 대응할 여력이 줄어든다.

    HTTP Polling
    클라이언트가 주기적으로 서버에 요청을 보내는 방식. 한번 연결이 되면 클라이언트와 서버 간의 데이터가 지속적으로 전송된다. 하지만 클라이언트에서 계속 요청을 보내기 떄문에 클라이언트가 많아지면 서버 부담이 급증한다.

  • 메시지 핸들링
    클라이언트와 서버가 메시지를 교환하는 과정에서 이벤트를 사용자가 정의할 수 있다. 이 부분은 아래 사용법을 보면 쉽게 감이 잡힌다.

  • 다중 플랫폼 지원
    : 기본적으로 백엔드는 Node.js 대상이지만, Java나 Python 등 다른 백엔드 언어도 사용할 수 있다.

  • 상태 저장 및 관리 기능
    : 클라이언트의 상태 정보를 저장하며, 상태를 공유할 수도 있다. 클라이언트의 세션 상태, 사용자 인증 정보 등의 활용이 가능하다.

사용법 - 설정

서버 인프라가 구축된 상태에서만 사용할 수 있으므로, 우선 모듈(socket.io, socket.io-client)을 설치한 후 서버부터 만들어준다. 클라이언트에서 'hi'라는 메시지를 보내고, 서버가 이를 받아 모든 사용자에게 전달하는 로직이다.

  • 서버 : 기본 설정
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);
  • 서버 : 소켓 연결, 메시지 수신(on) 및 전달(emit)
io.on('connection', (socket) => {
    console.log('소켓 연결');

    socket.on('chat message', (msg) => {
        console.log('메시지: ' + msg);
        // 모든 클라이언트에게 메시지 전송
        io.emit('chat message', msg);
    });
});

server.listen(3000, () => {
    console.log('3000 Port Open');
});
  • 클라이언트 코드
const io = require('socket.io-client');

const socket = io('http://localhost:3000');

// 소켓 연결 및 서버에 메시지 전송
socket.on('connect', () => {
    socket.emit('chat message', 'hi');
    
});

이제 모든 개념을 총망라한 간단한 예시로 각 메서드를 살펴보자.

사용법 - example

네임스페이스와 룸은 서버에서 만든다.

서버에서 네임스페이스와 룸을 생성하고, 클라이언트가 해당 네임스페이스의 룸에 join한다. 그리고 그 룸에 전송할 메시지 이벤트를 전달해 이를 서버에서 처리하는 로직을 예시로 작성해보았다.

  • 서버 코드
// 설정
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);

// 채팅용 네임스페이스(chat) 생성
const chatNamespace = io.of('/chat');

// 채팅용 네임스페이스 내 룸 생성
const roomA = 'roomA';
const roomB = 'roomB';

// 클라이언트가 네임스페이스에 연결되었을 때 이벤트 처리
chatNamespace.on('connection', (socket) => {
    socket.on('joinRoom', (room) => {
        socket.join(room);
    });

// 클라이언트가 메시지를 보낼 때 이벤트 처리
chatNamespace.on('connection', (socket) => {
    socket.on('msg', (room, message) => {
        // roomA에 속한 클라이언트에게 메시지 전송
        chatNamespace.to(room).emit('chatMsg', message);
    });
});
  • 클라이언트 코드
const io = require('socket.io-client');
// chat 네임스페이스에 연결
const socket = io('/chat');

// 소켓 연결
socket.on('connect', () => {
    console.log('소켓 연결');
});

// 서버에 roomA 조인 이벤트 전송
socket.emit('joinRoom', 'roomA');
// 서버에 메시지 이벤트 전송
socket.emit('msg', 'roomA', '반가워요!');

1. 연결

.on('connection', (socket) => {})

우선 소켓을 연결하기 위해서는 양측에서 설정을 마친 후, on 메서드에서 connection이라는 이름으로 콜백함수를 받는다.

.of('/chat');

기본 네임스페이스는 '/'이다. 커스텀 네임스페이스를 만들려면, of 메서드로 서버에서 생성해준다. 같은 경로로 클라이언트에서도 접속해준다.

2-1. 메시지 전송

.emit('이벤트이름', 보낼정보);

메시지를 보낼 땐 emit 메서드를 사용하여 이벤트의 이름과 함께 서버/클라이언트로 전달할 정보를 담는다.

2-2. 특정 사용자에게만 메시지 전송

.to(특정사용자).emit('이벤트이름2', 정보);

소켓이 처음 연결될 때 각 클라이언트는 고유한 id를 갖게 된다. 이를 클라이언트에서 서버에 넘겨주면, 서버에서 위 코드를 작성해 특정 사용자/그룹에게만 메시지를 보낼 수 있다.

3. 메시지 수신

.on('이벤트이름', (정보) => {});

수신할 때는 on이다. 웹소켓 기술은 이벤트 기반의 비동기 방식이므로, 받아온 데이터를 콜백함수로 처리한다.

언제 어떤 것을 사용할까?

Websocket API는 가볍고 빠르다. 고로 데이터 전송이 잦은 환경에 적합할 것 같다.
Ex. 주식/코인 시세 차트

하지만 프록시 서버가 있는 네트워크는 연결이 차단되어서 활용이 불가능하다. 이땐 Socket.io 같은 라이브러리가 필요하다.

사용자 인증 정보, 세션 데이터처럼 클라이언트의 상태가 필요할 때도 io 라이브러리가 적절하다.

오버헤드를 최소화하고 싶은 경우엔 Websocket API가, 다양한 메시징 기술을 여러 플랫폼에서 활용할 땐 Socket.io가 적절한 것 같다.

오버헤드
어떤 작업을 수행하기 위해 추가로 필요한 부가 비용, 시간, 자원. 즉 특정 기능을 수행할 때 발생하는 간접적인 리소스.

Websocket API와 io 라이브러리는 호환성이 없다

socket.io라이브러리는 웹소켓 기술을 원활하게 사용할 수 있도록 하는 라이브러리이긴 하나, 호환되진 않는다. 서로 프로토콜이나 연결 방식이 다르기 때문이다. 그래서 클라이언트는 Websocket API, 백엔드는 io라이브러리를 사용할 경우 메시지를 주고받기가 어려워진다.

개발하는 입장에선 가장 중요한 포인트가 아닌가 싶다. 얼핏 보면 관련 라이브러리니까 혼용이 될 것 같은데 그러지 않으니까.

정리

io 라이브러리는 브로드캐스팅이 가능해서 모든 사용자에게 동시에 메시지를 보내는 경우에 유용하다. 룸 기능이 있어서 특정 사용자들에게만 브로드캐스팅을 할 수도 있다.

fallback 옵션을 제공하여 연결이 실패했을 때 개발자가 설정한 방식으로 자동 재시도를 한다. 웹소켓 프로토콜은 HTTP 프로토콜을 바로 Websocket으로 업그레이드한다. 하지만 io 라이브러리에선 HTTP Polling으로 먼저 소켓을 연결한 뒤, 가능하다면 웹소켓으로 업그레이드한다. 그래서 초기 연결에 실패할 가능성이 줄어든다.

또한, 사용자가 정의한 이벤트로 메시지를 핸들링 할 수 있기 때문에 메시지 교환방식이 간편하다.

다중 플랫폼을 지원해서 다양한 언어 환경에서 활용할 수 있고, 클라이언트의 상태 정보를 저장하기 때문에 이를 활용할 수 있다.


참고

The WebSocket API and protocol explained
Socket.IO vs. WebSocket: Key differences and which to use

profile
일단 해보는 편

0개의 댓글

관련 채용 정보