Nest.js를 배워보자/11. NestJS WebSocket 실전 — 실시간 시스템 만들기

🖥️ NestJS 및 NuxtJS를 이용한 실시간 서버 상태 구현

_Blue_Sky_ 2025. 12. 3. 16:23
728x90

 


NestJS를 사용하여 실제 서버의 상태 정보를 실시간으로 수집하고 NuxtJS(클라이언트)로 전송하는 WebSocket Gateway의 구체적이고 실무적인 예제를 보강하여 제공합니다. 이 예제는 시스템 CPU 사용률을 주기적으로 모니터링하고 클라이언트에 푸시합니다.


1. NestJS 프로젝트 설정 및 의존성 설치

Node.js 시스템 정보 수집을 위해 os-utils 라이브러리를 사용합니다.

# NestJS 웹소켓 관련 의존성은 이미 설치되었다고 가정합니다.
# npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
# 서버 상태 모니터링을 위한 유틸리티 설치
npm install os-utils
# 또는
yarn add os-utils

2. 상태 모니터링 서비스 (MonitorService) 구현

실제 서버 상태를 측정하고 관리하는 역할을 담당합니다.

src/monitor/monitor.service.ts

import { Injectable, OnModuleInit } from '@nestjs/common';
import * as os from 'os-utils';

@Injectable()
export class MonitorService implements OnModuleInit {
  private currentCpuUsage: number = 0; // CPU 사용률 저장 변수 (0.0 ~ 100.0)
  
  onModuleInit() {
    this.startCpuMonitoring();
  }

  // 1초마다 CPU 사용률을 측정하고 업데이트하는 함수
  private startCpuMonitoring() {
    setInterval(() => {
      os.cpuUsage((v) => {
        // os-utils는 CPU 사용률을 0.0부터 1.0 사이의 값으로 반환합니다.
        // 클라이언트에게는 백분율(%)로 보여주기 위해 100을 곱합니다.
        this.currentCpuUsage = Math.floor(v * 1000) / 10; 
      });
    }, 1000); // 1000ms (1초) 간격으로 측정
  }

  // 현재 CPU 사용률을 반환하는 메서드
  public getCpuUsage(): number {
    return this.currentCpuUsage;
  }
  
  // 현재 서버의 RAM 사용률을 반환하는 메서드 (추가 정보)
  public getMemoryUsage(): { total: number; free: number; used: number; usagePercent: number } {
    const total = os.totalmem(); // GB
    const free = os.freemem(); // GB
    const used = total - free;
    const usagePercent = Math.floor((used / total) * 1000) / 10;

    return { total, free, used, usagePercent };
  }
}

3. WebSocket Gateway (StatusGateway) 구현

MonitorService에서 가져온 데이터를 주기적으로 연결된 모든 클라이언트에게 전송합니다.

src/gateway/status.gateway.ts

import { WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
import { MonitorService } from '../monitor/monitor.service'; // 상태 모니터링 서비스 주입

// NestJS 서버가 실행 중인 포트와 다른 포트를 사용하거나,
// CORS 설정을 조정해야 할 수 있습니다. (NuxtJS가 기본적으로 3000번을 사용한다고 가정)
@WebSocketGateway(8080, { 
    cors: { 
        origin: ['http://localhost:3000', 'http://127.0.0.1:3000'], // NuxtJS 개발 서버 주소
        credentials: true 
    } 
})
export class StatusGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() 
  server: Server; // Socket.io 서버 인스턴스

  private logger: Logger = new Logger('StatusGateway');
  private intervalId: NodeJS.Timeout | null = null;

  constructor(private readonly monitorService: MonitorService) {} // 서비스 주입

  // 1. 게이트웨이 초기화 시 (서버 시작 시)
  afterInit(server: Server) {
    this.logger.log('WebSocket Gateway Initialized');
    
    // 서버 시작과 동시에 주기적인 상태 전송 시작
    this.startStatusBroadcasting();
  }

  // 2. 클라이언트 연결 시
  handleConnection(client: Socket, ...args: any[]) {
    this.logger.log(`Client Connected: ${client.id}`);
    // 연결된 클라이언트에게 현재 상태를 즉시 전송
    this.sendServerStatus(client); 
  }

  // 3. 클라이언트 연결 해제 시
  handleDisconnect(client: Socket) {
    this.logger.log(`Client Disconnected: ${client.id}`);
    
    // 연결된 클라이언트가 0명이면 브로드캐스팅 중지 (선택 사항)
    // if (this.server.engine.clientsCount === 0) {
    //   this.stopStatusBroadcasting();
    // }
  }
  
  // **실무 핵심 로직:** 2초마다 모든 클라이언트에게 서버 상태 브로드캐스팅
  private startStatusBroadcasting() {
    if (this.intervalId) return; // 이미 실행 중이면 중복 실행 방지

    this.intervalId = setInterval(() => {
        // CPU 및 Memory 상태를 가져옴
        const cpuUsage = this.monitorService.getCpuUsage();
        const memoryStatus = this.monitorService.getMemoryUsage();

        const statusPayload = {
            timestamp: new Date().toISOString(),
            cpuUsage: cpuUsage, // %
            memory: {
                total: memoryStatus.total, // GB
                used: memoryStatus.used, // GB
                usagePercent: memoryStatus.usagePercent // %
            },
            // 추가적인 서버 상태 항목을 여기에 포함 가능 (예: DB 연결 상태, 요청 수 등)
        };

        // 연결된 모든 클라이언트에게 'server_status' 이벤트를 통해 상태 전송
        this.server.emit('server_status', statusPayload);
        
    }, 2000); // 2000ms (2초) 간격으로 전송
    this.logger.log('Status Broadcasting Started (Interval: 2s)');
  }
  
  // 특정 클라이언트에게만 상태 전송 (연결 직후 등)
  private sendServerStatus(client: Socket) {
     const cpuUsage = this.monitorService.getCpuUsage();
     const memoryStatus = this.monitorService.getMemoryUsage();
     const statusPayload = {
            timestamp: new Date().toISOString(),
            cpuUsage: cpuUsage,
            memory: { total: memoryStatus.total, used: memoryStatus.used, usagePercent: memoryStatus.usagePercent },
        };
     client.emit('server_status', statusPayload);
  }
  
  // 브로드캐스팅 중지 (선택 사항)
  // private stopStatusBroadcasting() {
  //   if (this.intervalId) {
  //     clearInterval(this.intervalId);
  //     this.intervalId = null;
  //     this.logger.log('Status Broadcasting Stopped');
  //   }
  // }
}

