socket.io 없이 ws모듈을 이용한 채팅방 구현 과정과 결과입니다.
ws모듈을 이용한 채팅방 구현
실시간 양방향 통신은 요즘 웹서비스에서 거의 필수입니다. 하다못해 배달 중인 음식의 위치까지 실시간으로 알려주는 시대에 살고 있습니다. web socket을 공부해보고 싶은 마음을 품고 미루던 찰나, node 강의에서 web socket 프로토콜과 ws모듈, socket.io를 소개했습니다. 제가 듣는 node 강의는 node 생태계 전체를 훑는 강의기에 깊은 내용보다는 이런 게 있다 정도의 설명이기에 스스로 예제를 만들며 학습했고 그 경험을 기록하고 공유할까 합니다.
들어가기 전에
- 이 글은 socket.io없이 ws모듈만을 사용하여 채팅방을 구현한 과정의 기록입니다.
- socket.io를 이용한 채팅방 구현은 추후 다른 글로 작성하겠습니다.
- 전체 코드는 깃헙에서 확인하실 수 있습니다. ( https://github.com/cleamer/node-study/tree/main/websocket/ws-module)
ps. "이런저런게 있다"를 듣고 필요한 부분은 스스로 깊게 공부하기 위해서 이 강의를 들은겁니다. 강의에 대한 부정적인 의도로 표현한 문장은 아닙니다.
ws 모듈
1. 왜 socket.io가 아닌 ws모듈 만 사용했는가?
socket.io 역시 ws 모듈을 사용합니다.(https://github.com/socketio/engine.io/blob/master/package.json#L43) express를 공부하기 전 http모듈을 사용해 서버를 띄워보듯 기본을 해봐야 그 위에 얹힌 편리한 도구들이 어떻게 작동하는지 이해가 됩니다. 또, html, css, js로 웹사이트를 만들어보지 않고 react를 먼저 배웠다면 어떻게 렌더링 되는지 virtual DOM의 장단점은 무엇인지 이해하기 어렵습니다. socket.io도 같다고 생각합니다. String만 주고받을 수 있는 ws모듈에서 어떻게 namespace,room 등 편리한 기능을 구현했는지 ws모듈을 사용해보지 않았다면 원래 web socket 프로토콜에서 제공되는 기능이라 생각하기 쉽습니다. 그래서 먼저 ws모듈만을 이용해 채팅방을 구현했고 다음 글에서 socket.io를 이용해 채팅방을 구현할 계획입니다.
2. 기능이 별로 없는 ws모듈
ws 모듈의 컨셉에 대해 document를 읽고 제가 이해한 대로 설명해 보겠습니다. ws모듈의 컨셉은 크게 두 가지를 알면 이해하기 쉬워집니다. webSocketServer와 webSocket입니다.
2.1. WebSocketServer
WebSocketServer의 인스턴스(이하 wss)는 하나의 포트를 갖는 서버입니다. 여기서 포트는 http포트와 공유할 수 있습니다. wss는 여러 클라이언트 혹은 다른 서버와 소켓으로 연결됩니다.
2.2. WebSocket
WebSocket의 인스턴스(이하 소켓)는 하나의 web socket 프로토콜 커넥션입니다. 제가 이해한 바로는 소켓은 입구와 출구가 하나인 터널입니다. 여러 명이 채팅방에 있다고 해서 하나의 소켓에 모두가 접속한 것은 아닙니다.
client1, 2, 3이 같은 채팅방에 속해있고 1이 메시지를 전송했다고 가정해보겠습니다. 1의 메세지를 패킷에 담아 소켓으로 연결된 wss에 전송합니다. wss는 받은 메세지를 전송자 1과 같은 방 안에 있는 client(2와 3)를 찾아 소켓을 통해 에게 전달합니다. 이때 ws모듈은 메세지를 누구에게 보낼 것인지 즉 라우팅을 직접 구현해야 합니다.
채팅방 구현
ws모듈을 사용하여 채팅방을 구현하기 어려운 이유는 위 그림처럼 wss에서 누구에게 보낼지를 직접 컨트롤해줘야 합니다. 이를 쉽게 하기 위해서는 채팅 방마다 다른 서버를 생성하면 됩니다. 아무 채팅방에도 접속되지 않은 사용자(이하 home), 1번 채팅방에 접속되어 있는 사용자, 등등 사용자 그룹마다 서버를 다르게 띄우면 구현이 쉬워집니다. 하지만 저는 연습도 할 겸 하나의 wss로 client를 구분하여 원하는 client에게 메시지를 전송하는 방법으로 구현했습니다. 따라서 이 글의 핵심은 하나의 wss에서 메세지 라우팅을 어떻게 구현했는가입니다.
거창하게 말했지만 사실 별건 없습니다. 제가 생각한 방법은 이렇습니다. 먼저 ws프로토콜의 uri를 이용하여 주고받는 데이터에 따라 소켓의 종류를 분류합니다(웹소켓 서버를 api서버로 사용합니다.). ws:localhost:8003/rooms로 연결된 소켓은 실시간 생성된 방 정보를 주고받는 소켓, ws:localhost:8003/chat/:roomId로 연결된 소켓은 실시간 채팅을 주고 받는 소켓으로 분류합니다. 그 뒤 데이터를 전송할 때 이들을 구분하기 위해 소켓에 locaiton프로퍼티를 생성해 소켓이 어디에 속했는지를 알 수 있도록 그루핑 했습니다. 데이터가 wss로 들어오면 uri에 의해 wss에서 전송하는 데이터 내용이 달라지고 wss의 모든 소켓의 location을 조회해 전송하는 소켓이 달라지는 방식입니다.
말보다는 코드가 이해하기 쉬우실 겁니다.
wss에서 라우팅 하는 코드입니다. 차근차근 설명해보겠습니다.
wss에 커넥션이 생기면 커넥션이 요청된 url을 확인해 소켓에 location을 부여합니다. /rooms일 경우 해당 소켓은 현재 생성된 방 목록을 주고받는 소켓이며 location에는 index가 부여됩니다. /rooms 커넥션이 연결되면 현재 생성되어있는 방들의 정보를 전달해줍니다.
/chat/:roomId로 커넥션이 연결되면 location에 roomId를 부여하고 소켓에 메시지 이벤트를 등록합니다. 소켓을 통해 메시지가 오면 자신을 제외하고 같은 채팅방 안의 커넥션이 열려있는 소켓들에게 메시지를 전달합니다.
채팅방이 새로 생성될 때 새로 생성된 방을 index페이지에 있는 커넥션이 연결된 소켓들에 전송해줍니다. 처음 개발 시 console.log로 clients.forEach의 client를 출력했을 때 자기 자신이 나오지않아 wss에 연결된 소켓의 수가 하나 적어 의아했으나 생각해보니 자신은 wss와 소켓을 연결하고 있지 않았습니다.
결과
파이어폭스, 크롬으로 보통 창과 시크릿 창을 열어 총 4명의 client로 테스트해보았습니다.
방이 새로 생성되면 app.js의 app.post('newroom')에 의해 생성한 사용자는 생성된 방으로 redirect 되고 index페이지에 있는 사용자는 실시간으로 방 목록이 업데이트됩니다.
본인이 보낸 메시지는 본인에게 ws프로토콜로 전송되지 않습니다. 본인이 보낸 메세지 즉, 오른쪽에 뜨는 메세지는 vanillaJS로 브라우저에서 직접 넣어준 것입니다. 소켓을 통해 받은 메세지는 왼쪽에 뜨도록 했습니다. 이 부분은 websocket/ws-module/public/chat.js에서 확인하실 수 있습니다.
다른 채팅방을 생성해 메시지를 보내보면 같은 채팅방에 있는 사람끼리만 소통할 수 있는 것을 확인할 수 있습니다.
다시 방에 접속하면 원래 방에 있던 사람과 통신할 수 있는 것을 확인 할 수 있습니다.
한계점
먼저 대화 내용을 저장하지 않았기 때문에 채팅방에 나갔다가 들어오면 내용이 모두 사라져 있습니다. 대화 내용은 작성자, ws.location, 전송시간 정보를 ws.onmessage에서 db 저장하면 채팅방에 입장했을 때 db에서 채팅 내용을 불러와 시간 순서에 맞게 배열해주면 될 것 같습니다. 추후 업데이트하거나 socket.io로 채팅 서비스를 만들 때 구현해 보겠습니다. 구현하면 이 글에 링크 달도록 하겠습니다.
두 번째로 작성자를 표시하지 않습니다. 처음에 간단히 ws모듈로 로직만 구현할 생각으로 시작한 토이 프로젝트라 기능을 많이 추가하지 않았습니다. 이 부분도 추후 업데이트하거나 socket.io를 사용해 채팅 서비스를 만들 때 추가하겠습니다.
정리
처음에는 socket과 webSocketServer 개념이 정리가 잘 안 됐는데 채팅방을 구현하며 콘솔에 객체를 찍어보고 깃헙을 읽으며 컨셉이 어느 정도 이해되었습니다. 구현하는 시간보다 ws모듈의 컨셉을 이해하고 클라이언트와 네트워킹에서 시간을 많이 쓴것같습니다. 다음번에는 socket.io를 사용하며 ws모듈과 비교해보겠습니다. 또 로그인, 채팅 내용저장 도 같이 구현해 보겠습니다.
'Node.js' 카테고리의 다른 글
[Node] __dirname is not defined in ES module scope (0) | 2022.03.17 |
---|---|
[Nodejs] passport.js 소스코드 까보기 (0) | 2022.02.27 |