NestJS와 WebSocket 2026: 실시간 통신, Gateway 패턴, 면접 질문 가이드

NestJS에서 WebSocket 구현을 완벽하게 마스터합니다. Gateway 패턴, Socket.IO 통합, 인증, 에러 핸들링, 2026년 기술 면접 빈출 질문까지 상세히 다룹니다.

NestJS WebSocket 실시간 통신 게이트웨이

NestJS WebSocket은 Gateway 패턴을 활용하여 구조화된 실시간 통신 방식을 제공합니다. 원시 WebSocket 구현과 달리 NestJS는 데코레이터를 통해 복잡성을 추상화하고 의존성 주입 시스템과 원활하게 통합됩니다.

핵심 개념

NestJS에서 WebSocket Gateway는 @WebSocketGateway() 데코레이터가 적용된 클래스로, 클라이언트와 서버 간의 양방향 통신을 처리합니다. 여러 어댑터(Socket.IO, ws)를 지원하며 NestJS Guards, Pipes, Interceptors와 통합됩니다.

Socket.IO를 사용한 WebSocket Gateway 설정

NestJS의 기본 어댑터는 Socket.IO를 사용하며, 자동 재연결, 룸 지원, HTTP 롱 폴링 폴백 기능을 제공합니다. 설치에는 핵심 WebSockets 모듈과 함께 플랫폼별 패키지가 필요합니다.

bash
# terminal
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io

기본 Gateway는 수신 메시지를 리스닝하고 연결된 클라이언트에게 응답을 브로드캐스트할 수 있습니다.

chat.gateway.tstypescript
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: { origin: '*' }, // Configure CORS for browser clients
})
export class ChatGateway {
  @WebSocketServer()
  server: Server; // Access to the underlying Socket.IO server

  @SubscribeMessage('message') // Listen for 'message' events
  handleMessage(
    @MessageBody() data: { text: string; room: string },
    @ConnectedSocket() client: Socket,
  ): void {
    // Broadcast to all clients in the room except sender
    client.to(data.room).emit('message', {
      text: data.text,
      senderId: client.id,
      timestamp: Date.now(),
    });
  }
}

@SubscribeMessage 데코레이터는 메서드를 특정 이벤트 이름에 바인딩합니다. @MessageBody()는 페이로드를 추출하고, @ConnectedSocket()은 타겟 응답을 위해 클라이언트 소켓에 대한 접근을 제공합니다.

연결 관리를 위한 라이프사이클 훅

NestJS Gateway는 클라이언트 연결 및 연결 해제를 처리하기 위한 라이프사이클 인터페이스를 구현합니다. 이러한 훅은 리소스 정리, 접속 상태 추적, 연결 검증을 가능하게 합니다.

presence.gateway.tstypescript
import {
  WebSocketGateway,
  OnGatewayConnection,
  OnGatewayDisconnect,
  OnGatewayInit,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';

@WebSocketGateway()
export class PresenceGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(PresenceGateway.name);
  private connectedUsers = new Map<string, { joinedAt: Date }>();

  afterInit(server: Server): void {
    // Called once when the gateway initializes
    this.logger.log('WebSocket Gateway initialized');
  }

  handleConnection(client: Socket): void {
    // Track new connections
    this.connectedUsers.set(client.id, { joinedAt: new Date() });
    this.logger.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket): void {
    // Cleanup on disconnect
    this.connectedUsers.delete(client.id);
    this.logger.log(`Client disconnected: ${client.id}`);
  }
}

세 가지 라이프사이클 인터페이스는 각각 다른 목적을 가집니다. OnGatewayInit은 시작 시 한 번 실행되고, OnGatewayConnection은 클라이언트 연결마다 트리거되며, OnGatewayDisconnect는 클라이언트 이탈 시 정리를 처리합니다.

WebSocket Guards를 통한 인증

WebSocket 연결은 보호된 이벤트에 대한 접근을 허용하기 전에 인증 검증이 필요합니다. NestJS Guards는 Gateway와 함께 작동하지만, 실행 컨텍스트는 HTTP 요청과 다릅니다. 공식 NestJS WebSockets 문서에서 Guard 통합에 대해 자세히 설명합니다.

ws-auth.guard.tstypescript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { JwtService } from '@nestjs/jwt';
import { Socket } from 'socket.io';

@Injectable()
export class WsAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient();
    // Extract token from handshake auth or query params
    const token =
      client.handshake.auth?.token ||
      client.handshake.query?.token;

    if (!token) {
      throw new WsException('Missing authentication token');
    }

    try {
      const payload = this.jwtService.verify(token as string);
      // Attach user data to socket for later use
      client.data.user = payload;
      return true;
    } catch {
      throw new WsException('Invalid token');
    }
  }
}

