Search
▪️

Understanding WebSockets & Socket.io

What Are WebSockets & Why Would You Use Them?

이제까지 일반적인 Client와 Server는 Client가 Server에게 Request를 보내게 되면 Server에서는 이와 관련해서 데이터베이스에 접근하여 작업한 후, Client에게 Response를 보내는 식이었다. 이런 방법이 나쁜 것은 아니다. 이와 같이 Client로부터 정보를 Pull 해내는, 즉 Server에게 원하는 것을 말하여 원하는 것을 Pull 해내는 Pull Approach는 대부분의 Web Application에서 사용하는 방식이다.
이처럼 Client가 원하는 것을 Server에게 알려서 데이터를 받는 Pull Approach 말고, 만일 Server가 Client에게 정보를 Inform하고 싶다면 어떻게 해야하는가? → 기존과 같은 HTTP 통신 방식으로는 해결할 수 없다. 따라서 등장한 것이 WebSocket이다. (일반적으로는 Server 상에서 바뀐 정보를 Active 하게 Client에게 알리고 싶을 때 사용한다. 채팅과 같이 말이다.)
HTTP는 Request를 보내고 Response를 받는 Protocol이었다. WebSocket은 완전히 새로운 것이 아니라 HTTP 통신을 기반으로 만들어진 것이다. WebSocket Protocol은 기존 HTTP Protocol에서 조금 더 발전한, HTTP Handshake라는 방식을 이용한다.
HTTP Protocol에서는 Client와 Server가 Request, Response로 통신을 했다면, WebSocket Protocol에서는 Client와 Server가 Pushing Data로 통신한다. (Server to Client, Client to Server 모두 Pushing Data가 가능하다.) 즉, 가장 큰 차이점은 Server가 데이터 변화에 대해서 Client보다 먼저 Client에게 알릴 수 있다는 것이다.
그렇다면 HTTP와 WebSocket 둘 중 하나만 골라서 Application을 구현해야 하는가? → No, 그렇지 않다. 두 Protocol을 모두 이용하여 Application을 구현할 수 있다. Request, Response Pattern이 필요하다면 이용할 수도 있고, Active Notification을 위해서 WebSocket도 쓸 수 있다는 것이다.

WebSocket Solutions - An Overview

Node.js에서 Express.js는 WebSocket을 지원하는 Third Party Package가 여럿 존재한다. 이 중에서 가장 흔히 쓰이는 Package는 socket.io라는 Package이다.
socket.io라는 Package는 WebSocket Channel을 간편하게 Set Up할 수 있도록 하며, Set Up된 Channel을 Client가 쉽게 이용할 수 있도록 구현된 Package이다.

Setting Up Socket.io on the Server

socket.io Package는 Frontend, Backend 모두 Import한다. (Client와 Server가 서로 통신할 것이기 때문이다.)
따라서 Frontend, Backend에서 모두 socket.io를 Import했다면 서로의 Endpoint를 맞춰 Channel을 생성해야 한다. 아래 명령어를 통해서 Third Party Package를 설치할 수 있다.
npm install —save socket.io (Backend) npm install —save socket.io-client (Frontend)
Package를 설치했다면, Backend에서는 app.js에서 Socket Connection에 대한 설정을 하도록 한다. 기존 Server는 app.listen($portNumber)를 통해서 구동 되었다. (Express.js의 listen은 HTTP createServer()를 포함하고 있었다.) 하지만 Socket에 대한 설정은 아래와 같이 한다. 이를 보면 알겠지만, 기존 createServer()가 구동되고 있는 listen하고 있는 Server를 인자로 받는 것을 통해 HTTP Protocol을 기반으로 WebSocket이 이용되는 것을 Code를 통해서 확인할 수 있다. 즉, WebSocket Connection은 HTTP Server를 기반으로 Set Up된다.
const server = app.listen(8080); const io = require('socket.io')(server);
위와 같이 Socket Connection이 설정 되었다면, io라는 Socket Object를 이용할 수 있다. 설정된 Socket Connection을 통해 io라는 Socket Object에 Event Listener를 정의하여 특정 Routine을 실행할 수 있도록 할 수 있다. 예를 들어, 새로운 Client가 Connect되었을 때, 수행될 Callback Function을 Event Listener에 둘 수 있다는 것이다. Connection 마다 Callback Function을 수행하도록 설정했을 때, io라는 Socket Object에 Connect하기 위해서는 단순히 HTTP Server에 Client가 접속한다고 끝이 아니라, Client 측에도 Socket에 대한 설정이 필요하다. Client Side에서는 Port Number를 포함한 Server의 URL을 인자로 하여 socket.io-client의 Function을 호출한다. 이렇게 하면 Backend의 Socket Object에 등록해두었던 Event Listener가 작동하는 것을 알 수 있다.

