[WebSocket] ❷ 메아리 애플리케이션 만들기

이 글은 [Node.js 백엔드 개발자 되기]에서 발췌했습니다.
골든래빗 출판사

웹소켓(WebSocket)은 하나의 TCP 컨넥션으로 서버와 클라이언트 간에 양방향 통신을 제공하는 프로토콜입니다. 본문에서는 웹소켓이 무엇인지 알아보고 Node.js와 웹소켓를 사용하여 메아리 애플리케이션을 만들어보겠습니다.

메아리 애플리케이션 만들기

동작 방법을 알아보았으니, 이제 웹소켓을 사용해 간단한 애플리케이션을 만들어봅시다. 클라이언트에서 메시지를 보내면 서버에서 같은 메시지를 반환하는 프로그램입니다. Node.js와 웹소켓만 사용해서 딱 3가지만 차례대로 진행하면 됩니다.

  1. ws패키지설치하기
  2. 서버 측 구축하기(server.js 파일 작성 및 서버 구동)
  3. 클라이언트 측 구현하기(index.html 파일 작성)

3. 메아리 애플리케이션 만들기

3.1 ws 패키지 설치하기

To do

01 ws는 Node.js에서 웹소켓 서버를 구동하는 라이브러리입니다. 먼저 프로젝트 디렉터리를 생성하고 패키지를 설치합시다.

 

$ mkdir chapter13
$ cd chapter13
$ mkdir echo-websocket $ cd echo-websocket
$ npm install ws

 

여기까지 진행하면 [echo-websocket] 디렉터리 아래에 package.json과 package-lock.json 파일, 그리고 [node_modules] 디렉터리가 생길 겁니다. 다음으로 웹소켓 서버를 작성하겠습니다.

 

3.2 서버 측 구축하기 : server.js 파일 작성 및 서버 구동

ws 패키지를 설치했으니 이제 웹소켓 서버를 Node.js를 사용해 구동할 수 있습니다. 웹소켓은 서버와 클라이언트가 양방향으로 통신하는 프로토콜이므로 서버와 클라이언트를 모두 작성해야 합니다. 서버는 Node.js를, 클라이언트는 웹브라우저를 사용합시다.

 

To do

01 먼저 서버를 구동하기 위해 server.js 파일을 생성하고 다음과 같이 작성해봅시다.

 

▼ 메아리 애플리케이션 웹소켓 서버

const WebSocket = require('ws');     // ws 패키지 임포트
const server = new WebSocket.Server({ port: 3000 }); // ❶ 서버 인스턴스 생성

server.on('connection', ws => {      // ❷ 서버 인스턴스 생성
  ws.send('[서버 접속 완료!]');         // 클라언트 접속 시 클라이언트로 메시지를 보냄

  // ❸ 클라이언트에서 메시지가 수신된 경우의 이벤트 핸들러
  ws.on('message', message => {    
    ws.send(`서버로부터 응답: ${message}`);
  });
  
  ws.on('close', () => {             // ❹ 클라이언트 접속 종료 이벤트
    console.log('클라이언트 접속 해제');
  });
});

 

ws의 Server( ) 함수를 사용해 서버 인스턴스를 생성하고 server 변수에 저장합니다. server는 WebSocketServer 클래스의 인스턴스입니다. ❷ 웹소켓 서버(server 변수)의 on( ) 함수는 이벤트를 받는 함수입니다. 첫 번째 인수로 이벤트 유형을 받습니다. ‘connection’은 클라이언트가 접속 시 발생하는 이벤트입니다. 두 번째 인수로 이벤트 발생 시 실행할 콜백 함수를 인수로 설정합니다. 콜백 함수의 인수로 ws를 받는데 WebSocket 클래스의 인스턴스입니다.

 

▼ WebSocketServer의 이벤트

 

ws.on( ) 함수는 클라이언트에서 이벤트가 발생할 때 실행하는 함수입니다. WebSocketServer의 on() 함수처럼 첫 번째는 이벤트 타입, 두 번째는 콜백 함수를 인수로 사용합니다. ws.on(‘message’)는 클라이언트로부터 메시지가 서버로 발송되었을 때 실행합니다.

 

▼ WebSocket의 이벤트

 

❹ ws.on(‘close’,콜백함수)는 클라이언트가 접속을 종료 했을 때 실행합니다.

 