@UseGuards() 데코레이터를 사용하여 Gateway 클래스 또는 개별 메시지 핸들러에 Guard를 적용합니다. WsException 클래스는 클라이언트가 Socket.IO 에러 이벤트를 통해 수신하는 에러를 던집니다.

secure-chat.gateway.tstypescript
import { UseGuards } from '@nestjs/common';
import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';
import { WsAuthGuard } from './ws-auth.guard';

@WebSocketGateway()
@UseGuards(WsAuthGuard) // Protect all handlers in this gateway
export class SecureChatGateway {
  @SubscribeMessage('privateMessage')
  handlePrivateMessage(): void {
    // Only authenticated clients reach this handler
  }
}

모듈 수준 인증에 관한 질문은 JWT 전략과 세션 관리를 다루는 NestJS 인증 면접 모듈을 참조하세요.

Node.js / NestJS 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

룸 기반 브로드캐스트 패턴

Socket.IO 룸은 연결된 클라이언트의 하위 집합에 대한 타겟 메시지 전달을 가능하게 합니다. 일반적인 사용 사례로는 채팅방, 게임 로비, 라이브 협업 기능이 있습니다.

room.gateway.tstypescript
import {
  WebSocketGateway,
  SubscribeMessage,
  ConnectedSocket,
  MessageBody,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class RoomGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('joinRoom')
  handleJoinRoom(
    @MessageBody() roomId: string,
    @ConnectedSocket() client: Socket,
  ): { event: string; data: string } {
    // Add client to the specified room
    client.join(roomId);
    // Notify others in the room
    client.to(roomId).emit('userJoined', { userId: client.id });
    return { event: 'joinedRoom', data: roomId };
  }

  @SubscribeMessage('leaveRoom')
  handleLeaveRoom(
    @MessageBody() roomId: string,
    @ConnectedSocket() client: Socket,
  ): void {
    client.leave(roomId);
    client.to(roomId).emit('userLeft', { userId: client.id });
  }

  @SubscribeMessage('roomBroadcast')
  handleRoomBroadcast(
    @MessageBody() payload: { roomId: string; message: string },
  ): void {
    // Send to all clients in room, including sender
    this.server.to(payload.roomId).emit('roomMessage', payload.message);
  }
}

client.to(room).emit()server.to(room).emit()의 차이점은 중요합니다. 전자는 발신자를 제외하고, 후자는 모든 룸 멤버를 포함합니다.

예외 필터를 사용한 에러 핸들링

WebSocket 예외는 HTTP 예외 필터가 Gateway 컨텍스트에 적용되지 않으므로 전용 필터가 필요합니다. 커스텀 필터는 WsException을 캐치하고 클라이언트용 에러 응답을 포맷합니다.

ws-exception.filter.tstypescript
import { Catch, ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch(WsException)
export class WsExceptionFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost): void {
    const client: Socket = host.switchToWs().getClient();
    const error = exception.getError();
    // Send structured error to client
    client.emit('error', {
      type: 'WsException',
      message: typeof error === 'string' ? error : error,
      timestamp: new Date().toISOString(),
    });
  }
}

모든 메시지 핸들러에서 일관된 에러 핸들링을 위해 Gateway 레벨에서 필터를 적용합니다.

filtered.gateway.tstypescript
import { UseFilters } from '@nestjs/common';
import { WebSocketGateway } from '@nestjs/websockets';
import { WsExceptionFilter } from './ws-exception.filter';

@WebSocketGateway()
@UseFilters(WsExceptionFilter)
export class FilteredGateway {
  // All handlers benefit from centralized error handling
}

이 패턴은 미들웨어와 인터셉터 모듈에서 설명하는 NestJS 데코레이터와 필터 철학을 반영합니다.

Redis 어댑터를 사용한 스케일링

단일 서버 WebSocket 배포는 로드 밸런싱이 클라이언트를 여러 인스턴스에 분산할 때 실패합니다. Socket.IO Redis 어댑터는 pub/sub 메커니즘을 통해 인스턴스 간 이벤트를 동기화합니다.

app.module.tstypescript
import { Module } from '@nestjs/common';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [
    ChatGateway,
    {
      provide: 'REDIS_ADAPTER',
      useFactory: async () => {
        const pubClient = createClient({ url: process.env.REDIS_URL });
        const subClient = pubClient.duplicate();
        await Promise.all([pubClient.connect(), subClient.connect()]);
        return createAdapter(pubClient, subClient);
      },
    },
  ],
})
export class AppModule {}

Gateway는 인스턴스 간 통신을 위해 어댑터를 사용합니다.

