실시간 (Socket.IO)
ASAPJS는 @asapjs/socket 패키지를 통해 Socket.IO 를 래핑합니다. 소켓 이벤트 핸들러는 createSocket()을 사용하여 *Socket.ts 파일에 선언되고, 시작 시 자동으로 검색되며, 연결된 모든 클라이언트에 자동으로 바인딩됩니다. 선택적 Redis 어댑터를 통해 여러 서버 인스턴스에 걸친 수평 확장이 가능합니다.
Socket.IO 레이어를 활성화하려면 config의 extensions 배열에 '@asapjs/socket'을 추가하세요. 전체 config 레퍼런스는 부트스트랩을 참조하세요.
타입
RouteRequest
소켓 핸들러가 수신하는 이벤트를 설명합니다.
interface RouteRequest {
path: string; // 소켓 이벤트 이름, 예: 'chat:message'
roles?: string[]; // 향후 역할 기반 필터링을 위해 예약됨
}ExecuteFunction
소켓 이벤트의 핸들러 함수 시그니처입니다.
type ExecuteFunction = (
socket: Socket,
args: { body: any; user: any }
) => Promise<unknown> | unknown | void;| 파라미터 | 타입 | 설명 |
|---|---|---|
socket | Socket | 연결된 클라이언트의 Socket.IO Socket 인스턴스. 이 클라이언트에게 다시 내보내거나, 룸에 참가하거나, 다른 클라이언트에게 브로드캐스트하는 데 사용합니다. |
args.body | any | 클라이언트가 이벤트를 내보낼 때 전송하는 페이로드. |
args.user | any | 소켓의 HTTP 업그레이드 요청의 JWT에서 디코딩된 인증된 사용자 객체(socket.request.user로 사용 가능). |
SocketOption
소켓 모듈이 getConfig().socket에서 읽는 설정 객체입니다.
interface SocketOption {
adapter?: unknown; // 커스텀 Socket.IO 어댑터 (고급 사용)
listener?: (socket: unknown) => void; // 원시 연결 훅
redis?: RedisClientOptions; // pub/sub 어댑터를 위한 Redis 클라이언트 설정
}| 필드 | 타입 | 설명 |
|---|---|---|
adapter | unknown | 커스텀 Socket.IO 어댑터 인스턴스. 거의 필요하지 않습니다. 내장 Redis 어댑터에는 redis를 사용하세요. |
listener | (socket) => void | 이벤트 핸들러가 등록되기 전 모든 새 연결마다 호출됩니다. 소켓별 일회성 설정에 유용합니다. |
redis | RedisClientOptions | redis npm 패키지의 Redis 클라이언트 옵션. 제공되면 모듈이 pub/sub 클라이언트 쌍을 생성하고 @socket.io/redis-adapter를 연결합니다. |
createSocket()
import { createSocket } from '@asapjs/socket';
createSocket(request: RouteRequest, execute: ExecuteFunction): void소켓 이벤트 핸들러를 등록합니다. *Socket.ts 파일의 모듈 스코프에서 이 함수를 호출하세요. 모듈은 initSocketModule이 자동으로 로드하므로 클라이언트가 연결되기 전에 모든 createSocket 호출이 등록됩니다.
| 파라미터 | 타입 | 설명 |
|---|---|---|
request | RouteRequest | 수신할 소켓 이벤트 이름을 지정하는 path 필드를 가진 객체. |
execute | ExecuteFunction | 연결된 클라이언트가 해당 이벤트를 내보낼 때마다 호출되는 비동기 또는 동기 핸들러. |
클라이언트가 연결되면 프레임워크는 등록된 각 createSocket 항목에 대해 socket.on(request.path, handler)를 호출합니다.
socketSendTo()
import { socketSendTo } from '@asapjs/socket';
socketSendTo(to: string, event: string, data: any): void특정 소켓 ID 또는 룸 이름에 data와 함께 event를 내보냅니다. io.to(to).emit(event, data)에 위임합니다.
| 파라미터 | 타입 | 설명 |
|---|---|---|
to | string | 소켓 ID(특정 클라이언트 대상) 또는 룸 이름(해당 룸의 모든 클라이언트 대상). |
event | string | 수신 클라이언트가 수신하는 이벤트 이름. |
data | any | 전송할 페이로드. JSON 직렬화 가능해야 합니다. |
// 소켓 ID로 단일 클라이언트에 전송
socketSendTo(socket.id, 'notification', { message: 'Your order shipped' });
// 룸의 모든 클라이언트에 전송
socketSendTo('room:lobby', 'chat:message', { text: 'Hello everyone' });socketSendAll()
import { socketSendAll } from '@asapjs/socket';
socketSendAll(event: string, data: any): void모든 연결된 클라이언트에 data와 함께 event를 브로드캐스트합니다. io.emit(event, data)에 위임합니다.
| 파라미터 | 타입 | 설명 |
|---|---|---|
event | string | 이벤트 이름. |
data | any | 브로드캐스트할 페이로드. |
// 모든 연결된 클라이언트에 시스템 공지 브로드캐스트
socketSendAll('system:announcement', { message: 'Scheduled maintenance in 5 minutes' });getSocketIO()
import { getSocketIO } from '@asapjs/socket';
const io = getSocketIO(); // Socket.IO Server | undefined 반환전역 Socket.IO Server 인스턴스를 반환합니다. 소켓 모듈이 아직 초기화되지 않은 경우 undefined를 반환합니다.
네임스페이스 생성, 연결된 소켓 검사, 서버 레벨에서 미들웨어 사용 등 고급 작업을 위해 Socket.IO 서버에 직접 접근할 때 getSocketIO()를 사용하세요.
const io = getSocketIO();
if (io) {
const sockets = await io.fetchSockets();
console.log(`Connected clients: ${sockets.length}`);
}initSocketModule()
import { initSocketModule } from '@asapjs/socket';
const initSocketModule = async (
server: http.Server,
dirname: string
): Promise<boolean>Socket.IO 모듈을 초기화합니다.
| 파라미터 | 타입 | 설명 |
|---|---|---|
server | http.Server | Socket.IO가 바인딩할 Node.js HTTP 서버 인스턴스. Express가 사용하는 것과 동일한 서버입니다. |
dirname | string | 컴파일된 출력 디렉토리의 절대 경로. *Socket.js 파일을 찾기 위해 모든 하위 디렉토리를 재귀적으로 스캔합니다. |
반환값: 성공 시 Promise<true>.
참고:
Application.run()은 내부적으로@asapjs/socket에서SocketPlugin클래스를 import하려고 시도하지만, 소켓 패키지는 현재SocketPlugin을 export하지 않습니다. 소켓 패키지가 export하는 초기화 함수는initSocketModule입니다. 플러그인 패턴을 통한 자동 초기화가 실패할 수 있으므로, 수동으로initSocketModule을 호출해야 할 수 있습니다.
초기화 단계:
*Socket.js에 해당하는 파일을 찾기 위해dirname을 재귀적으로 스캔합니다(.map으로 끝나는 파일은 건너뜁니다). 매칭되는 각 파일은require()되어 최상위 레벨의createSocket()호출이 실행되고 이벤트 핸들러가 등록됩니다.socketInit(server, getConfig().socket)을 호출하여SocketServer인스턴스를 생성합니다.config.socket.redis가 설정된 경우 Redis pub 클라이언트와 duplicate sub 클라이언트를 생성하고 둘 다 연결한 후@socket.io/redis-adapter를 Socket.IO 서버에 연결합니다.connection이벤트 핸들러를 등록합니다. 새 연결마다socket.request.user를 읽고 등록된 모든 이벤트 핸들러를 소켓에 바인딩합니다.
파일 네이밍 규칙:
소켓 핸들러 파일은 Socket.ts(소스) / Socket.js(컴파일됨)로 끝나야 합니다. 예: ChatSocket.ts, NotificationSocket.ts. 파일은 dirname 아래 어떤 깊이에도 배치할 수 있습니다.
설정에서 활성화
extensions에 '@asapjs/socket'을 추가하고 config 최상위 레벨에 socket 키를 제공하세요:
const config = {
// ... 다른 필드
extensions: ['@asapjs/sequelize', '@asapjs/socket'],
// Socket.IO 설정 (단일 서버 모드 — Redis 없음)
socket: {},
// Socket.IO 설정 (Redis 어댑터를 사용한 다중 서버 모드)
// socket: {
// redis: {
// socket: {
// host: process.env.REDIS_HOST || 'localhost',
// port: parseInt(process.env.REDIS_PORT || '6379', 10),
// },
// },
// },
};socket.redis가 없거나 undefined이면 모듈은 기본 인메모리 어댑터로 단일 서버 모드로 실행됩니다.
전체 예제
소켓 핸들러 파일
// 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 });
}
);애플리케이션 진입점
// src/index.ts
import 'reflect-metadata';
import dotenv from 'dotenv';
import { Application } from '@asapjs/core';
dotenv.config();
const config = {
name: 'My App',
debug: false,
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: 6379,
},
},
},
};
const app = new Application(__dirname, config);
app.run();클라이언트 측 (브라우저 / 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('Connected:', socket.id);
});
socket.on('success', (data) => {
console.log('Handshake:', 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}`);
});모든 새 연결에서 서버는 연결하는 클라이언트에게 { message: 'success connected' }와 함께 success 이벤트를 내보냅니다.