3.3 클라이언트 측 구현하기 : client.html 파일 작성

To do

01 클라이언트로 사용할 index.html 파일에 웹소켓 연결을 하고 메시지를 주고받을 수 있도록 작성해봅시다.

개인적으로 백엔드 개발자도 기본적인 HTML과 CSS 문법을 읽고 쓸 줄 알면 더 좋다고 생각하지만 반드시 그래야만 하는 것은 아니므로 익숙하지 않다면 어떻게 흘러가는지 주석을 보면서 흐름만 판단해도 됩니다.

 

▼ 애플리케이션 웹소켓 클라이언트

<style>
/* ❶ 메시지를 꾸미는 CSS 코드 */
  .message {
    width: 300px;
    color: #fff;
    background-color: purple;
    margin-top: 5px;
    padding: 5px;
  }
</style>
<body>
  <!-- ❷ 메시지를 적을 텍스트 영역 -->
  <textarea id="message" name="message" cols="50" rows="5"></textarea>
  <br />
  
  <!-- ❸ 버튼 -->
  <button onclick="sendMessage()">전송</button>
  <button onclick="webSocketClose()">종료</button>
  <div id="messages"></div>
</body>

<script>
// ❹ 웹소켓 연결
const ws = new WebSocket("ws://localhost:3000");

// ❺ send 함수로 메시지 발송
function sendMessage() {
  ws.send(document.getElementById("message").value);
}

// ❻ 웹소켓연결종료
function webSocketClose() {
  console.log("종료누름");
  ws.close(1000, "정상종료");
}

// ❼ WebSocket의 open 이벤트 핸들러
ws.onopen = function () {
  console.log("클라이언트 접속 완료!");
};

// ❽ WebSocket의 message 이벤트 핸들러. 서버에서 메시지 수신 시 실행
ws.onmessage = function (event) {
  // ❾ 엔터 키를 <br /> 태그로 변경
  let message = event.data.replace(/(\r\n|\n|\r)/g, "<br />");
  let el = document.createElement("div"); // ❿ div 태그 생성
  el.innerHTML = message;      // ⓫ <div>{메시지}</div>값이 됨. HTML로 파싱
  el.className = "message";    // ⓬ <div class='message'>{메시지}</div>값이 됨
  document.getElementById("messages").append(el); // ⓭ messages 요소에 추가
}

// ⓮ 접속종료시실행
ws.onclose = function (e) {
  console.log("종료");
  document.getElementById("messages").append("서버 접속 종료");
}
</script>

 

❶ 메시지 영역을 꾸미는 CSS 코드입니다. 가로 폭을 300픽셀, 글자색을 흰색, 배경색을 보라색으로 지정하고, 마진과 패딩을 추가했습니다. ❷ 메시지를 넣는 영역은 textarea 태그를 사용했습니다. id는 getElementById를 사용하는 데 필요합니다. getElementById는 주어진 id값으로 해당 요소를 찾아서 해당 엘리먼트 객체를 가져오는 데 사용합니다. cols는 가로 길이는 50자, rows는 5줄 크기를 가지는 텍스트 영역이 생깁니다. ❸ 버튼으로는 메시지 전송, 종료 버튼이 있습니다. onclick에 각각 sendMessage( ), webSocketClose( ) 함수를 바인딩해두었습니다. ❹ 웹소켓 연결은 new WebSocket(서버 주소)를 하면 맺어집니다. 웹브라우저에는 웹소켓 기능이 이미 있기 때문에 별도로 라이브러리가 추가하지 않아도 됩니다. 반환값으로 WebSocket의 인스턴스가 돌아오는데 해당 값을 ws 변수에 저장합니다.

❺ 메시지를 서버로 발송하는 함수입니다. 웹소켓 인스턴스 ws의 send( ) 함수를 사용합니다. 텍스트 영역의 값을 서버로 송신합니다. 서버에서는 웹소켓의 message 이벤트가 발생합니다. ❻ 웹소켓 연결 종료 시 사용하는 함수입니다. 종료 버튼에 바인딩되어 있으며 종료 버튼 클릭 시 웹소켓 연결을 종료합니다. ❼ 서버와 연결되면 발생하는 이벤트인 open의 이벤트 핸들러입니다. 서버와 연동되면 실행됩니다. ‘ 클라이언트 접속 완료’ 문구가 브라우저의 콘솔에 찍힙니다. ❽ 서버에서 메시지를 수신하면 발생하는 이벤트의 핸들러입니다.

