Skip to Content
문서가이드실시간 소켓 통신

실시간 소켓 통신

ASAPJS는 @asapjs/socket 패키지를 통해 Socket.IO 를 래핑합니다. 소켓 이벤트 핸들러는 *Socket.ts 파일에서 createSocket()으로 선언하며, 시작 시 자동으로 발견되어 연결된 모든 클라이언트에 바인딩됩니다. Redis 어댑터를 사용하면 여러 서버 인스턴스 간 수평 확장이 가능합니다.

설정

extensions에 소켓 추가

config의 extensions 배열에 '@asapjs/socket'을 추가하고, 최상위에 socket 키를 제공합니다:

// src/index.ts const config = { name: 'My App', port: 3000, basePath: 'api', extensions: ['@asapjs/sequelize', '@asapjs/socket'], // 소켓 확장 활성화 // 단일 서버 모드 (Redis 없음) socket: {}, // ... 기타 설정 }; const app = new Application(__dirname, config); app.run();

Redis 어댑터 (다중 서버 모드)

여러 서버 인스턴스 간 소켓 이벤트를 공유하려면 socket.redis를 설정합니다:

const config = { // ... socket: { redis: { socket: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), }, }, }, };

Redis가 설정되면 소켓 모듈은 내부적으로 pub/sub 클라이언트 쌍을 생성하고 @socket.io/redis-adapter를 Socket.IO 서버에 연결합니다.

[!NOTE] socket.redis가 없거나 undefined이면 기본 인메모리 어댑터로 단일 서버 모드로 동작합니다.

초기화 흐름

Application.run() 호출 시 @asapjs/socket이 extensions에 포함되어 있으면 소켓 모듈 초기화를 시도합니다.

현재 알려진 이슈: Application.run()@asapjs/socket에서 SocketPlugin 클래스를 import하려고 시도하지만, 소켓 패키지는 SocketPlugin을 export하지 않습니다. 소켓 패키지가 제공하는 실제 초기화 함수는 initSocketModule(server, dirname)입니다. 플러그인 패턴을 통한 자동 초기화가 실패할 수 있으므로, 소켓 기능을 사용하려면 initSocketModule을 직접 호출해야 할 수 있습니다:

import { Application } from '@asapjs/core'; import { initSocketModule } from '@asapjs/socket'; const app = new Application(__dirname, config); const expressApp = await app.run(); // 소켓 모듈 수동 초기화 const server = app.getServer(); await initSocketModule(server, __dirname);

initSocketModule()의 내부 동작:

initSocketModule(server, dirname) ├─ loadPath(dirname, '') // *Socket.js 파일 재귀 탐색 → createSocket() 실행 └─ socketInit(server, config.socket) ├─ new SocketServer(server) ├─ Redis 어댑터 설정 (선택) └─ connection 이벤트 핸들러 등록

loadPath()는 컴파일된 출력 디렉토리에서 *Socket.js 파일을 재귀적으로 찾아 require()합니다. 이로 인해 각 파일의 최상위 createSocket() 호출이 실행되어 이벤트 핸들러가 등록됩니다.

이벤트 핸들러 작성

createSocket()

createSocket()은 소켓 이벤트 핸들러를 등록합니다. *Socket.ts 파일의 모듈 스코프에서 호출합니다:

import { createSocket } from '@asapjs/socket'; createSocket(request: RouteRequest, execute: ExecuteFunction): void
파라미터타입설명
requestRouteRequestpath 필드에 수신할 이벤트 이름을 지정
executeExecuteFunction이벤트 수신 시 호출되는 핸들러 함수

타입 정의

interface RouteRequest { path: string; // 소켓 이벤트 이름 (예: 'chat:message') roles?: string[]; // 역할 기반 필터링 (예약) } type ExecuteFunction = ( socket: Socket, args: { body: any; user: any } ) => Promise<unknown> | unknown | void;
파라미터타입설명
socketSocket연결된 클라이언트의 Socket.IO 인스턴스. emit, join, broadcast 등에 사용
args.bodyany클라이언트가 이벤트 전송 시 포함한 페이로드
args.userany소켓의 HTTP 업그레이드 요청에서 디코딩된 JWT 인증 사용자 객체 (socket.request.user)

핸들러 파일 예시

// src/chat/ChatSocket.ts import { createSocket, socketSendAll, socketSendTo } from '@asapjs/socket'; // 'chat:message' 이벤트 수신 — 모든 클라이언트에 브로드캐스트 createSocket( { path: 'chat:message' }, async (socket, { body, user }) => { const payload = { from: user?.id ?? 'anonymous', text: body.text, timestamp: new Date().toISOString(), }; socketSendAll('chat:message', payload); } ); // 'chat:whisper' — 특정 클라이언트에게만 전송 createSocket( { path: 'chat:whisper' }, async (socket, { body, user }) => { const { targetSocketId, text } = body; socketSendTo(targetSocketId, 'chat:whisper', { from: user?.id, text, timestamp: new Date().toISOString(), }); } ); // 'room:join' — 방 참가 및 알림 createSocket( { path: 'room:join' }, async (socket, { body, user }) => { const { roomId } = body; await socket.join(roomId); // 방의 다른 멤버에게 알림 socketSendTo(roomId, 'room:member_joined', { userId: user?.id, roomId, }); // 참가한 클라이언트에게 확인 socket.emit('room:joined', { roomId }); } );

파일 명명 규칙

소켓 핸들러 파일은 반드시 Socket.ts (소스) / Socket.js (컴파일)로 끝나야 합니다:

올바른 예잘못된 예
ChatSocket.tschat.ts
NotificationSocket.tssocketHandler.ts
deep/path/OrderSocket.tsOrderEvents.ts

파일은 dirname 하위 어떤 깊이에도 위치할 수 있습니다.

메시지 전송 API

socketSendTo() — 특정 대상에게 전송

import { socketSendTo } from '@asapjs/socket'; socketSendTo(to: string, event: string, data: any): void
파라미터타입설명
tostring소켓 ID (특정 클라이언트) 또는 방 이름 (해당 방의 모든 클라이언트)
eventstring수신 측이 리스닝하는 이벤트 이름
dataanyJSON 직렬화 가능한 페이로드
// 특정 클라이언트에게 전송 socketSendTo(socket.id, 'notification', { message: '주문이 발송되었습니다' }); // 방의 모든 클라이언트에게 전송 socketSendTo('room:lobby', 'chat:message', { text: '안녕하세요' });

socketSendAll() — 전체 브로드캐스트

import { socketSendAll } from '@asapjs/socket'; socketSendAll(event: string, data: any): void
파라미터타입설명
eventstring이벤트 이름
dataany브로드캐스트할 페이로드
// 모든 연결된 클라이언트에게 시스템 공지 socketSendAll('system:announcement', { message: '5분 후 예정된 점검이 있습니다', });

getSocketIO() — Socket.IO 서버 인스턴스 접근

import { getSocketIO } from '@asapjs/socket'; const io = getSocketIO(); // Socket.IO Server | undefined

socketInit() 이전에 호출하면 undefined를 반환합니다. 네임스페이스 생성, 연결된 소켓 조회 등 고급 작업에 사용합니다:

const io = getSocketIO(); if (io) { const sockets = await io.fetchSockets(); console.log(`연결된 클라이언트: ${sockets.length}`); }

연결 이벤트

클라이언트가 연결되면 서버는 자동으로 다음 작업을 수행합니다:

  1. socket.request.user에서 인증 사용자 정보를 추출
  2. success 이벤트를 연결한 클라이언트에게 emit: { message: 'success connected' }
  3. 등록된 모든 createSocket() 핸들러를 해당 소켓에 바인딩

클라이언트 연결 예시

// 브라우저 또는 Node.js 클라이언트 import { io } from 'socket.io-client'; const socket = io('http://localhost:3000', { auth: { token: 'Bearer <your-jwt-token>' }, }); socket.on('connect', () => { console.log('연결됨:', socket.id); }); socket.on('success', (data) => { console.log('핸드셰이크:', data.message); // 'success connected' }); // 방 참가 socket.emit('room:join', { roomId: 'room:lobby' }); // 메시지 전송 socket.emit('chat:message', { text: 'Hello, world!' }); // 브로드캐스트 메시지 수신 socket.on('chat:message', (payload) => { console.log(`[${payload.timestamp}] User ${payload.from}: ${payload.text}`); });

전체 구성 예시

서버 엔트리 포인트

// src/index.ts import 'reflect-metadata'; import dotenv from 'dotenv'; import { Application } from '@asapjs/core'; dotenv.config(); const config = { name: 'My App', port: 3000, basePath: 'api', extensions: ['@asapjs/sequelize', '@asapjs/socket'], auth: { jwt_access_token_secret: process.env.JWT_SECRET, }, db: { /* ... */ }, socket: { redis: { socket: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), }, }, }, }; const app = new Application(__dirname, config); app.run();

소켓 핸들러

// src/notification/NotificationSocket.ts import { createSocket, socketSendTo } from '@asapjs/socket'; createSocket( { path: 'notification:subscribe' }, async (socket, { body, user }) => { // 사용자별 방에 참가 const userRoom = `user:${user.id}`; await socket.join(userRoom); socket.emit('notification:subscribed', { room: userRoom }); } );

REST 컨트롤러에서 소켓 사용

HTTP 요청 처리 중에 소켓 메시지를 전송할 수 있습니다:

// src/post/controller/PostController.ts import { RouterController, Post, ExecuteArgs } from '@asapjs/router'; import { socketSendAll } from '@asapjs/socket'; export default class PostController extends RouterController { public tag = 'Post'; public basePath = '/posts'; @Post('/', { title: '게시글 작성', auth: true }) async createPost({ body, user }: ExecuteArgs) { const post = await this.postService.createPost(user, body); // 새 게시글 알림을 모든 클라이언트에게 브로드캐스트 socketSendAll('post:created', { id: post.id, title: post.title, author: user.id, }); return post; } }

SocketOption 설정

interface SocketOption { adapter?: unknown; // 커스텀 Socket.IO 어댑터 (고급) listener?: (socket: unknown) => void; // 연결 시 호출되는 원시 훅 redis?: RedisClientOptions; // Redis pub/sub 설정 }
필드타입설명
adapterunknown커스텀 Socket.IO 어댑터. redis 옵션이 있으면 자동으로 Redis 어댑터가 사용되므로 일반적으로 불필요
listener(socket) => void새 연결마다 이벤트 핸들러 등록 전에 호출되는 콜백
redisRedisClientOptionsredis npm 패키지의 클라이언트 옵션. 설정 시 pub/sub 어댑터 자동 구성

관련 문서

  • Real-time API 레퍼런스createSocket, socketSendTo, socketSendAll 상세 API
  • BootstrapApplication.run(), extensions 배열, SocketOption 설정
  • 인증 — JWT 미들웨어와 socket.request.user
Last updated on