scalable.gateway.tstypescript
import {
  WebSocketGateway,
  WebSocketServer,
  OnGatewayInit,
} from '@nestjs/websockets';
import { Inject } from '@nestjs/common';
import { Server } from 'socket.io';
import { Adapter } from 'socket.io-adapter';

@WebSocketGateway()
export class ScalableGateway implements OnGatewayInit {
  @WebSocketServer()
  server: Server;

  constructor(@Inject('REDIS_ADAPTER') private redisAdapter: Adapter) {}

  afterInit(): void {
    // Attach Redis adapter for multi-instance support
    this.server.adapter(this.redisAdapter as any);
  }
}

Redis 어댑터를 사용하면 한 서버 인스턴스에서 발행된 메시지가 클러스터 내 모든 인스턴스에 연결된 클라이언트에게 도달합니다.

NestJS WebSocket 면접 빈출 질문

기술 면접에서는 실시간 아키텍처 결정과 NestJS 특화 구현 세부 사항에 대한 이해를 자주 검증합니다.

자주 묻는 질문

NestJS에서 WebSocket 인증을 HTTP와 다르게 처리하는 방식은 무엇입니까?

HTTP 미들웨어는 WebSocket 연결에 대해 실행되지 않습니다. 인증은 핸드셰이크 단계에서 또는 Gateway에 적용된 Guards를 통해 발생합니다. 토큰 검증은 일반적으로 handleConnection()이나 socket.handshake.auth에서 읽는 커스텀 Guard에서 수행됩니다.

@WebSocketGateway() 포트 설정과 메인 애플리케이션 포트의 차이점은 무엇입니까?

기본적으로 WebSocket Gateway는 HTTP 서버 포트를 공유합니다. @WebSocketGateway(3001)에서 포트를 지정하면 해당 포트에 별도의 WebSocket 서버가 생성됩니다. 공유 포트는 배포를 단순화하지만 리버스 프록시를 사용할 때 경로 기반 라우팅(/socket.io)이 필요합니다.

NestJS에서 WebSocket Gateway를 테스트하는 방법은 무엇입니까?

NestJS 테스트 유틸리티는 Test.createTestingModule()을 통해 Gateway 테스트를 지원합니다. socket.io-client 라이브러리가 테스트 서버에 연결합니다. 라이프사이클 훅과 메시지 핸들러는 이벤트를 발행하고 응답이나 부작용을 검증하여 테스트합니다.

chat.gateway.spec.tstypescript
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { io, Socket } from 'socket.io-client';
import { ChatGateway } from './chat.gateway';

describe('ChatGateway', () => {
  let app: INestApplication;
  let client: Socket;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [ChatGateway],
    }).compile();

    app = module.createNestApplication();
    await app.listen(3000);
    client = io('http://localhost:3000');
  });

  afterAll(async () => {
    client.disconnect();
    await app.close();
  });

  it('receives message acknowledgment', (done) => {
    client.emit('message', { text: 'test', room: 'lobby' });
    client.on('message', (data) => {
      expect(data.text).toBe('test');
      done();
    });
  });
});

Socket.IO 대신 ws 어댑터를 사용해야 하는 경우는 언제입니까?

ws 라이브러리 어댑터(@nestjs/platform-ws)는 Socket.IO의 자동 재연결, 룸, 폴백 트랜스포트 같은 기능이 필요하지 않은 애플리케이션에 더 낮은 오버헤드를 제공합니다. 고빈도 트레이딩 시스템과 게임 서버는 레이턴시 감소를 위해 ws를 선호하는 경우가 많습니다.

더 넓은 NestJS 아키텍처 개념에 대해서는 Guards, Interceptors, 모듈러 아키텍처 관련 글에서 이러한 패턴이 HTTP와 WebSocket 컨텍스트 전반에 걸쳐 어떻게 통합되는지 설명합니다.

결론

  • WebSocket Gateway는 NestJS 의존성 주입, Guards, Filters와 통합되어 HTTP와 실시간 엔드포인트 전반에 걸쳐 일관된 패턴을 제공합니다
  • 라이프사이클 훅(OnGatewayConnection, OnGatewayDisconnect)은 접속 상태 추적과 리소스 정리를 관리합니다
  • 인증 흐름은 HTTP와 다르며, 토큰 검증은 핸드셰이크 중 또는 전용 WebSocket Guards를 통해 수행됩니다
  • Socket.IO의 룸 기반 브로드캐스팅으로 소켓 목록을 수동으로 관리하지 않고도 타겟 메시징이 가능합니다
  • Redis 어댑터는 여러 서버 인스턴스 간 이벤트를 동기화하여 수평 스케일링 문제를 해결합니다
  • 예외 필터는 WsException과 커스텀 필터를 사용하는 WebSocket 전용 구현이 필요합니다

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

풀스택 개발자, SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 10일 업데이트

공유

관련 기사