❾ replace(/(\r\n|\n|\r)/g,”<br />”);은 텍스트 영역에서 enter 를 입력하면\r\n을 새 줄을 뜻하는 태그인 <br />로 변경하는 코드입니다.

❿ 서버에서 입력받은 값을 화면에 나타내려면 웹페이지이므로 HTML 태그로 표현해야 합니다. document.createElement(‘div’) 함수를 사용해 <div></div> 태그를 생성해 el에 저장합니다.

⓫ el은 <div></div>입니다. innerHTML 함수로 <div>와 </div> 사이에 태그를 넣을 수 있습니다. 값은 텍스트 영역에서 받은 메시지입니다. 빈 줄이 있다면 <br/>로 변경된 값입니다. 예를 들어 “안녕\r\n하세요”라는 문자열은 **<div>안녕<br />하세요<div>**라는 태그로 변경해 저장됩니다. ⓬ <div></div>에 클래스를 붙여줍니다. 클래스명은 message이므로 **<div class=’message’>{메시지}</div>**가 됩니다. ⓭ ❾~⓫에 걸쳐 만든 메시지를 HTML의 mesages라는 id를 가진 요소에 추가해줍니다. ⓮ 접속 종료 시 실행되는 코드입니다. 콘솔에는 ‘종료’가 찍히고 화면에는 서버 접속 종료가 찍힙니다.

 

3.4 테스트하기

만든 웹소켓 메아리 애플리케이션을 테스트해봅시다.

 

To do

01 우선 서버를 띄우겠습니다. 다음과 같이 서버를 실행합니다.

 

▼ 웹소켓 서버 실행

$ cd chapter13/echo-websocket 
$ node server.js

 

터미널에 아무 값도 안 올라오겠지만, 에러가 나면서 종료되는 게 아니라면 성공한 겁니다.

 

02 다음으로 브라우저에서 client.html을 실행해서 서버에 접속해봅시다. 다음과 같은 화면이 보이면 성공입니다.

 

 

화면에 “[서버 접속 완료!]” 문구가 있고, 콘솔에는 “ 클라이언트 접속 완료!”라는 문구가 있습니다. ‘[서버 접속 완료!]’는 서버에서 보내준 값입니다. WebSocketServer의 ‘connection’에서 ws.send(‘[서버 접속 완료!]’);를 실행해 클라이언트 측의 message 이벤트를 발생시켰습니다. “클라이언트 접속 완료!”는 클라이언에서 open 이벤트가 발생할 때 출력되는 값입니다.

 

03 개발자 도구의 네트워크 탭으로 들어가면 client.html이 있고 localhost가 있을 겁니다. localhost를 선택해줍시다.

그러면 오른쪽에 ⬇️ [서버 접속 완료!]라고 되어 있을 겁니다. ⬇️는 서버에서부터 메시지를 받았다는의미입니다. ⬆️는 아직 안 나왔습니다만, 클라이언트에서 서버로 메시지를 보낸다는 의미입니다.

이제부터 HTML 화면과 서버의 콘솔과 메시지 창을 모두 확인해주세요.

 

 

💡Tip: 개발자 도구는 크롬 기준 브라우저 오른쪽 상단의 버튼에서 → [도구 더보기] → [개발자 도구]를 선택해주면 보입니다.

 

04 텍스트 영역에 입력 후 [전송] 버튼을 클릭해 서버로 메시지를 보내봅시다.

 

 

웹소켓으로 서버로 메시지를 보내었고, 서버가 같은 메시지를 응답으로 주었습니다.

 

 

05 메시지가 잘 보내지고 받아지는 것을 확인했습니다. 이제 종료 버튼을 눌러서 연결을 끊어봅시다.

 

 

접속을 종료하면 서버로부터 종료 메시지를 받고 onclose( ) 함수에서 처리합니다. 서버의 터미널에서도 close 이벤트를 받아서 클라이언트 접속 해제를 띄워줍니다.

 

 

네트워크 탭에서 [콘솔] 탭으로 다시 돌아오면, 로그들이 나와있습니다. webSocketClose 함수를 실행 후, onclose 이벤트가 발생한 것을 알 수 있습니다.

 

 

