프로그래밍/Node.js

실시간 웹 애플리케이션 개발: WebSocket과 Socket.IO 완벽 가이드

shimdh 2025. 2. 19. 00:07
728x90

1. 실시간 애플리케이션 개요

1.1 실시간 애플리케이션이란?

실시간 애플리케이션은 사용자 간의 즉각적인 상호작용을 필요로 하는 소프트웨어를 의미합니다. 이러한 애플리케이션은 사용자의 행동에 대해 지연 없이 반응해야 하며, 전통적인 HTTP 요청-응답 모델보다 더 효율적이고 빠른 통신 방식을 필요로 합니다.

주요 특징:

  • 즉각적인 반응성: 사용자 입력 또는 서버 데이터 변경에 실시간에 가까운 반응을 제공합니다.
  • 양방향 통신: 클라이언트와 서버가 자유롭게 데이터를 주고받을 수 있어야 합니다.
  • 지속적인 연결: 한 번 연결이 맺어지면 연결을 유지하여 통신 효율성을 높입니다.
  • 효율적인 데이터 전송: 최소한의 오버헤드로 데이터를 전송하여 네트워크 자원을 절약합니다.

1.2 실시간 애플리케이션의 예시

다양한 분야에서 실시간 애플리케이션이 활용되고 있으며, 대표적인 예시는 다음과 같습니다.

  • 커뮤니케이션: 채팅 애플리케이션, 화상 회의 시스템
  • 엔터테인먼트: 온라인 게임, 라이브 스트리밍 플랫폼
  • 금융: 주식 거래 플랫폼, 실시간 경매 시스템
  • 협업: 협업 문서 편집기, 프로젝트 관리 도구
  • 기타: 실시간 위치 추적 앱, 스마트 홈 제어 시스템, 실시간 알림 시스템

2. WebSocket: 실시간 통신의 핵심 프로토콜

2.1 WebSocket 프로토콜 소개

WebSocket은 웹 환경에서 양방향 통신을 가능하게 해주는 프로토콜입니다. HTTP가 클라이언트의 요청에 서버가 응답하는 단방향 방식인 반면, WebSocket은 클라이언트와 서버 간에 지속적인 연결을 유지하여 양방향 데이터 교환을 지원합니다.

WebSocket의 주요 특징:

  • 양방향 통신: 클라이언트와 서버가 동시에 메시지를 주고받을 수 있습니다.
  • 지속적인 연결: 한 번 연결되면 계속 유지되므로 실시간 데이터 전송에 적합합니다.
  • 낮은 지연 시간: 빠른 데이터 전송 속도로 실시간에 가까운 반응성을 제공합니다.
  • 효율적인 데이터 전송: HTTP 헤더 정보가 줄어들어 네트워크 대역폭을 절약합니다.

2.2 WebSocket 활용 예시

WebSocket은 다양한 실시간 기능 구현에 활용될 수 있습니다.

  • 채팅 애플리케이션: 사용자가 보낸 메시지를 다른 사용자에게 즉시 전달합니다.
  • 온라인 게임: 플레이어의 행동을 실시간으로 동기화하고 반영합니다.
  • 주식 거래 플랫폼: 주식 가격 변동 정보를 실시간으로 업데이트합니다.
  • 실시간 경매 시스템: 입찰 정보를 실시간으로 업데이트하고 즉시 결과를 표시합니다.
  • 화상 회의 시스템: 실시간 오디오 및 비디오 스트리밍을 제공합니다.

2.3 Node.js에서 WebSocket 구현하기

Node.js 환경에서는 ws 패키지를 사용하여 WebSocket 서버를 쉽게 구축할 수 있습니다.

// ws 패키지 설치: npm install ws
const WebSocket = require('ws');

// WebSocket 서버 생성
const wss = new WebSocket.Server({ port: 8080 });