Sharing the IO Instance Across Files

Backend에서 HTTP Server를 기반으로 생성된 Socket Object에 Client의 Connection을 감지할 수 있는 Event Listener를 추가했었다. 즉, Client의 Connection을 관리하는 Socket Object를 Controller와 같은 다른 File에서 활용하기 위해서는 app.js에서 선언된 Socket Object를 다른 File에도 공유할 수 있어야 한다.
일반적으로는 다른 Script로 Socket Object를 공유 해야하기 때문에 app.js에 Socket Object를 생성하는 것이 아니라, Singleton Pattern으로 Socket Object를 생성한다. 즉, Project의 최상위 Path에 socket.js를 두어, Socket Object를 생성하고, 생성된 Socket Object를 Get하는 로직을 구현한다.
해당 Script에는 Object를 Export하게 되며, Object 내에는 두 개의 Method가 포함된다. 하나는 init이라는 Field Name을 가진 Method이고, 나머지는 getIO라는 Field Name을 가진 Method이다.
이와 같이 정의된 두 Method를 통해서 app.js에서는 init으로 할당된 Function에 httpServer를 인자로 주어 호출하여 Socket Object를 생성하게 된다. 이렇게 생성된 Socket은 Singleton이기 때문에 다른 Script에서도 쉽게 접근 가능하다. 접근할 때는 getIO로 할당된 Function을 호출함으로써 기존에 생성되어 있는 Socket Object를 사용하게 된다.

Synchronizing POST Additions

생성된 Socket Object를 통해서 데이터의 변화를 Inform하기 위해선, 해당 Socket Object를 get한 후, emit()이나 broadcast() Method를 이용한다.
두 Method의 차이는 다음과 같다. emit()의 경우 현재 Socket에 연결되어 있는 모든 사용자에게 메세지를 보내게 되고, broadcast()의 경우 메세지를 보내는 당사자를 제외하고 모든 사용자에게 메세지를 보내게 된다.
emit() Method의 인자는 Event Name과 Inform하려는 데이터가 들어간 Object가 되겠다. Event Name은 정하기 나름이며, 데이터 역시 정하기 나름이다.
위와 같이 Backend에서 Socket에 밀어 넣을 데이터에 대해서 설정을 했다면, 해당 데이터를 밀어 넣었다는 것을 인지할 수 있도록 Frontend에서 socket.io-client로 생성된 Socket에 Event Listener를 달아줘야 한다. Socket에 달리는 Event Listener의 형태는 이전에 app.js에서 Socket을 httpServer로 생성했을 때 Event Listener를 달아서 Client의 Connection을 감지할 수 있도록 Callback Function을 두는 것과 같은 형태이다.
** 이와 같이 서로 다른 Client에 대한 동작을 확인하려고 할 때는 다른 Browser를 쓰면 된다. 이는 기기 자체가 다른 것으로 (즉, 각 Browser가 아예 다른 PC에서 작동하는 것으로) 인식하게 된다.
** Backend에서는 Frontend에서 원하는 데이터들로 보내줘야 정상적으로 작동한다. 이에 따라 Model에서 추가로 데이터를 넣어서 보내야할 때도 있는데, 이 때는 Spread Operator를 이용하도록 한다. Spread Operator를 이용할 때, Meta Data가 같이 딸려 들어가지 않도록 ._doc을 붙여서 Spread Operator를 이용한다.
** Mongoose에서 데이터를 받아올 때 populate() Method와 같이 세부 항목까지 모두 펼쳐서 보여주는 것도 가능하지만 해당 데이터들을 받아오고 sort() Method를 통해서 정렬을 하는 것도 가능하다. 사용 방법은 Official Document를 찾아보자.