실시간 소켓 통신
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| 파라미터 | 타입 | 설명 |
|---|---|---|
request | RouteRequest | path 필드에 수신할 이벤트 이름을 지정 |
execute | ExecuteFunction | 이벤트 수신 시 호출되는 핸들러 함수 |
타입 정의
interface RouteRequest {
path: string; // 소켓 이벤트 이름 (예: 'chat:message')
roles?: string[]; // 역할 기반 필터링 (예약)
}
type ExecuteFunction = (
socket: Socket,
args: { body: any; user: any }
) => Promise<unknown> | unknown | void;| 파라미터 | 타입 | 설명 |
|---|---|---|
socket | Socket | 연결된 클라이언트의 Socket.IO 인스턴스. emit, join, broadcast 등에 사용 |
args.body | any | 클라이언트가 이벤트 전송 시 포함한 페이로드 |
args.user | any | 소켓의 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.ts | chat.ts |
NotificationSocket.ts | socketHandler.ts |
deep/path/OrderSocket.ts | OrderEvents.ts |
파일은 dirname 하위 어떤 깊이에도 위치할 수 있습니다.
메시지 전송 API
socketSendTo() — 특정 대상에게 전송
import { socketSendTo } from '@asapjs/socket';
socketSendTo(to: string, event: string, data: any): void| 파라미터 | 타입 | 설명 |
|---|---|---|
to | string | 소켓 ID (특정 클라이언트) 또는 방 이름 (해당 방의 모든 클라이언트) |
event | string | 수신 측이 리스닝하는 이벤트 이름 |
data | any | JSON 직렬화 가능한 페이로드 |
// 특정 클라이언트에게 전송
socketSendTo(socket.id, 'notification', { message: '주문이 발송되었습니다' });
// 방의 모든 클라이언트에게 전송
socketSendTo('room:lobby', 'chat:message', { text: '안녕하세요' });socketSendAll() — 전체 브로드캐스트
import { socketSendAll } from '@asapjs/socket';
socketSendAll(event: string, data: any): void| 파라미터 | 타입 | 설명 |
|---|---|---|
event | string | 이벤트 이름 |
data | any | 브로드캐스트할 페이로드 |
// 모든 연결된 클라이언트에게 시스템 공지
socketSendAll('system:announcement', {
message: '5분 후 예정된 점검이 있습니다',
});getSocketIO() — Socket.IO 서버 인스턴스 접근
import { getSocketIO } from '@asapjs/socket';
const io = getSocketIO(); // Socket.IO Server | undefinedsocketInit() 이전에 호출하면 undefined를 반환합니다. 네임스페이스 생성, 연결된 소켓 조회 등 고급 작업에 사용합니다:
const io = getSocketIO();
if (io) {
const sockets = await io.fetchSockets();
console.log(`연결된 클라이언트: ${sockets.length}`);
}연결 이벤트
클라이언트가 연결되면 서버는 자동으로 다음 작업을 수행합니다:
socket.request.user에서 인증 사용자 정보를 추출success이벤트를 연결한 클라이언트에게 emit:{ message: 'success connected' }- 등록된 모든
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 설정
}| 필드 | 타입 | 설명 |
|---|---|---|
adapter | unknown | 커스텀 Socket.IO 어댑터. redis 옵션이 있으면 자동으로 Redis 어댑터가 사용되므로 일반적으로 불필요 |
listener | (socket) => void | 새 연결마다 이벤트 핸들러 등록 전에 호출되는 콜백 |
redis | RedisClientOptions | redis npm 패키지의 클라이언트 옵션. 설정 시 pub/sub 어댑터 자동 구성 |
관련 문서
- Real-time API 레퍼런스 —
createSocket,socketSendTo,socketSendAll상세 API - Bootstrap —
Application.run(),extensions배열,SocketOption설정 - 인증 — JWT 미들웨어와
socket.request.user