// 클라이언트 연결 이벤트 처리
wss.on('connection', (ws) => {
  console.log('Client connected.');

  // 클라이언트로부터 메시지 수신 이벤트 처리
  ws.on('message', (message) => {
    console.log(`Received message from client: ${message}`);
    // 모든 연결된 클라이언트에게 메시지 브로드캐스트
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(`User sent: ${message}`);
      }
    });
  });

    // 클라이언트에서 메시지 보낼 때 다른 클라이언트들에게 알림 추가
    ws.on('message', (message) => {
        wss.clients.forEach(client => {
            if (client !== ws && client.readyState === WebSocket.OPEN) { // 메시지를 보낸 클라이언트 제외, 연결 상태 확인
                client.send(`User sent: ${message}`);
            }
        });
    });

  // 에러 처리
  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });

  // 클라이언트 연결 종료 이벤트 처리
  ws.on('close', () => {
    console.log('Client disconnected.');
  });
});

// 서버 에러 처리
wss.on('error', (error) => {
    console.error('Server error:', error);
});

코드 설명:

  1. const WebSocket = require('ws');: ws 패키지를 불러와 WebSocket 기능을 사용합니다.
  2. const wss = new WebSocket.Server({ port: 8080 });: 8080 포트에서 WebSocket 서버를 생성합니다.
  3. wss.on('connection', (ws) => { ... });: 클라이언트가 서버에 연결될 때마다 이벤트를 처리합니다.
  4. ws.on('message', (message) => { ... });: 클라이언트로부터 메시지를 받을 때마다 이벤트를 처리합니다.
  5. wss.clients.forEach((client) => { ... });: 연결된 모든 클라이언트에게 메시지를 브로드캐스트합니다.
  6. ws.on('message', (message) => { ... });: 클라이언트에서 메시지 보낼 때 다른 클라이언트들에게 알림 추가합니다.
  7. ws.on('close', () => { ... });: 클라이언트 연결이 종료될 때 이벤트를 처리합니다.
  8. ws.on('error', (error) => { ... });: 클라이언트에서 에러 발생시 처리합니다.
  9. wss.on('error', (error) => { ... });: 서버에서 에러 발생시 처리합니다.

추가 예제:

  • 특정 클라이언트에게 메시지 보내기: 클라이언트 ID를 기반으로 특정 사용자에게 메시지를 전송합니다.

       wss.on('connection', (ws) => {
        const clientId = generateUniqueId(); // 고유 ID 생성 함수 필요
        ws.clientId = clientId; // 클라이언트 ID 저장
    
        ws.on('message', (message) => {
            const targetClientId = extractTargetClientId(message) // 메시지에서 대상 클라이언트 ID 추출 함수 필요
            if (targetClientId) {
               wss.clients.forEach((client)=>{
                 if (client.clientId === targetClientId && client.readyState === WebSocket.OPEN) {
                     client.send(`Private Message from ${ws.clientId}: ${message}`);
                 }
               })
            } else {
                wss.clients.forEach(client=>{
                    if(client !== ws && client.readyState === WebSocket.OPEN) {
                         client.send(`${ws.clientId}: ${message}`);
                    }
                })
            }
    
        })
        })
  • 채팅 기능 개선: 사용자 이름과 함께 메시지를 전송하고, 사용자 연결/종료 이벤트를 처리합니다.

       wss.on('connection', (ws) => {
          ws.on('message', (message)=>{
            try{
                const parsedMessage = JSON.parse(message); //JSON 형식으로 메시지 받기
                if(parsedMessage.type === 'username') {
                    ws.username = parsedMessage.username;
                } else if (parsedMessage.type === 'message') {
                    wss.clients.forEach(client=>{
                        if(client !== ws && client.readyState === WebSocket.OPEN) {
                             client.send(JSON.stringify({
                                type: 'message',
                                sender: ws.username,
                                text: parsedMessage.text,
                            }))
                        }
                    })
                }
    
            } catch (e) {
               console.error('Error parsing message:', e)
            }
          })
       })

3. Socket.IO: WebSocket을 활용한 편리한 추상화

3.1 Socket.IO 소개