4. 모듈 구성

NestJS 앱 모듈에 서비스와 게이트웨이를 등록해야 합니다.

src/app.module.ts

import { Module } from '@nestjs/common';
import { MonitorService } from './monitor/monitor.service';
import { StatusGateway } from './gateway/status.gateway';

@Module({
  imports: [],
  controllers: [],
  providers: [
    MonitorService, // 서버 상태 모니터링 서비스
    StatusGateway,  // WebSocket 게이트웨이
  ],
})
export class AppModule {}

결론

이 예제는 MonitorService에서 서버 상태(CPU, Memory)를 지속적으로 측정하고, StatusGateway가 이 데이터를 받아 setInterval 타이머를 이용해 2초마다 연결된 모든 NuxtJS 클라이언트에게 server_status라는 이벤트로 실시간 푸시하는 실무적인 NestJS 백엔드 구현 방식을 보여줍니다.

클라이언트(NuxtJS)는 socket.on('server_status', ...)를 통해 이 데이터를 수신하고 UI를 갱신하게 됩니다.

 

728x90

🌐 NuxtJS (클라이언트) 구현: 실시간 서버 상태 수신

NuxtJS 프로젝트에서 앞서 NestJS가 포트 8080으로 송신하는 server_status 이벤트를 수신하고 화면에 표시하는 클라이언트 로직입니다.

1. 의존성 설치

클라이언트 측에서 WebSocket 서버에 연결하기 위해 socket.io-client를 설치합니다.

npm install socket.io-client
# 또는
yarn add socket.io-client

2. 상태 수신 및 표시 컴포넌트 구현

Nuxt 3 환경에서 Vue의 Composition API를 사용하여 컴포넌트의 상태를 관리하고, 서버에 연결 및 데이터를 수신합니다.

pages/index.vue (메인 페이지)

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { io } from 'socket.io-client';

// NestJS WebSocket 서버 주소 (게이트웨이 포트 8080)
const NESTJS_WS_URL = 'http://localhost:8080';

// 실시간 상태를 저장할 반응형 변수
const serverStatus = ref(null);
const connectionStatus = ref('연결 중...');
let socket = null;

onMounted(() => {
  // 1. Socket.io 서버에 연결
  socket = io(NESTJS_WS_URL);

  // 2. 연결 성공 이벤트 처리
  socket.on('connect', () => {
    connectionStatus.value = '🟢 연결 성공';
    console.log('Socket connected successfully!');
  });

  // 3. 연결 실패 이벤트 처리
  socket.on('disconnect', () => {
    connectionStatus.value = '🔴 연결 끊김';
    console.log('Socket disconnected.');
  });
  
  // 4. 서버로부터 'server_status' 이벤트 수신
  // NestJS StatusGateway에서 2초마다 전송하는 데이터 수신
  socket.on('server_status', (data) => {
    console.log('실시간 서버 상태 수신:', data);
    serverStatus.value = data; // 수신된 데이터를 반응형 상태에 저장
  });
  
  // 5. 연결 오류 처리 (선택 사항)
  socket.on('connect_error', (err) => {
      connectionStatus.value = `🔴 연결 오류: ${err.message}`;
      console.error('Connection Error:', err);
  });
});