06 접속을 끊고 메시지를 보내려면 어떻게 될까요? [전송] 버튼을 다시 눌러서 시도해봅시다. 웹소켓이 이미 종료되었거나 종료하는 중이라는 메시지인 “WebSocket is already in CLOSING or CLOSED state”가 보일 겁니다.

 

 

3.5 향후 과제 확인하기

이제 브라우저를 두 개 띄워봅시다. 원래의 창은 리프레시해서 최초 상태를 유지합시다. 각각 메시지를 전송하면 어떻게 될까요?

 

 

채팅 대화를 주고받을 수 있으면 좋을 것 같은데, 안타깝게도 따로따로 동작합니다. 이는 웹소켓의 경우 메시지를 브로드캐스팅(접속한 클라이언트에 각각 보내주는 것) 기능을 따로 구현해야 하기때문입니다. 앞서 설명드렸지만, 웹소켓은 데이터를 주고받을 수 있게 하는 것뿐이라서 웹소켓을 사용하는 경우 데이터 전송 이외의 부분은 모두 개발자가 개발해야 합니다.

그래서 웹소켓으로 된 애플리케이션을 만들 때 도움을 주는 sockjs, socket.io 같은 라이브러리구현체들이 있습니다. 이 중 대표적인 것이 socket.io이고, 1편에서 설명드린 것처럼 채팅방 기능이나 연결이 끊어졌을 때 재접속, 브로드캐스팅 기능을 제공해줍니다.

 

4. 웹소켓 사용하기 마무리하기

지금까지 웹소켓을 알아보고 메아리 애플리케이션을 만들어보았습니다.

웹소켓은 프로토콜입니다만, 데이터에 관련해서는 아무것도 정해둔 게 없어서 채팅 같은 정형화된 애플리케이션을 만드는 때 사용하는 하위 프로토콜들이 있습니다. 대표적으로 STOMP 프로토콜*이 있습니다. STOMP 프로토콜은 클라이언트/서버 간 전송할 메시지의 유형, 형식, 내용을 정의한 프로토콜로 텍스트 메시지 전송 기능 구현 시 많이 사용합니다.

웹브라우저에서의 동시 편집 기능이나, 리프레시 없이 현재 접속한 유저의 화면을 갱신하는 기능, 주식 사이트 같은 곳에서 실시간으로 데이터를 갱신 등에 사용해보기 바랍니다.

 

4.1 핵심 용어

  1. 웹소켓은 하나의 TCP 컨넥션으로 서버와 클라이언트 간에 양방향 통신을 제공하는 프로토콜입니다.
  2. socket.io는 서버와 클라이언트의 양방향 통신을 제공하는 라이브러리입니다. 웹소켓을 주로 사용하지만 롱폴링 방식을 사용해 웹소켓을 지원하지 않는 브라우저에서도 양방향 통신을 가능하게 해줍니다.
  3. 폴링은 클라이언트가 주기적으로 데이터를 가져오는 것을 의미합니다. 롱폴링은 서버 측에서 즉시 응답을 주지 않고 기다리다가 응답을 주거나 타임아웃 시에 응답을 줍니다. 서버의 응답을 받은 클라이언트가 즉시 다시 서버에 요청을 보내는 방식입니다.
  4. 멀티플렉싱은 컨넥션 하나를 논리적으로 나누어서 데이터를 원하는 채널에만 전송하는 기법입니다. socket.io의 네임스페이스 기능이 같은 기능입니다.
  5. 게이트웨이는 NestJS에서 웹소켓으로 전송되는 이벤트를 핸들링하는 클래스를 의미합니다. @WebStocketGateay( ) 데코레이터가 붙어 있습니다.
  6. 브로드캐스팅은 접속한 클라이언트 모두에게 메시지를 전송하는 것을 의미합니다. socket.broadcast( ) 메서드에서는 나 이외의 클라이언트에게 메시지를 전송합니다.

박승규


아직도 개발이 재미 있는 15년차 천상 개발자입니다. 웹 개발, 게임 백엔드 개발, 플랫폼 및 인프라 개발 등 다양한 영역을 경험했습니다. 현재는 카카오엔터테인먼트에서 백엔드 개발자로 일합니다.


현) 카카오엔터테인먼트 페이지 서비스 개발팀
전) 트리노드 (포코팡, 포코포코) 서버 개발자
전) NHN Japan 플랫폼 개발팀

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내