Socket.IO는 WebSocket을 기반으로 더 편리하고 강력한 기능을 제공하는 JavaScript 라이브러리입니다. WebSocket보다 더 높은 수준의 API를 제공하며, 폴백 기능을 통해 WebSocket을 지원하지 않는 브라우저에서도 실시간 통신을 가능하게 합니다.

Socket.IO의 주요 특징:

  • WebSocket 추상화: WebSocket API를 보다 쉽게 사용할 수 있도록 제공합니다.
  • 폴백 지원: WebSocket을 지원하지 않는 브라우저를 위해 Long Polling 등 다양한 전송 방식을 지원합니다.
  • 네임스페이스: 여러 개의 채널을 만들어 클라이언트와 서버 간의 통신을 분리합니다.
  • 룸: 특정 사용자 그룹 간에만 메시지를 주고받을 수 있도록 합니다.
  • 미들웨어: 연결 및 메시지 처리에 추가적인 로직을 적용합니다.
  • 자동 재연결: 연결이 끊어질 경우 자동으로 재연결을 시도합니다.
  • 메시지 확인: 메시지가 성공적으로 전달되었는지 확인할 수 있습니다.

3.2 Node.js에서 Socket.IO 구현하기

Socket.IO는 Node.js에서 socket.io 패키지를 사용하여 쉽게 구현할 수 있습니다. 먼저 필요한 패키지를 설치합니다.

npm install socket.io express

서버 코드:

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);
let userCount = 0;

// 클라이언트 연결 이벤트 처리
io.on('connection', (socket) => {
    userCount++;
    console.log(`A new client connected. Current users: ${userCount}`);

     // 새 사용자 접속 시 알림
    io.emit('user joined', { message: '새로운 사용자가 접속했습니다.', userCount });

     // 사용자 이름 설정
    socket.on('set username', (username) => {
        socket.username = username;
        io.emit('user list', { users: Object.values(io.sockets.sockets).map(s => s.username).filter(name=>name) })
        console.log(`User ${username} set.`);
    });


    // 클라이언트로부터 메시지 수신 이벤트 처리
    socket.on('chat message', (msg) => {
        console.log(`Message from ${socket.username}: ${msg}`);
        io.emit('chat message', { username: socket.username, message: msg });
    });


    // 클라이언트 연결 종료 이벤트 처리
    socket.on('disconnect', () => {
      userCount--;
        io.emit('user left', {message: `${socket.username} 님이 떠나셨습니다.`, userCount})
        console.log(`Client disconnected. Current users: ${userCount}`);
    });
});

// 서버 시작
server.listen(3000, () => {
    console.log('Server listening on port 3000.');
});

클라이언트 코드 (HTML):

<!DOCTYPE html>
<html>
<head>
    <title>Chat Application</title>
    <style>
      #messages {
        list-style-type: none;
        margin: 0;
        padding: 0;
      }
      #messages li {
        padding: 5px 10px;
      }
      #messages li:nth-child(odd) {
        background: #eee;
      }
      #form {
        background: rgba(0, 0, 0, 0.15);
        padding: 3px;
        position: fixed;
        bottom: 0;
        width: 100%;
      }
      #form input {
        border: 0;
        padding: 10px;
        width: 90%;
        margin-right: 0.5%;
      }
      #form button {
        background: rgb(130, 224, 255);
        border: none;
        padding: 10px;
        width: 9%;
      }
      #user-list {
        position: absolute;
        top: 0px;
        left: 0px;
        padding: 10px;
      }
      #user-list li {
        margin-bottom: 5px;
      }
        </style>