// 컴포넌트가 파괴될 때 (페이지 이동 등) 소켓 연결 해제
onUnmounted(() => {
  if (socket) {
    socket.disconnect();
  }
});

// UI 표시를 위한 시간 포맷팅 함수
const formatTime = (isoString) => {
    if (!isoString) return 'N/A';
    return new Date(isoString).toLocaleTimeString('ko-KR');
};
</script>

<template>
  <div class="container">
    <h1>🚀 실시간 NestJS 서버 상태 모니터링</h1>
    <p><strong>WebSocket 상태:</strong> <span :style="{ color: connectionStatus.startsWith('🟢') ? 'green' : 'red' }">{{ connectionStatus }}</span></p>

    <div v-if="serverStatus" class="status-card">
      <p class="timestamp">최종 수신 시간: {{ formatTime(serverStatus.timestamp) }}</p>
      
      <h2>💻 CPU 사용률</h2>
      <div class="status-item">
        <p class="value">{{ serverStatus.cpuUsage.toFixed(1) }} %</p>
        <div class="bar-container">
          <div class="bar" :style="{ width: serverStatus.cpuUsage + '%' }"></div>
        </div>
      </div>
      
      <h2>🧠 메모리 사용률</h2>
      <div class="status-item">
        <p class="value">{{ serverStatus.memory.usagePercent.toFixed(1) }} %</p>
        <div class="bar-container">
          <div class="bar" :style="{ width: serverStatus.memory.usagePercent + '%' }"></div>
        </div>
        <p class="detail">사용 중: {{ serverStatus.memory.used.toFixed(2) }} GB / 전체: {{ serverStatus.memory.total.toFixed(2) }} GB</p>
      </div>

    </div>
    <div v-else class="loading">
      <p>서버 상태 데이터 수신 대기 중...</p>
    </div>
  </div>
</template>

