웹소켓(WebSocket)은 하나의 TCP 컨넥션으로 서버와 클라이언트 간에 양방향 통신을 제공하는 프로토콜입니다. 본문에서는 웹소켓이 무엇인지 알아보고 Node.js와 웹소켓를 사용하여 메아리 애플리케이션을 만들어보겠습니다.
❶ 웹소켓과 socket.io
1. 웹소켓 소개
지금까지는 서버에 요청을 보내서 응답을 받는 애플리케이션을 만들었습니다. 당연히 요청을 줘야지만 응답을 받는 것이 아닌가라고 생각할 수 있지만, 사용자가 직접 화면을 갱신하지 않아도 자동으로 화면을 갱신하는 애플리케이션도 있습니다. 주식이나 채팅 애플리케이션이 그 예입니다. 이러한 일을 하려면 서버에 주기적으로 요청을 보내서 받아오거나 서버에서 데이터를 보내주어야 합니다.
▼ 요청에 대해서만 응답하는 HTTP 통신
웹은 HTTP 프로토콜 위에서 동작하고 있기 때문에 요청을 보내야지만 서버가 응답을 주게 됩니다. 즉 양방향 통신을 지원하지 않습니다. 그렇다고 주기적으로 받아오는 것은 매우 비효율적입니다. 몇 초에 한 번씩 서버가 알아서 응답을 주면 좋은데, 단방향 통신이므로 클라이언트에서 주기적으로 의미없는 요청을 보내야 하기 때문입니다.
▼ 주기적으로 요청을 보내서 응답을 받는 폴링 방식
그렇다면 화면 갱신 없이 실시간성을 요구하는 애플리케이션을 어떻게 만들어야 할까요? 웹소켓(WebSocket)을 사용하면 됩니다. 웹소켓 이전에는 폴링 또는 롱폴링이라는 방법을 사용했습니다. 폴링은 주기적으로 요청을 보내는 방식이고, 롱폴링은 클라이언트와 서버 간의 커넥션을 유지한 상태로 응답을 주고받는 방식입니다. 클라이언트가 서버로 요청을 보내면 서버는 클라이언트의 요청을 기다립니다. 그리고 요청한 데이터에 변화가 있을 때 응답을 보냅니다. 폴링이나 롱폴링이나 둘 다 클라이언트가 서버에게 요청을 보내야 합니다.
▼ 요청을 보내고 서버에서 응답이 올 때까지 대기 후, 응답이 오면 바로 다시 요청을 보내는 롱폴링
웹소켓은 하나의 TCP 컨넥션으로 서버와 클라이언트 간에 양방향 통신을 할 수 있게 만든 프로토콜입니다.* 2022년 현재 거의 대부분의 웹브라우저에서 안정적으로 사용할 수 있습니다.* 다만, IE9 등 오래된 웹브라우저는 지원하지 않습니다. 오래된 브라우저를 지원하려면 socket.io 같은 라이브러리를 사용해 폴링이나 롱폴링을 사용해 기능을 구현해야 합니다. 웹소켓은 양방향 통신을 지원하므로 브라우저 상에서 리프레시 없이 실시간성을 요구하는 애플리케이션을 구현할 수 있습니다.
▼ 양방향 통신이 가능한 웹소켓
웹소켓의 특징은 크게 두 가지입니다. 첫 번째 특징은 양방향 통신입니다. 이는 데이터의 송수신을 동시에 처리한다는 뜻이며 클라이언트와 서버가 원하는 때 데이터를 주고받을 수 있다는 의미입니다. 통상적인 HTTP 통신은 클라이언트가 요청하는 때만 서버가 응답하는 단방향 통신이었습니다. 두 번째 특징으로는 실시간 네트워킹을 구현하는 것이 용이하다는 겁니다. 웹 환경에서 연속된 데이터를 빠르게 노출하고 싶은 때, 예를 들어 채팅이나 주식 앱에 적합합니다. 또한 브로드캐스팅을 지원하므로 여러 클라이언트와 빠르게 데이터를 교환할 수 있어 편리합니다.
* 2011년 12월에 인터넷의 표준을 정하는 기관인 IETF에 의해서 RFC 6455로 표준화되었습니다.
* 웹소켓 브라우저 호환성: https://developer.mozilla.org/enUS/docs/Web/API/WebSocket#browser_compatibility
1.1 웹소켓의 동작 방법
웹소켓 프로토콜은 크게 핸드 쉐이크와 데이터 전송으로 나눌 수 있습니다. 핸드 쉐이크는 서버와 클라이언트가 커넥션을 맺는 과정으로써 최초 한 번만 일어납니다. 이때는 HTTP 1.1 프로토콜을 사용하고 헤더에 Upgrade: websocket과 Connection: Upgrade를 추가해서 웹소켓 프로토콜을 사용하도록 해줍니다.
▼ 웹소켓 프로토콜 작동 과정
1단계에서는 HTTP 핸드쉐이크를 수행해 연결을 맺습니다. 클라이언트가 다음과 같은 데이터를 서버로 보냅니다.
GET /chat HTTP/1.1 // ❶ HTTP 1.1 이상
Host: server.example.com
Upgrade: websocket // ❷ 현재 프로토콜에서 다른 프로토콜로 업그레이드하는 규칙
Connection: Upgrade // ❸ Upgrade 필드가 있으면 반드시 같이 명시
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 클라이언트 키
Origin: http://example.com // 필수 항목. 클라이언트 주소
Sec-WebSocket-Protocol: chat, superchat
// ❹ 클라이언트가 요청하는 하위 프로토콜
Sec-WebSocket-Version: 13
❶ 핸드쉐이킹은 GET으로 보내야 하며 HTTP 1.1 이상이 필수입니다. ❷ Upgrade 필드는 HTTP 프로토콜에서 웹소켓 프로토콜로 변경에 필요한 값입니다. 프로토콜을 업그레이드하려면 반드시 명시해야 합니다. ❸ Upgrade 필드가 있으면 반드시 같이 명시해야 합니다. 없으면 연결이 되지 않습니다. ❹ Sec-WebSocket-Protocol은 클라이언트가 요청하는 하위 프로토콜입니다. 순서에 따라서 우선순위를 부여하며, 서버에서 여러 프로토콜이나 프로토콜 버전을 나눠서 서비스할 때 필요한 정보입니다.
그러면 서버는 핸드쉐이크의 응답으로 다음과 같은 데이터를 보내줍니다.
HTTP/1.1 101 Switching Protocols // ❶ 연결 성공
Upgrade: websocket
Connection: Upgrade
// ❷ 클라이언트로부터 받은 키를 사용해 계산된 값
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
❶ 101 Switching Protocols로 응답이 오면 웹소켓 프로토콜로 전환해 연결이 잘된다는 것을 의미합니다. 이제부터는 양방향 송수신을 할 수 있습니다. ❷ Sec-WebSocket-Accept는 클라이언트로부터 받은 키를 사용해 계산한 값입니다. 해당 값은 클라이언트와 서버 간 인증에 사용됩니다. 핸드쉐이크가 완료되면 프로토콜이 HTTP에서 ws로, HTTPS라면 wss로 변경되어 데이터를 전송할 수 있는 2단계로 접어듭니다. 데이터는 메시지(Message)라 부르며 메시지는 프레임(Frame)의 모음입니다. 프레임은 바이트의 배열이며 다음과 같은 형태를 가집니다.
▼ 프레임구조(출처:https://www.rfc-editor.org/rfc/rfc6455#page-28)
프레임은 헤더와 페이로드(Payload)로 이루어져 있습니다. 헤더는 FIN, RSV1~3, 오프코드(Opcode), 마스크MASK, 페이로드 길이, 마스킹 키가 있습니다.
▼ 프레임 필드
마지막 3단계에서는 접속을 끊습니다. 접속은 클라이언트와 서버 양쪽에서 모두 끊을 수 있으며, opcode에 1000을 담아서 보내면 됩니다.
지금까지 웹소켓의 양방향성과 웹소켓 프로토콜의 동작 방법을 알아보았습니다. 웹소켓은 데이터를 전송하는 것만 지원할 뿐 전송되는 데이터를 가지고 무엇을 할지는 개발자의 몫입니다. 예를 들어 채팅 애플리케이션을 만들고자 할 때 채팅방 만들기, 채팅방에 들어온 사람들 전체에게 메시지 발송하기, 접속이 끊어진 경우 자동으로 다시 연결하기 등의 기능을 구현하고자 한다면 이런 부분은 개발자가 모두 만들어줘야 합니다. socket.io를 사용하면 추가로 이런 부분들을 지원해주므로 웹소켓보다는 코드 작성의 부담이 적습니다.
* ping, pong은 서버와 클라이언트의 연결 확인 시 사용하는 코드입니다. 클라이언트가 ping을 날리면 서버는 pong으로 응답합니다.
2. socket.io
2편에서는 프레임워크의 도움 없이 웹소켓을 사용한 메아리 애플리케이션을 만들 겁니다. 하지만 웹소켓은 프로토콜이기 때문에 메시지 전송만을 제공해주고 있어서 채팅을 위한 채팅방, 접속한 유저 모두에게 메시지를 발송하는 브로드캐스팅 기능, 접속이 의도치 않게 끊어졌을 때 재접속하는 방법 등 기능은 모두 개발자가 만들어야 합니다. 그래서 웹소켓 기반의 다양한 기능을 제공하는 라이브러리인 socket.io(소켓아이오)를 소개하고 넘어가고자 합니다.
socket.io는 Node.js 기반으로 서버와 클라이언트의 웹소켓 양방향 통신을 지원하는 라이브러리입니다. 기본적으로 웹소켓을 지원합니다. 웹소켓을 지원하지 않는 브라우저에서는 롱폴링 방식을 사용한 통신을 지원합니다. 또한 재접속, 브로드캐스팅, 멀티플렉싱 기능도 제공합니다.
브로드캐스팅은 접속한 클라이언트 모두에게 메시지를 전송하는 것을 의미합니다.
멀티플렉싱은 커넥션 하나를 논리적으로 나누어서 데이터를 원하는 채널에만 전송하는 기법입니다. 슬랙의 워 크스페이스나 게임에서는 채널의 개념과 비슷하다고 생각하면 됩니다. socket.io에서는 네임스페이스라고 부르며, 채팅방을 만드는 기능인 룸(Room)과 함께 사용해서 채팅방별로 메시지를 통신하는, 조금 더 정교하게 메시지 송수신을 제어할 수 있습니다.
성능이 중요하다면 웹소켓을 사용하는 것이 좋습니다. 다만, 커넥션이 끊어지는 경우 메시지의 처리는 알아서 구현해야 하는 점이 조금 불편할 수 있습니다. socket.io와 웹소켓은 각자 장단점이 있기 때문에 상황에 따라서 사용해야 합니다.
다음 편에서는 Node.js와 웹소켓를 사용하여 메아리 애플리케이션을 만들어보겠습니다.