</head>
<body>
  <div id="user-list">
     <h4>Online Users</h4>
     <ul id="users"></ul>
  </div>
    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="username" placeholder="Enter your username" autocomplete="off" />
        <input id="input" placeholder="Enter message" autocomplete="off" disabled/><button disabled>Send</button>
    </form>

    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io();

        const form = document.getElementById('form');
        const input = document.getElementById('input');
        const usernameInput = document.getElementById('username');
        const messages = document.getElementById("messages");
        const users = document.getElementById('users');
        const sendButton = form.querySelector('button');

        usernameInput.addEventListener('change', function(e){
          e.preventDefault();
            if(usernameInput.value) {
             socket.emit('set username', usernameInput.value);
                usernameInput.disabled = true;
                input.disabled = false;
                sendButton.disabled = false;
            }

        });
         form.addEventListener('submit', function(e) {
            e.preventDefault();
            if (input.value) {
                socket.emit('chat message', input.value);
                input.value = '';
            }
        });


        socket.on('user joined', function(data) {
            const item = document.createElement('li');
            item.textContent = data.message + ` (접속자 수: ${data.userCount})`;
              messages.appendChild(item);
             window.scrollTo(0, document.body.scrollHeight);
        });

           socket.on('user left', function(data) {
            const item = document.createElement('li');
            item.textContent = data.message + ` (접속자 수: ${data.userCount})`;
              messages.appendChild(item);
            window.scrollTo(0, document.body.scrollHeight);
        });


        socket.on('chat message', function(data) {
            const item = document.createElement('li');
            item.textContent = `${data.username}: ${data.message}`;
            messages.appendChild(item);
            window.scrollTo(0, document.body.scrollHeight);
        });

        socket.on('user list', function(data) {
          users.innerHTML = '';
          data.users.forEach(user=> {
             const item = document.createElement('li');
              item.textContent = user;
              users.appendChild(item);
          })
        })
   </script>
</body>
</html>

코드 설명:

  1. 서버 코드:
    • express, http, socket.io 패키지를 사용하여 서버를 구성합니다.
    • io.on('connection', (socket) => { ... });: 클라이언트 연결 시 이벤트를 처리합니다.
    • socket.on('chat message', (msg) => { ... });: 클라이언트로부터 'chat message' 이벤트를 받을 때마다 처리합니다.
    • io.emit('chat message', msg);: 모든 클라이언트에게 'chat message' 이벤트를 브로드캐스트합니다.
    • server.listen(3000, () => { ... });: 3000 포트에서 서버를 실행합니다.
  2. 클라이언트 코드:
    • /socket.io/socket.io.js를 통해 Socket.IO 클라이언트 라이브러리를 가져옵니다.
    • const socket = io();: Socket.IO 클라이언트를 초기화합니다.
    • usernameInput.addEventListener('change', function(e){ ... });: 사용자 이름 설정 이벤트를 처리합니다.
    • form.addEventListener('submit', function(e) { ... });: 폼 제출 이벤트를 처리합니다.
    • socket.emit('chat message', input.value);: 서버에 'chat message' 이벤트를 전송합니다.
    • socket.on('chat message', function(data) { ... });: 서버로부터 'chat message' 이벤트를 받을 때마다 처리합니다.
      • socket.on('user joined', function(data) { ... }); : 서버에서 새로운 유저가 접속했다는 정보를 받아 처리합니다.
    • socket.on('user left', function(data) { ... }); : 서버에서 유저가 떠났다는 정보를 받아 처리합니다.
    • socket.on('user list', function(data) { ... }); : 서버에서 유저 리스트 정보를 받아 처리합니다.

추가 예제:

  • 개인 메시지 기능: 클라이언트 ID를 이용하여 특정 사용자에게 개인 메시지를 보냅니다.

        io.on('connection', (socket) => {
        socket.on('private message', (data) => {
            const targetSocket = io.sockets.sockets.get(data.targetUserId);
            if (targetSocket) {
                targetSocket.emit('private message', {
                    from: socket.id,
                    message: data.message,
                })
            }
        });
    });
  • 타이핑 알림: 사용자가 메시지를 입력 중임을 다른 사용자에게 실시간으로 알립니다.

            socket.on('typing', () => {
              socket.broadcast.emit('user typing', { username: socket.username })
            });
    
            socket.on('stop typing', () => {
               socket.broadcast.emit('user stop typing', { username: socket.username })
            });
  • 룸 기능: 사용자들이 특정 룸에 참여하고 해당 룸에 있는 사용자들에게만 메시지를 보낼 수 있도록 합니다.

    ```javascript
      io.on('connection', (socket) => {
    socket.on('join room', (room) => {
      socket.join(room);
        socket.room = room
      io.to(room).emit('chat message', { username: socket.username, message: `${socket.username} 님이 방에 참가하셨습니다.`})
      });
    
    socket.on('chat message', (msg) => {
       io.to(socket.room).emit('chat message', { username: socket.username, message: msg });
    });
    })
    ```

