Socket.IO 4.8
개요
이 스킬은 Socket.IO 4.8에서 자주 실수하기 쉬운 규칙들을 다룹니다: 임시 socket.id를 사용자 키로 사용하기, 리스너가 두 번 등록되는 문제, timeout()에 따라 콜백 형태가 달라지는 acknowledgement, WebSocket을 보호하지 않는 CORS, 로드 밸런서 뒤에서 잊혀진 스티키 세션, 더 이상 작동하지 않는 v2 또는 v3 API 등. 각 주장은 출처인 페이지 URL을 함께 제공합니다. 참조 파일들은 도메인별 세부 정보를 담고 있습니다.
지원 버전
Socket.IO 4.8 (서버 socket.io, 클라이언트 socket.io-client), 문서 v4. JS 클라이언트 3.x 및 4.x는 서버 3.x 및 4.x와 통신 가능합니다. https://socket.io/docs/v4/client-installation/#version-compatibility
빠른 시작
- 생성한 HTTP 서버에 연결하고, Express
app이 아닌 해당 서버에서listen을 호출하세요:app.listen()은 Socket.IO가 연결되지 않은 다른 HTTP 서버를 생성합니다. https://socket.io/docs/v4/server-initialization/#with-express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: ["https://example.com"] } });
io.on("connection", (socket) => {
socket.on("hello", (arg, callback) => callback("got it"));
});
httpServer.listen(3000);
https://socket.io/docs/v4/server-initialization/#with-express
- 클라이언트: 동일 출처에서는
io(), 교차 출처(서버에서 CORS 활성화 필요)에서는io("https://server-domain.com")를 사용합니다.import { io } from "socket.io-client"로 임포트합니다. https://socket.io/docs/v4/client-initialization/#from-a-different-domain
import { io } from "socket.io-client";
const socket = io("https://server-domain.com", { auth: { token: "abc" } });
const answer = await socket.timeout(5000).emitWithAck("hello", "world");
https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args
- 타입은
socket.io-client와 함께 제공됩니다.@types/socket.io-client를 설치하지 마세요; 번들된 타입과 충돌합니다. https://socket.io/docs/v4/client-installation/#from-npm
규칙 및 함정
연결 및 식별
- Socket.IO는 WebSocket 구현체가 아닙니다: 일반 WebSocket 클라이언트는 Socket.IO 서버에 접속할 수 없고, Socket.IO 클라이언트는 일반 WebSocket 서버에 접속할 수 없습니다. https://socket.io/docs/v4/#what-socketio-is-not
- 연결 상태 복구가 활성화되지 않은 경우,
socket.id는 재연결 및 페이지 새로고침 시마다 변경되며, 탭마다 다르고 메시지 큐가 없습니다. 쿠키 또는auth에 담긴 자체 세션 ID로 사용자를 식별하고, 사용자 ID를 이름으로 하는 방을 통해 사용자의 모든 탭에 도달하세요. https://socket.io/docs/v4/server-socket-instance/#socketid
io.on("connection", async (socket) => {
const userId = await computeUserIdFromHeaders(socket.handshake.headers);
socket.join(userId);
io.to(userId).emit("hi");
});
https://socket.io/docs/v4/rooms/#sample-use-cases
- 연결 미들웨어에서
socket.handshake.auth로 인증합니다. 모든 코드 경로에서next()를 호출하세요; 그렇지 않으면 연결이 타임아웃되어 닫힐 때까지 대기 상태로 남습니다.next(err)는 클라이언트에connect_error로 전달되며,err.data에 세부 정보가 포함됩니다. https://socket.io/docs/v4/middlewares/
io.use((socket, next) => {
if (isValid(socket.handshake.auth.token)) return next();
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
https://socket.io/docs/v4/middlewares/#handling-middleware-error
- 미들웨어에 의해 거부된 연결은 재시도되지 않습니다.
connect_error에서socket.active === false는 서버에서 연결이 거부되었음을 의미하며, 클라이언트가 재연결을 위해 직접socket.connect()를 호출해야 합니다. https://socket.io/docs/v4/client-socket-instance/#connect_error - 클라이언트 리스너는 최상위 레벨에 등록하고, 절대
connect핸들러 내부에 등록하지 마세요:connect는 재연결 시마다 다시 실행되어 리스너가 중복 등록됩니다. https://socket.io/docs/v4/client-socket-instance/#connect - 서버에서는
connection핸들러 내에서await전에 동기적으로socket.on(...)을 등록하세요; 그렇지 않으면 초기 패킷이 처리되지 않습니다. https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Express 미들웨어는
io.engine.use(...)를 사용해야 하며,io.use(...)를 사용하지 않습니다. https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
이벤트 및 Acknowledgement
- 객체를 그대로 전달하세요. 절대
JSON.stringify()로 페이로드를 변환하지 마세요.Date는 문자열로 도착하며,Map과Set은 수동으로 변환해야 합니다. https://socket.io/docs/v4/emitting-events/#basic-emit - Ack 콜백은 마지막 인자입니다.
timeout()없으면(response)를 받고,timeout(ms)있으면(err, response)를 받습니다. https://socket.io/docs/v4/emitting-events/#with-timeout try블록 안에서await socket.timeout(ms).emitWithAck(...)을 선호하세요: 타임아웃 시 reject됩니다. 수신 측은 1개의 인자로 콜백을 호출합니다. https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args- Acknowledgement가 있는 브로드캐스트는
timeout()이 필요하며 클라이언트당 1개의 응답을 반환합니다. https://socket.io/docs/v4/server-api/#serveremiteventname-args
const responses = await io.timeout(10000).emitWithAck("some-event");
https://socket.io/docs/v4/server-api/#serveremitwithackeventname-args
- 리스너에는 내장된 오류 처리 및 유효성 검사가 없습니다. 각 핸들러에서 오류를 포착하고, 스키마 라이브러리로 페이로드를 검증하며, 콜백을 사용하기 전에
typeof callback === "function"을 확인하세요. https://socket.io/docs/v4/listening-to-events/#validation connect,connect_error,disconnect,disconnecting,newListener또는removeListener를 애플리케이션 이벤트로 절대 사용하지 마세요:socket.emit("disconnecting")과 같이 예약된 이름을 사용하면 오류가 발생합니다. https://socket.io/docs/v4/emit-cheatsheet/
브로드캐스팅 및 방
socket.to(room)과socket.broadcast는 발신자를 제외하며,io.to(room)은 제외하지 않습니다.socket.to(socket.id).emit()은 아무것도 전송하지 않습니다:io.to(socket.id).emit()을 사용하세요. https://socket.io/docs/v4/server-api/#sockettoroom- 연결된
to()호출은 합집합이며, 각 소켓은 이벤트를 한 번만 받습니다. https://socket.io/docs/v4/rooms/#joining-and-leaving - 각 브로드캐스트 수정자는 새로운 불변 연산자를 반환합니다:
io.to()를 호출한 후io.emit()을 호출하는 대신 변수에 저장하여 사용하세요. https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - 방 이름은 강제 변환되지 않습니다:
socket.join(42)는io.to("42")와 일치하지 않습니다. https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - 방은 연결 해제 시 자동으로 나가집니다.
disconnect에서는socket.rooms가 비어 있으므로disconnecting에서 읽으세요. https://socket.io/docs/v4/rooms/#disconnection io.of("/").adapter로 어댑터를 읽으세요: 문서는io.adapter()가 그 목적으로 작동하지 않을 것이라고 경고합니다. 이는 setter입니다. https://socket.io/docs/v4/adapter/#api
전달
- 기본 보장은 최대 한 번(at most once)이며 서버는 메시지를 저장하지 않습니다: 연결이 끊긴 클라이언트는 브로드캐스트를 영원히 놓칩니다.
auth에 보내진 오프셋을 사용하여 데이터베이스에서 재생하거나, 연결 상태 복구를 활성화하세요. https://socket.io/docs/v4/delivery-guarantees#at-most-once - 클라이언트는 오프라인 중에 emit을 버퍼링하고 재연결 시 플러시합니다.
socket.connected를 확인하거나 드롭 가능한 데이터에는socket.volatile.emit(...)을 사용하세요. https://socket.io/docs/v4/client-offline-behavior/#buffered-events - 클라이언트에서 서버로 최소 한 번(at least once):
io({ retries: 3, ackTimeout: 10000 }). 정확히 한 번(exactly once)을 위해서는 고유 제약 조건 아래에 클라이언트 생성 ID를 저장하세요. 성공 시와 중복 시에는 승인하지만, 다른 오류에서는 승인하지 않아 클라이언트가 타임아웃되어 재시도하도록 합니다. https://socket.io/docs/v4/tutorial/step-8 - 연결 상태 복구는 선택 사항이며, 제한된
maxDisconnectionDuration이 필요하고 실패할 수 있습니다:socket.recovered를 분기하여 확인하세요. Redis (Pub/Sub) 어댑터는 이를 지원하지 않으며, Redis Streams 어댑터는 지원합니다. https://socket.io/docs/v4/connection-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: false },
});
https://socket.io/docs/v4/connection-state-recovery#usage
CORS 및 배포
- CORS는 기본적으로 비활성화되어 있습니다. 옵션은
cors.origin이며, 절대origins가 아닙니다.origin: "*"와credentials: true는 브라우저에서 실패합니다: 명시적인 출처를 나열하세요. https://socket.io/docs/v4/server-options/#cors - CORS는 HTTP 롱폴링에서만 브라우저를 다루며, WebSocket은 다루지 않습니다.
allowRequest로 누가 연결할 수 있는지 제한하세요. https://socket.io/docs/v4/handling-cors/ - 프록시는
Upgrade및Connection헤더를 전달해야 하며, 읽기 타임아웃은pingInterval + pingTimeout(기본 45초)을 초과해야 합니다. 그렇지 않으면 클라이언트가transport close를 보게 됩니다. https://socket.io/docs/v4/reverse-proxy/#nginx - 여러 서버에서 HTTP 롱폴링이 활성화된 경우, 어떤 어댑터를 사용하든 스티키 세션이 여전히 필요합니다 (대안은 롱폴링 비활성화 또는 클라이언트 측 로드 밸런싱). https://socket.io/docs/v4/using-multiple-nodes/ 및 https://socket.io/docs/v4/adapter/
- 스티키 세션이 없으면 클라이언트는
{"code":1,"message":"Session ID unknown"}과 함께 HTTP 400 응답을 받습니다. https://socket.io/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup path(기본값/socket.io/)는 양쪽에서 일치해야 합니다.io("/my-path/")는 네임스페이스를 선택하며, 경로가 아닙니다. https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sidesio.close()로 종료하며, 이는 HTTP 서버도 닫습니다. https://socket.io/docs/v4/server-api/#serverclosecallback
참조 파일
| 파일 | 읽을 내용 |
|---|---|
| references/server-setup.md | 설치, Express/Koa/Fastify/Hono/HTTPS/HTTP/2/Bun/uWebSockets.js 초기화, 서버 옵션 및 기본값, 번들링, 핸들러 레이아웃 |
| references/middleware-and-auth.md | 연결, 소켓 및 엔진 미들웨어, 자격 증명, 사용자 식별, 헤더 훅, allowRequest, 클라이언트 인증서 |
| references/cors-and-proxies.md | CORS 옵션 및 오류, 쿠키, nginx, Apache, Caddy, http-proxy, 클라우드 로드 밸런서, ws와 포트 공유 |
| references/server-api.md | 서버 및 소켓 인스턴스, io의 네임스페이스, 유틸리티 메서드, serverSideEmit, 연결 해제 이유, 엔진 이벤트 및 오류 코드, 종료 |
| references/client.md | 번들, 초기화, 소켓 이벤트, connect_error 및 disconnect, Manager 및 다중화, 모든 클라이언트 옵션, 번들러 |
| references/events-rooms-namespaces.md | 직렬화, acknowledgement, catch-all 리스너, 유효성 검사, 오류 처리, 브로드캐스팅, 방, 동적 네임스페이스 |
| references/delivery-and-recovery.md | 전달 보장, 재시도, 정확히 한 번, 오프셋 재생, 연결 상태 복구 |
| references/multiple-nodes.md | 스티키 세션, 여러 노드에 대한 프록시 설정, 대안, Node.js 클러스터, PM2 |
| references/adapters.md | Redis, 샤딩된 Redis, Redis Streams, Postgres, MongoDB, Cluster, AWS SQS, Azure, GCP 어댑터, 기능 매트릭스, 이미터 |
| references/performance-and-operations.md | 네이티브 애드온, wsEngine, 메모리, 사용자 정의 파서, OS 제한, 부하 테스트, Admin UI, 디버그 로그 |
| references/typescript.md | 서버, 클라이언트 및 네임스페이스의 이벤트 인터페이스, SocketData, 타입화된 테스트 |
| references/testing-and-troubleshooting.md | 서버 통합 테스트, 가짜 소켓을 사용한 클라이언트 테스트, 연결 실패, 연결 해제, 롱폴링 중단 |
| references/migrations-and-protocol.md | v2에서 v3 및 v3에서 v4 변경 사항, allowEIO3을 사용한 롤링 업그레이드, Engine.IO 및 Socket.IO 와이어 프로토콜 |
이전 패턴
| 이전 사용법 | 대체 방법 | 출처 |
|---|---|---|
origins 또는 handlePreflightRequest 옵션 | cors: { origin: [...] } | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#cors-handling |
io.set("authorization", fn) | io.use((socket, next) => ...) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#ioset-is-removed |
Socket query 옵션의 토큰 | auth 옵션, socket.handshake.auth로 읽음 | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
연결 거부에 대한 클라이언트 error 이벤트 | connect_error, err.message 및 err.data 포함 | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Socket의 socket.on("reconnect_attempt") | Manager의 socket.io.on("reconnect_attempt") | https://socket.io/docs/v4/client-socket-instance/#events |
지연 시간 측정을 위한 클라이언트 pong 이벤트 | 간격을 두고 socket.volatile.emit("ping", cb) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#no-more-pong-event-for-retrieving-latency |
Namespace.connected 객체 | Namespace.sockets, Map | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Object.keys(socket.rooms) | socket.rooms는 Set | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.join(room, cb) | socket.join(room), 비동기 어댑터로 await | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Namespace.clients(cb) (v2), 이후 allSockets() (v3, 다음 주요 버전에서 제거 예정) | fetchSockets() | https://socket.io/docs/v4/server-api/#namespaceallsockets |
socket.binary(false) | 사용자 정의 parser | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.emit("a").emit("b") | 개별 호출, emit()은 true 반환 | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.io 또는 socket.io-client의 기본 import | import { Server } from "socket.io", import { io } from "socket.io-client" | https://socket.io/docs/v4/migrating-from-3-x-to-4-0/ |