<style scoped>
.container {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 { color: #3498db; }
h2 { font-size: 1.2em; border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 20px;}
.status-card {
  margin-top: 20px;
  padding: 15px;
  background: #f9f9f9;
  border-radius: 6px;
}
.timestamp {
    font-size: 0.8em;
    color: #777;
    text-align: right;
}
.status-item {
  margin-bottom: 15px;
}
.value {
  font-size: 1.5em;
  font-weight: bold;
  color: #2c3e50;
  margin: 5px 0;
}
.detail {
    font-size: 0.9em;
    color: #555;
    margin-top: 5px;
}
.bar-container {
  height: 10px;
  background-color: #eee;
  border-radius: 5px;
  overflow: hidden;
}
.bar {
  height: 100%;
  background-color: #e74c3c; /* 빨간색으로 상태 표시 */
  transition: width 0.3s ease;
}
</style>

3. 주요 로직 요약

  • 연결 URL: io('http://localhost:8080')를 사용하여 NestJS의 StatusGateway가 열려 있는 포트 8080에 접속합니다.
  • 반응형 상태: serverStatus = ref(null)을 사용하여 수신된 데이터를 저장하고, 이 변수가 업데이트되면 NuxtJS/Vue 템플릿이 자동으로 갱신됩니다.
  • 이벤트 수신: socket.on('server_status', (data) => { ... })를 통해 백엔드에서 브로드캐스트된 server_status 이벤트를 포착하고 그 안에 담긴 JSON 데이터를 serverStatus에 저장합니다.
  • 라이프사이클 관리: onMounted에서 연결을 시작하고, onUnmounted에서 socket.disconnect()를 호출하여 페이지를 벗어날 때 연결을 깔끔하게 해제합니다.

이로써 NestJS와 NuxtJS 간의 실시간 WebSocket 통신을 통한 서버 상태 모니터링 시스템의 전체 구현이 완료됩니다.

728x90

이 영상은 NestJS에서 WebSocket Gateway를 생성하고 사용하는 기본 방법을 설명하여 백엔드 구현에 도움이 될 수 있습니다.

NestJS Websockets Tutorial #1 - Creating a Websocket Gateway Server


🛡️ NestJS/NuxtJS 실시간 시스템의 확장성 및 인증 처리

네, 실시간 애플리케이션을 실제 서비스에 배포할 때는 확장성인증 문제가 필수적으로 고려되어야 합니다. 특히 WebSocket은 HTTP와 달리 한 번 연결되면 장시간 유지되므로 세션 관리가 중요합니다.


1. 🚀 확장성 (Scaling) 처리

NestJS 서버를 여러 인스턴스로 확장(Scale-out)할 때, WebSocket 연결을 관리하는 방법에 대한 접근 방식입니다.

문제:

일반적으로 클라이언트는 로드 밸런서를 통해 특정 NestJS 인스턴스(Server A)에 연결됩니다. 만약 Server A에 연결된 클라이언트가 Server B에서 발생한 상태 변화 데이터를 받아야 한다면, 이 두 서버 인스턴스는 서로 통신할 방법이 필요합니다.

해결책: 어댑터 (Adapter) 사용

Socket.io는 이 문제를 해결하기 위해 Adapter 개념을 제공합니다. 가장 일반적인 방법은 Redis를 메시지 브로커로 사용하는 것입니다.

  • Redis Adapter 설치 (NestJS 백엔드): 
    npm install @socket.io/redis-adapter ioredis
    
  • 작동 방식:
    1. 모든 NestJS 서버 인스턴스(A, B, C...)는 동일한 Redis Pub/Sub 채널에 연결됩니다.
    2. Server B에서 상태 업데이트가 발생하여 this.server.emit('event', data)를 호출하면, Redis Adapter는 이 이벤트를 Redis의 특정 채널에 발행(Publish)합니다.
    3. 다른 모든 인스턴스(A, C...)는 Redis 채널을 구독(Subscribe)하고 있다가 이 메시지를 수신합니다.
    4. 수신한 메시지를 통해 자신에게 연결된 클라이언트들에게 이벤트를 푸시(Push)합니다.

이 방식을 사용하면, 수많은 클라이언트가 여러 NestJS 인스턴스에 분산 연결되어 있어도 모든 클라이언트가 실시간 데이터를 일관되게 수신할 수 있습니다.


2. 🔑 인증 (Authentication) 및 권한 부여 (Authorization) 처리

WebSocket 연결 시 클라이언트가 유효한 사용자인지 확인하고, 해당 사용자가 특정 데이터를 수신할 권한이 있는지 확인해야 합니다.

1. Handshake 단계에서의 인증 (JWT 사용)

WebSocket 연결이 설정되는 초기 단계(Handshake)에서 사용자를 인증합니다.

  • NuxtJS 클라이언트: 클라이언트는 HTTP 로그인 후 받은 JWT(JSON Web Token)를 WebSocket 연결 요청 시 서버에 전달해야 합니다.
    // NuxtJS (Client)
    const token = localStorage.getItem('access_token');
    const socket = io(NESTJS_WS_URL, {
        auth: {
            token: token // 토큰을 Handshake 데이터에 포함하여 전송
        }
    });
    
  • NestJS Gateway: NestJS의 StatusGateway에서 Socket.io의 미들웨어 기능을 사용하여 연결 전에 토큰을 검증합니다.
    // NestJS Gateway (status.gateway.ts) - 일부 수정
    afterInit(server: Server) {
        // 서버 인스턴스에 미들웨어를 적용
        server.use(async (socket: Socket, next) => {
            const token = socket.handshake.auth.token;
            if (!token) {
                return next(new Error('인증 토큰 누락'));
            }
            try {
                // 1. 토큰 검증 로직 (예: JWT Service를 사용하여 검증)
                const payload = await this.jwtService.verifyAsync(token); 
    
                // 2. 소켓 인스턴스에 사용자 정보 저장
                socket['user'] = payload; 
                next(); // 인증 성공 -> 연결 허용
            } catch (e) {
                next(new Error('유효하지 않은 토큰')); // 인증 실패 -> 연결 거부
            }
        });
        // ... 기존 초기화 로직 ...
    }
    

2. 권한 부여 (Authorization)

인증된 사용자에게만 특정 이벤트를 보내야 할 경우, NestJS Gateway에서 소켓 인스턴스에 저장된 사용자 정보(socket['user'])를 사용합니다.

  • 특정 방(Room) 사용: 사용자 인증 후, 해당 사용자의 ID를 기반으로 Room에 가입시킵니다.
    // NestJS Gateway - handleConnection 내
    handleConnection(client: Socket) {
        const userId = client['user'].sub; // 인증 미들웨어에서 저장된 user 정보
        client.join(`user-room-${userId}`); // 사용자 ID 기반의 방에 가입
        this.logger.log(`Client ${client.id} joined room user-room-${userId}`);
    }
    
    // 특정 사용자에게만 메시지 전송
    sendNotificationToUser(targetUserId: number, message: string) {
        this.server.to(`user-room-${targetUserId}`).emit('private_notification', message);
    }
    

이 방식을 통해 수백만 개의 소켓 연결이 분산된 상황에서도 특정 사용자에게만 알림을 보내거나, 권한이 있는 사용자에게만 서버 상태 데이터를 제공할 수 있게 됩니다.

728x90