3.3 Socket.IO 고급 기능 활용하기

Socket.IO는 네임스페이스, 룸, 미들웨어 등의 고급 기능을 제공하여 더욱 확장성 있는 실시간 애플리케이션을 구축할 수 있도록 지원합니다.

  • 네임스페이스: 여러 채널로 클라이언트와 서버 간 통신을 분리할 수 있습니다.

        const adminNamespace = io.of('/admin').on('connection', (socket) => {
            console.log("Admin namespace connected");
            socket.emit('adminMessage', 'Admin panel connected')
        });

    클라이언트에서 연결시:

      const adminSocket = io('/admin');
            adminSocket.on('adminMessage', (msg)=>{
                 //adminMessage received
            })
  • 룸: 특정 사용자 그룹 간에 메시지를 주고받을 수 있습니다.

         io.on('connection', (socket) => {
          socket.join('room1');
          io.to('room1').emit('message', 'Welcome to room 1!');
             socket.on('leaveRoom', ()=>{
             socket.leave('room1');
        })
    });
  • 미들웨어: 연결 및 메시지 처리에 추가적인 로직을 적용하여 보안을 강화할 수 있습니다.

        io.use((socket, next) => {
        if (socket.request.headers['x-auth-token'] !== 'mysecrettoken') {
            return next(new Error('authentication error'));
         }
        next();
        });

4. WebSocket vs Socket.IO: 선택의 기로에서

4.1 기술 비교

WebSocket과 Socket.IO는 모두 실시간 통신을 위한 기술이지만, 각각의 장단점을 고려하여 프로젝트에 적합한 기술을 선택해야 합니다.

WebSocket:

  • 장점: 효율적인 양방향 통신, 낮은 오버헤드
  • 단점: 복잡한 직접 구현 필요, 폴백 기능 부재, 메시지 브로드캐스팅 및 에러 처리 추가 개발 필요

Socket.IO:

  • 장점: 편리한 API, 폴백 기능 지원, 브로드캐스팅, 네임스페이스, 룸, 에러 처리, 자동 재연결 기능 제공
  • 단점: WebSocket 대비 약간의 오버헤드 발생 가능성

4.2 선택 가이드

  • 단순한 실시간 통신 또는 성능 최적화: WebSocket 고려
  • 다양한 기능 및 편의성, 브라우저 호환성: Socket.IO 권장
  • 대규모 서비스 환경: WebSocket을 기반으로 자체 라이브러리 개발 고려

5. 결론 및 추가 고려 사항

본 가이드에서는 실시간 웹 애플리케이션 개발에 필수적인 WebSocket과 Socket.IO 기술에 대해 자세히 살펴보았습니다. 각 기술의 특징과 활용 사례, 구현 방법, 장단점을 비교 분석하여 독자 여러분이 프로젝트에 적합한 기술을 선택하고 실시간 기능을 성공적으로 구현할 수 있도록 지원했습니다.

실시간 기능은 사용자 경험을 향상시키고 역동적인 서비스를 제공하는 데 핵심적인 역할을 합니다. 따라서 최신 기술 트렌드와 프로젝트 요구 사항을 지속적으로 고려하여 가장 적합한 기술을 선택하고 적용해야 합니다. 또한, WebSocket과 Socket.IO 외에도 Server-Sent Events(SSE)와 같은 다른 실시간 기술들을 함께 고려하여 프로젝트의 특성에 맞는 최적의 솔루션을 선택하는 것이 중요합니다. 본 가이드가 실시간 웹 애플리케이션 개발 여정에 도움이 되기를 바랍니다.

728x90