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

실시간 채팅 예제와 인증된 WebSocket 연결

_Blue_Sky_ 2025. 12. 3. 11:09
728x90


💬 1. 실시간 채팅 예제 구현 (Full Code)

앞서 설정한 ChatGateway 클래스를 확장하여 기본적인 실시간 채팅 기능을 완성해 보겠습니다. 이 예제에서는 클라이언트가 메시지를 전송하면, 서버가 이를 수신하여 발신자를 제외한 모든 연결된 클라이언트에게 메시지를 브로드캐스팅합니다.

// src/chat/chat.gateway.ts
import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  WsResponse,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';

@WebSocketGateway(8080, { namespace: '/chat', cors: { origin: '*' } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(ChatGateway.name);
  
  // 서버 인스턴스를 저장하여 브로드캐스팅에 사용 (afterInit에서 초기화)
  private io: Server; 

  afterInit(server: Server) {
    this.io = server;
    this.logger.log('Chat Gateway 초기화됨');
  }

  handleConnection(client: Socket) {
    this.logger.log(`새 클라이언트 연결: ${client.id}`);
    // 연결된 클라이언트들에게 새로운 사용자 접속 알림 (선택 사항)
    this.io.emit('userJoined', { id: client.id, message: '새 사용자가 접속했습니다.' });
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`클라이언트 연결 해제: ${client.id}`);
    // 연결 해제된 클라이언트들에게 사용자 퇴장 알림 (선택 사항)
    this.io.emit('userLeft', { id: client.id, message: '사용자가 나갔습니다.' });
  }

  // **핵심 채팅 로직**
  @SubscribeMessage('sendMessage')
  handleMessage(
    @MessageBody() data: { username: string; message: string },
    @ConnectedSocket() client: Socket,
  ) {
    this.logger.log(`[${data.username}] 메시지 수신: ${data.message}`);

    const messagePayload = {
      username: data.username,
      message: data.message,
      timestamp: new Date().toISOString(),
    };

    // 1. 발신자에게만 응답 (선택 사항)
    // client.emit('messageSent', { success: true });

    // 2. 발신자를 제외한 모두에게 메시지 브로드캐스팅
    client.broadcast.emit('receiveMessage', messagePayload);
    
    // 3. 연결된 모두에게 메시지 브로드캐스팅 (발신자 포함)
    // this.io.emit('receiveMessage', messagePayload);
  }
}

클라이언트 측 예시 (Socket.io-client):

const socket = io('http://localhost:8080/chat');

// 메시지 전송 socket.emit('sendMessage', { username: 'Alice', message: '안녕하세요!' });

// 메시지 수신 리스너 socket.on('receiveMessage', (payload) => { console.log(${payload.username}: ${payload.message}); });


728x90

🔒 2. 인증된 WebSocket 연결 (Authentication)

HTTP 요청과 달리 WebSocket 연결은 지속적이므로, 초기 연결 시 인증 정보를 확인하고 이후 연결 상태를 유지해야 합니다. 가장 일반적인 방법은 연결 시 쿼리 파라미터 또는 헤더를 통해 JWT 토큰을 전달받아 검증하는 것입니다.

1. 연결 시 토큰 전달 및 검증

socket.io를 사용하면, 클라이언트 측에서 연결 요청 시 auth 옵션을 통해 토큰을 전달할 수 있습니다.

클라이언트 (JavaScript/TypeScript):

const token = 'YOUR_JWT_TOKEN_HERE';
const socket = io('http://localhost:8080/chat', {
  auth: {
    token: token // 토큰을 auth 객체에 담아 전송
  }
});
728x90

서버 (NestJS Gateway - handleConnection에서 처리):

Gateway에서 handleConnection 메서드를 오버라이드하여 연결 시점에 인증 로직을 구현합니다.

import { WsException } from '@nestjs/websockets';
import * as jwt from 'jsonwebtoken'; // 실제 환경에서는 NestJS Passport 및 JwtService를 사용합니다.

@WebSocketGateway(...)
export class AuthenticatedChatGateway implements OnGatewayConnection {
    // ... 기타 설정 생략

    handleConnection(client: Socket) {
        const token = client.handshake.auth.token; // 1. 클라이언트가 보낸 토큰 추출

        if (!token) {
            // 2. 토큰이 없으면 연결 거부 및 오류 전송
            throw new WsException('인증 토큰이 필요합니다.');
        }

        try {
            // 3. JWT 검증 및 사용자 페이로드 추출
            const payload = jwt.verify(token, 'YOUR_SECRET_KEY'); // 실제 비밀키 사용
            
            // 4. 인증 성공! 소켓 객체에 사용자 정보 저장 (이후 메시지 처리 시 사용)
            client.data.user = payload; 
            this.logger.log(`인증 성공 및 연결: ${client.id}, 사용자: ${payload.userId}`);

        } catch (e) {
            // 5. 토큰이 유효하지 않으면 연결 거부 및 오류 전송
            // client.disconnect() 대신 WsException을 사용하여 클라이언트에게 명확한 오류를 보낼 수 있습니다.
            throw new WsException('유효하지 않은 토큰입니다.');
        }
    }

    @SubscribeMessage('sendMessage')
    handleMessage(@MessageBody() data: string, @ConnectedSocket() client: Socket) {
        // 인증된 사용자 정보 사용
        const userId = client.data.user.userId; 
        this.logger.log(`[${userId}]로부터 메시지 수신: ${data}`);
        // ... 메시지 처리 로직
    }
}

2. NestJS Guard 활용 (고급)

더 견고한 인증을 위해, HTTP 환경에서 사용하는 것처럼 WsGuard를 만들어 @UseGuards() 데코레이터를 Gateway 메서드에 적용할 수도 있습니다. 이는 canActivate 로직을 통해 메시지 이벤트가 처리되기 직전에 인증을 검사할 수 있게 해줍니다.


 

728x90