Node.js에서 WebSockets를 사용하여 실시간 앱 만들기

이 튜토리얼은 브라우저와 서버 간의 양방향, 대화식 통신을 위해 Node.js에서 WebSockets를 사용하는 방법을 보여줍니다. 대시보드, 채팅 앱, 멀티플레이어 게임과 같은 빠른 실시간 애플리케이션에 필수적인 기술입니다.

Table of Contents

웹은 요청-응답 기반의 HTTP 메시지로 구성되어 있습니다. 사용자의 브라우저가 URL 요청을 보내고, 서버는 데이터를 응답합니다. 이로 인해 이미지, CSS, JavaScript 등에 대한 추가적인 브라우저 요청과 서버 응답이 발생할 수 있지만, 서버는 브라우저에 임의로 데이터를 보낼 수 없습니다.

롱 폴링 아작스 기술은 웹 애플리케이션이 실시간으로 업데이트되는 것처럼 보이게 할 수 있지만, 이 과정은 진정한 실시간 애플리케이션에는 너무 제한적입니다. 매 초마다 폴링하는 것은 어떤 시점에서는 비효율적이고, 다른 시점에서는 너무 느릴 수 있습니다.

브라우저에서 초기 연결을 수행한 후, 서버 전송 이벤트는 서버가 언제든지 메시지를 보낼 수 있는 표준(스트리밍) HTTP 응답입니다. 그러나 채널은 단방향이며, 브라우저는 메시지를 다시 보낼 수 없습니다. 빠른 양방향 통신을 위해서는 웹소켓이 필요합니다.

웹소켓 개요

용어 웹소켓ws:// 또는 보안 암호화된 wss://를 통한 TCP 통신 프로토콜을 가리킵니다. HTTP와는 다르지만, 비웹 트래픽을 차단하는 곳에서도 작동하도록 포트 80 또는 443에서 실행할 수 있습니다. 2012년 이후 출시된 대부분의 브라우저는 웹소켓 프로토콜을 지원</diy11합니다.

전형적인 실시간 웹 애플리케이션에서는 웹 콘텐츠(HTML, CSS, JavaScript, 이미지 등)를 제공하는 웹 서버와 양방향 통신을 처리하는 웹소켓 서버가 적어도 하나씩 필요합니다.

브라우저는 여전히 서버에 초기 WebSocket 요청을 보내어 통신 채널을 연다. 그 후 브라우저나 서버 중 어느 쪽이든 해당 채널에 메시지를 보낼 수 있으며, 이는 상대 기기에서 이벤트를 발생시킨다.

다른 연결된 브라우저와 통신

초기 요청 이후, 브라우저는 WebSocket 서버로 메시지를 보내고 받을 수 있다. WebSocket 서버는 연결된 어떤 클라이언트 브라우저든 메시지를 보내고 받을 수 있다.

P2P 통신은 불가능하다. BrowserA는 같은 기기에서 실행 중이고 같은 WebSocket 서버에 연결되어 있더라도 BrowserB에게 직접 메시지를 보낼 수 없다! BrowserA는 서버에 메시지를 보내고 필요에 따라 다른 브라우저로 전달되기를 바라야 한다.

WebSocket 서버 지원

Node.js는 아직 기본 WebSocket 지원을 제공하지 않으나, 곧 추가될 소식이 있다! 이 글에서는 타사 ws 모듈을 사용하지만, 수십 가지 다른 모듈도 있다.

DenoBun JavaScript 런타임에는 내장 WebSocket 지원이 제공된다.

WebSocket 라이브러리는 PHP, Python, Ruby 등 다양한 런타임에서 사용할 수 있습니다. 또한 PusherPubNub과 같은 타사 SaaS 옵션은 호스팅된 WebSocket 서비스도 제공합니다.

WebSocket 데모 빠른 시작

채팅 앱은 WebSocket 데모의 Hello, World!입니다. 따라서 사과드립니다:

  1. 창의적이지 못함. 그렇지만 채팅 앱은 개념을 설명하기에 좋습니다.

  2. 완전히 호스팅된 온라인 솔루션을 제공하지 못함. 난 익명의 메시지 스트림을 감시하고 관리해야 하는 것보다는 덜 좋다!

