최근 프로젝트에서 실시간 폴더 트리 동기화 기능을 구현하면서 NestJS의 WebSocket 게이트웨이와 EventEmitter를 활용한 아키텍처를 구성했습니다. 이 과정에서 발견한 중요한 설정과 구조에 대해 정리해보려고 합니다.

전체 아키텍처 구조

현재 프로젝트의 실시간 동기화는 다음과 같은 구조로 동작합니다:

Client ← WebSocket → Gateway ← EventEmitter ← Service (DB 변경)  

1. WebSocket Gateway (websocket.gateway.ts)

/tree 네임스페이스를 사용하여 폴더 트리 전용 WebSocket 연결을 관리합니다:

@WebSocketGateway({  
    cors: {  
        origin: '*',  
        methods: ['GET', 'POST'],  
        credentials: true,  
    },  
    namespace: '/tree',  
    transports: ['websocket', 'polling'],  
})  
export class WebsocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {  
    @WebSocketServer()  
    server: Server;  
  
    // Room 기반 접근: private, public 구분  
    async handleJoinTree(payload: { organizationId: string; userId: string }, client: SocketWithUser) {  
        const privateRoom = `private:${organizationId}:${userId}`;  
        const publicRoom = `public:${organizationId}`;  
        await client.join(privateRoom);  
        await client.join(publicRoom);  
    }  
  
    // EventEmitter로부터 이벤트 수신  
    @OnEvent('folderTree.changed')  
    async handleTreeChanged(payload: { organizationId: string; userId?: string; type: 'PRIVATE' | 'PUBLIC' | 'ALL' }) {  
        // Room별 선택적 브로드캐스팅  
        if (payload.type === 'PRIVATE' && payload.userId) {  
            const privateRoom = `private:${payload.organizationId}:${payload.userId}`;  
            this.server.to(privateRoom).emit('tree-update', { /* 데이터 */ });  
        }  
        // ...    }  
}  

2. EventEmitter 기반 이벤트 시스템 (event.service.ts)

데이터 변경 시 WebSocket 게이트웨이에 이벤트를 전달하는 중간 계층입니다:

@Injectable()  
export class EventService {  
    constructor(private readonly eventEmitter: EventEmitter2) {}  
  
    emitFolderTreeEvent(type: 'PRIVATE' | 'PUBLIC' | 'ALL', organizationId: string, userId?: string) {  
        this.emit('folderTree.changed', { organizationId, userId, type });  
    }  
  
    // 노드 ID로 폴더타입을 조회하여 적절한 이벤트 발생  
    async emitFolderTreeEventById(nodeType: NodeType, id: number | string, userId?: string) {  
        // DB에서 폴더/문서 조회 후 적절한 room에 이벤트 전송  
        // ...    }  
}  

3. Room 기반 선택적 브로드캐스팅

Socket.IO의 Room 기능을 활용하여 효율적인 메시지 전송을 구현했습니다:

  • Private Room: private:{organizationId}:{userId} - 개인 폴더 변경사항
  • Public Room: public:{organizationId} - 공용 폴더 변경사항

이를 통해 불필요한 네트워크 트래픽을 줄이고, 보안을 강화할 수 있습니다.

🚨 중요: IoAdapter 설정의 필수성

NestJS에서 WebSocket을 사용할 때 가장 중요하면서도 놓치기 쉬운 설정이 바로 IoAdapter 설정입니다.

main.ts의 핵심 설정

// main.ts:25  
app.useWebSocketAdapter(new IoAdapter(app));  

⚠️ IoAdapter가 없을 때 발생하는 문제

이 한 줄의 설정이 빠지면 다음과 같은 문제들이 발생합니다:

  1. 조용한 실패: 별다른 에러 로그 없이 WebSocket 연결이 실패합니다.
  2. 서비스 시작 실패: 애플리케이션도 정상적으로 시작되지 않습니다.
  3. 클라이언트 연결 거부: 404 Not Found 또는 연결 타임아웃이 발생합니다

왜 이런 문제가 발생하는가?

NestJS는 기본적으로 HTTP 어댑터만 설정되어 있습니다. WebSocket Gateway를 사용하려면 반드시 WebSocket 어댑터를 명시적으로 설정해야 합니다. 이 설정이 없으면:

  • WebSocket 게이트웨이가 초기화되지 않음
  • Socket.IO 서버가 HTTP 서버와 연결되지 않음
  • 클라이언트의 WebSocket 연결 시도가 처리되지 않음

모듈 구조와 의존성

WebSocket 모듈 구성

@Module({  
    providers: [WebsocketGateway],  
    imports: [FolderTreeModule], // 폴더 트리 서비스 주입  
})  
export class WebSocketModule {}  

EventEmitter 설정

공통 모듈과 폴더 트리 모듈 모두에서 EventEmitter를 설정:

// common.module.ts  
@Module({  
    imports: [EventEmitterModule.forRoot()],    // ...})  
export class CommonModule {}  
  
// folder-tree.module.ts  @Module({  
    imports: [EventEmitterModule.forRoot()],    // ...})  
export class FolderTreeModule {}  

실시간 동기화 플로우

  1. 클라이언트 연결: 사용자가 /tree 네임스페이스로 연결
  2. Room 참여: join-tree 이벤트로 적절한 Room 참여
  3. 데이터 변경: 폴더/문서 생성/수정/삭제 시
  4. 이벤트 발생: EventService.emitFolderTreeEvent() 호출
  5. 게이트웨이 수신: @OnEvent('folderTree.changed') 핸들러 실행
  6. 선택적 브로드캐스트: 해당 Room의 클라이언트들에게만 업데이트 전송

트러블슈팅 팁

1. WebSocket 연결이 안 될 때

  • app.useWebSocketAdapter(new IoAdapter(app)) 설정 확인
  • CORS 설정 점검
  • 네트워크/방화벽 설정 확인

2. 이벤트가 전달되지 않을 때

  • EventEmitterModule 설정 확인
  • Room 참여 상태 확인
  • Gateway의 @OnEvent 핸들러 등록 확인

3. 메모리 누수 방지

  • 클라이언트 연결 해제 시 자동 Room 해제 (Socket.IO가 자동 처리)
  • 하트비트 대신 Socket.IO 내장 ping/pong 사용

마치며

NestJS의 WebSocket 게이트웨이는 강력한 실시간 기능을 제공하지만, IoAdapter 설정과 같은 기본 설정을 놓치면 디버깅하기 어려운 문제가 발생할 수 있습니다. 특히 별다른 에러 없이 조용히 실패하는 특성 때문에 초기 설정 시 각별한 주의가 필요합니다.

EventEmitter와 Room 기반 선택적 브로드캐스팅을 조합하면 효율적이고 확장 가능한 실시간 동기화 시스템을 구축할 수 있습니다.