GitHub에서 node-wschat 저장소를 복제하거나 다운로드:

git clone https://github.com/craigbuckler/node-wschat

Node.js 종속성 설치:

cd node-wschat
npm install

채팅 애플리케이션 시작:

npm start

다양한 브라우저나 탭에서 http://localhost:3000/을 열어보세요 (쿼리 문자열에서 채팅 이름을 정의할 수도 있습니다. 예를 들어 http://localhost:3000/?Craig). 한 창에 무언가를 입력하고 SEND를 누르거나 Enter를 누르면 연결된 모든 �라우저에 나타납니다.

Node.js 코드 개요

Node.js 애플리케이션의 index.js 진입 파일은 두 개의 서버를 시작합니다:

  1. 클라이언트 측 HTML, CSS 및 JavaScript를 사용하여 단일 페이지를 제공하는 EJS 템플릿으로 http://localhost:3000/에서 실행되는 Express 앱입니다. 브라우저 JavaScript는 WebSocket API를 사용하여 초기 연결을 수행한 다음 메시지를 보내고 받습니다.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // WebSocket 서버
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // 클라이언트 연결
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // 수신된 메시지
      socket.on('message', (msg, binary) => {
    
        // 모든 클라이언트에게 방송
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // 종료됨
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

The Node.js ws 라이브러리:

  • 브라우저가 연결하려고 할 때 "connection" 이벤트를 발생시킵니다. 핸들러 함수는 해당 개별 기기와 통신하는 데 사용되는 socket 객체를 받습니다. 연결 기간 동안 유지되어야 합니다.

  • 브라우저가 메시지를 보낼 때 socket "message" 이벤트를 발생시킵니다. 핸들러 함수는 메시지를 모든 연결된 브라우저(보낸 브라우저 포함)에게 다시 방송합니다.

  • 브라우저가 연결을 끊을 때 socket "close" 이벤트를 발생시킵니다. — 일반적으로 탭이 닫히거나 새로 고침될 때입니다.

클라이언트 측 자바스크립트 코드 개요

애플리케이션의 static/main.js 파일은 wsInit() 함수를 실행하고, 웹소켓 서버의 주소(페이지의 도메인에 템플릿의 HTML 페이지에 정의된 포트 값을 더한 것)를 전달합니다:

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// 웹소켓 통신 처리
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // 서버에 연결
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

브라우저가 웹소켓 서버에 연결되면 open 이벤트가 발생합니다. 핸들러 함수는 sendMessage()를 호출하여 채팅방에 입장했습니다 메시지를 보냅니다:

// 메시지 전송
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

sendMessage() 함수는 HTML 폼에서 사용자의 이름과 메시지를 가져오지만, 전달된 setMsg 인수에 의해 메시지가 대체될 수 있습니다. 값들은 JSON 객체로 변환되어 ws.send() 메서드를 사용하여 웹소켓 서버로 전송됩니다.

웹소켓 서버는 들어오는 메시지를 수신하여 "message" 핸들러(위에서 보았듯이)를 트리거하고 모든 브라우저에 다시 방송합니다. 이로 인해 각 클라이언트에서 "message" 이벤트가 발생합니다:

// 메시지 수신
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

핸들러는 이벤트 객체의 .data 속성에서 전송된 JSON 데이터를 수신합니다. 함수는 이를 자바스크립트 객체로 구문 분석하고 채팅 창을 업데이트합니다.

마지막으로, 양식의 "submit" 핸들러가 트리거될 때마다 sendMessage() 함수를 사용하여 새 메시지가 전송됩니다:

// 폼 제출
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

오류 처리

WebSocket 통신이 실패할 때 "error" 이벤트가 발생합니다. 이는 서버에서 처리할 수 있습니다:

// 서버에서 WebSocket 오류 처리
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

또는 클라이언트에서:

// 클라이언트에서 WebSocket 오류 처리
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

클라이언트만이 new WebSocket() 생성자를 다시 실행하여 연결을 재설정할 수 있습니다.

연결 종료

어느 기기든 언제든지 WebSocket을 사용하여 연결을 닫을 수 있습니다. .close() 메서드를 사용합니다. 옵션으로 code 정수와 reason 문자열(최대 123바이트) 인수를 제공할 수 있으며, 이는 연결이 끊어지기 전에 다른 기기에 전송됩니다.

고급 WebSockets

Node.js에서 WebSocket을 관리하는 것은 쉽습니다: 한 기기는 .send() 메서드를 사용하여 메시지를 보내고, 이는 다른 기기에서 "message" 이벤트를 발생시킵니다. 각 기기가 이러한 메시지를 생성하고 응답하는 방법은 더 어려울 수 있습니다. 다음 섹션에서는 고려해야 할 문제에 대해 설명합니다.

WebSocket 보안

WebSocket 프로토콜은 인증이나 인가를 처리하지 않습니다. 들어오는 통신 요청이 웹 브라우저나 웹 애플리케이션에 로그인한 사용자로부터 시작되었는지 보장할 수 없습니다. 특히 웹 서버와 WebSocket 서버가 다른 기기에 있을 때 많이 발생합니다. 초기 연결은 쿠키와 서버 Origin을 포함하는 HTTP 헤더를 받지만, 이러한 값을 위조할 수 있습니다.

다음 기술을 사용하면 인증된 사용자로부터만 WebSocket 통신을 제한할 수 있습니다:

  1. 초기 WebSocket 요청을 보내기 전에 브라우저는 HTTP 웹 서버와 통신합니다(Ajax를 사용하여 가능성이 있음).

  2. 서버는 사용자의 자격 증명을 확인하고 새로운 인증 티켓을 반환합니다. 티켓은 일반적으로 사용자 ID, IP 주소, 요청 시간, 세션 만료 시간 등을 포함하는 데이터베이스 레코드를 참조합니다.

  3. 브라우저는 초기 핸드셰이크에서 WebSocket 서버로 티켓을 전달합니다.

  4. WebSocket 서버는 티켓을 확인하고 IP 주소, 만료 시간 등을 확인한 후 연결을 허용합니다. 티켓이 유효하지 않으면 WebSocket .close() 메서드를 실행합니다.

  5. WebSocket 서버는 사용자 세션이 계속 유효한지 확인하기 위해 주기적으로 데이터베이스 레코드를 재확인해야 할 수 있습니다.

중요한 것은 들어오는 데이터를 항상 검증하는 것:

  • HTTP와 마찬가지로 WebSocket 서버는 SQL 인젝션 및 기타 공격에 취약합니다.

  • 클라이언트는 DOM에 원시 값을 삽입하거나 JavaScript 코드를 실행해서는 안 됩니다.

별도의 WebSocket 서버 인스턴스 vs 다중 인스턴스

온라인 멀티플레이어 게임을 고려해 보십시오. 이 게임에는 많은 우주가 있으며, 각각 별도의 게임 인스턴스를 실행하고 있습니다: universeA, universeB, universeC입니다. 플레이어는 단일 우주에 연결합니다:

  • universeA: player1, player2, player3가 참여했습니다.
  • universeB: player99가 참여했습니다.

다음과 같이 구현할 수 있습니다:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. 모든 게임 우주에 대해 단일 WebSocket 서버를 사용합니다.

    이 방식은 리소스를 덜 사용하고 관리하기 쉬우나, WebSocket 서버는 각 플레이어가 어떤 우주에 참여했는지 기록해야 합니다. player1이 어떤 행동을 할 때, 이를 player2player3에게만 방송해야 하며, player99에게는 방송하지 않아야 합니다.

여러 WebSocket 서버

예시 채팅 애플리케이션은 수백 명의 동시 사용자를 감당할 수 있지만, 인기와 메모리 사용량이 주요 임계점을 넘어서면 충돌합니다. 결국 추가 서버를 추가하여 수평적으로 확장해야 할 것입니다.

각 WebSocket 서버는 자신에게 연결된 클라이언트만 관리할 수 있습니다. 브라우저에서 serverX로 보내진 메시지는 serverY에 연결된 사용자들에게 방송될 수 없습니다. 백엔드 게시자-구독자(pub-sub) 메시징 시스템을 구현해야 할 수도 있습니다. 예를 들어:

  1. WebSocket serverX가 모든 클라이언트에게 메시지를 보내고 싶다면, 이 메시지를 pub-sub 시스템에 게시합니다.

  2. pub-sub 시스템에 가입한 모든 WebSocket 서버(포함 serverX)는 새 메시지 이벤트를 받습니다. 각 서버는 메시지를 처리하고 연결된 클라이언트에게 방송할 수 있습니다.

WebSocket 메시징 효율성

WebSocket 통신은 빠르지만, 서버는 모든 연결된 클라이언트를 관리해야 합니다. 특히 멀티플레이어 액션 게임을 구축할 때 메시지의 구성과 효율성을 고려해야 합니다:

  • 모든 클라이언트 기기에서 플레이어의 동작을 어떻게 동기화합니까?

  • 만약 player1player2와 다른 위치에 있다면, player2에게 볼 수 없는 액션에 대한 정보를 보내야 합니까?

  • 네트워크 대기 시간을 어떻게 대처합니까 — 또는 통신 지연은? 빠른 기계와 연결을 가진 사람이 불공평한 우위를 점할 수 있습니까?

빠른 게임은 타협을 해야 합니다. 다른 사람들의 활동에 영향을 받는 일부 객체를 제외하고 로컬 기기에서 게임을 하는 것처럼 생각하세요. 모든 객체의 정확한 위치를 항상 보내는 대신, 게임은 종종 더 간단하고 덜 자주 메시지를 보냅니다. 예를 들어:

  • objectX가 pointX에 나타났습니다
  • objectY가 새로운 방향과 속도를 가지고 있습니다
  • objectZ가 파괴되었습니다

각 클라이언트 게임은 공백을 채웁니다. objectZ가 폭발할 때, 폭발이 각 기기에서 다르게 보일지라도 문제되지 않습니다.

결론

Node.js는 WebSockets를 처리하기 쉽게 만듭니다. 실시간 애플리케이션을 설계하거나 코딩하기 더 쉽게 만드는 것은 아니지만, 기술 자체가 방해하지 않습니다!

주요 단점:

  • WebSockets는 자체 별도의 서버 인스턴스를 필요로 합니다. Ajax Fetch() 요청과 서버-전송 이벤트는 이미 운영 중인 웹 서버에서 처리할 수 있습니다.

  • WebSocket 서버는 자체 보안 및 인증 검사를 요구합니다.

  • 끊어진 WebSocket 연결은 수동으로 재설정해야 합니다.

하지만 그것이 당신을 打退하지 않습니다!

WebSocket과 Server-Sent Events를 사용한 실시간 앱에 대한 질문 및 답변 (FAQ)

WebSockets는 HTTP와 비교할 때 성능 및 기능 측면에서 어떻게 다릅니까?

WebSockets는 단일 TCP 연결을 통한 전체 중복 통신 채널을 제공하므로 데이터를 동시에 보내고 받을 수 있습니다. 이는 각 요청이 새 연결을 필요로 하는 HTTP에 비해 상당한 개선입니다. WebSockets는 실시간 데이터 전송을 허용하므로 채팅 앱이나 실시간 스포츠 업데이트와 같이 즉각적인 업데이트가 필요한 애플리케이션에 적합합니다. 반면 HTTP는 무상태이며 각 요청-응답 쌍은 독립적이므로 실시간 업데이트가 필요하지 않은 애플리케이션에 더 적합할 수 있습니다.

WebSocket 연결의 수명주기를 설명할 수 있나요?

WebSocket 연결의 수명주기는 HTTP 연결을 WebSocket 연결로 업그레이드하는 핸드셰이크로 시작됩니다. 연결이 설정되면 클라이언트와 서버 간에 데이터를 주고받을 수 있으며 양쪽 중 어느 한 쪽이든 연결을 종료하기로 결정할 때까지 계속됩니다. 클라이언트나 서버가 종료 프레임을 보내고 다른 쪽이 종료 프레임을 확인하여 연결을 종료할 수 있습니다.

Android 애플리케이션에서 WebSockets를 구현하려면 어떻게 해야 하나요?

Android 애플리케이션에서 WebSockets 구현은 WebSocket 서버에 연결할 수 있는 WebSocket 클라이언트를 생성하는 것으로 이루어집니다. OkHttp나 Scarlet과 같은 라이브러리를 사용하여 이를 수행할 수 있습니다. 클라이언트를 설정한 후 서버에 연결을 열고 메시지를 보내고 받고, 연결 열림, 메시지 수신, 연결 종료와 같은 다양한 이벤트를 처리할 수 있습니다.

Server-Sent Events란 무엇이며 이것이 WebSockets와 어떻게 비교되나요?

Server-Sent Events(SSE)는 서버가 HTTP를 통해 클라이언트에 업데이트를 푸시할 수 있게 하는 표준입니다. WebSocket과 달리 SSE는 단방향이므로 서버에서 클라이언트로 데이터를 보낼 수만 있습니다. 이로 인해 양방향 통신이 필요한 애플리케이션에는 적합하지 않지만 서버에서 업데이트만 필요한 애플리케이션에는 더 간단하고 효율적인 솔루션이 될 수 있습니다.

WebSockets과 Server-Sent Events의 일반적인 사용 사례는 무엇인가요?

WebSockets은 채팅 앱, 멀티플레이어 게임, 협업 도구와 같이 실시간, 양방향 통신이 필요한 애플리케이션에서 일반적으로 사용됩니다. 반면 Server-Sent Events는 서버의 실시간 업데이트가 필요한 애플리케이션에서 자주 사용됩니다. 이는 라이브 뉴스 업데이트, 주식 가격 업데이트 또는 장시간 실행 작업의 진행 보고서와 같은 애플리케이션입니다.

Spring Boot 애플리케이션에서 WebSocket 연결을 어떻게 처리할 수 있나요?

Spring Boot는 Spring WebSocket 모듈을 통해 WebSocket 통신을 지원합니다. @EnableWebSocket 어노테이션을 사용하여 WebSocket 지원을 활성화하고 WebSocketHandler를 정의하여 연결 수명 주기 및 메시지 처리를 처리할 수 있습니다. 또한 SimpMessagingTemplate을 사용하여 연결된 클라이언트에 메시지를 보낼 수 있습니다.

WebSocket을 사용할 때의 보안 고려 사항은 무엇인가요?

다른 웹 기술과 마찬가지로 WebSockets은 Cross-Site WebSocket Hijacking (CSWSH) 및 Denial of Service (DoS) 공격과 같은 다양한 보안 위협에 취약할 수 있습니다. 이러한 위험을 완화하려면 항상 보안 WebSocket 연결 (wss://)을 사용하고 모든 들어오는 데이터를 검증하고 정리해야 합니다. 인증 및 권한 부여 메커니즘을 사용하여 WebSocket 서버에 대한 액세스를 제어하는 것도 고려해야 합니다.

REST API와 WebSockets을 함께 사용할 수 있나요?

예, WebSockets을 REST API와 함께 사용할 수 있습니다. REST API는 상태 비저장 요청-응답 통신에는 좋지만 WebSockets은 실시간, 양방향 통신에 사용할 수 있습니다. 이는 채팅 앱이나 실시간 스포츠 업데이트와 같이 즉시 업데이트가 필요한 애플리케이션에 특히 유용합니다.

WebSocket 서버를 어떻게 테스트할 수 있나요?

WebSocket.org의 Echo Test나 Postman과 같은 WebSocket 서버를 테스트하기 위한 여러 도구가 있습니다. 이러한 도구를 사용하면 서버에 WebSocket 연결을 열고 메시지를 보내고 응답을 받을 수 있습니다. Jest나 Mocha와 같은 라이브러리를 사용하여 WebSocket 서버에 대한 자동화된 테스트를 작성할 수도 있습니다.

WebSocket과 Server-Sent Events의 한계는 무엇인가요?

WebSocket과 Server-Sent Events은 실시간 통신을 위한 강력한 기능을 제공하지만 한계도 있습니다. 예를 들어, 모든 브라우저와 네트워크가 이러한 기술을 지원하지 않으며 적절하게 관리되지 않으면 상당한 자원을 소비할 수 있습니다. 또한 기존 HTTP 통신에 비해 구현 및 관리가 복잡할 수 있습니다.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/