저번 시간엔 websocket을 알아보았다. 저번 포스트에서 websocket보다는 socket.io라는 라이브러리를 쓰면 더 쉽게 양방향으로 통신하고 더 많은 기능을 사용할 수 있다고 썼었다. 오늘은 socket.io가 무엇인지에 대해, 웹소켓과 어떤 차이점이 있는지에 대해 알아보려고 한다. 양방향 통신 프로토콜이 무엇인지에 대해서는 저번 포스트 때 공부했기에 여기서는 socket.io에 대해서만 다루려고 한다.
TIL #10 WebSocket [22.07.10]
오해할수도 있지만 websocket안에 속해있는 라이브러리가 아니다. Socket.IO는 양방향 데이터 통신이 가능한 프로토콜이 맞긴하지만 만약 브라우저가 웹소켓을 지원하지 않는다거나, 해당 통신이 끊겼을 때 다른 방법으로 양방향 통신을 지원한다. 그러니까 웹소켓은 socket.io가 사용하는 하나의 방법일 뿐이라는 것!
websocket으로 연결했을 때는 서버 연결이 끊겼을 때 그것으로 끝이었다. 다시 직접 서버에 연결해줬어야 했는데 socket.io는 끊임없이 서버를 찾고 다시 자동으로 재접속해주는 기능이 있다.
websocket을 이용하여 서버에 연결했을 때는 message라는 고정된 이벤트밖에 사용하지 못했다. 하지만 socket.io에서는 직접 이벤트를 커스텀할 수 있다.
const wss = new WebSocket.Server({ server });
wss.on("connection", (ws, req) => {
ws.on("message", (msg) => { // message이벤트밖에 가능하지 않다.
console.log("유저의 메세지: " + msg);
ws.send("Server request")
})
})
const wsServer = SocketIO(httpServer);
wsServer.on("connection", (socket) => {
socket.on("enter_room", (roomName) => {
console.log(roomName);
});
});
disconnecting
io.on("connection", (socket) => {
socket.on("disconnecting", (reason) => {
console.log(socket.rooms); // Set { ... }
});
});
disconnect
io.on("connection", (socket) => {
socket.on("disconnect", (reason) => {
// ...
});
});
웹소켓에서는 string으로만 값을 서버로 전송할 수 있었다. 그래서 만약 객체로 보낼 시에는 JSON형식으로 보내 stringify, parse라는 복잡한 절차를 거쳐야 했는데, socket.io에서는 어떤 자료형이든지 그대로 전송할 수 있다.
function handleRoomSubmit(event) {
event.preventDefault();
const input = form.querySelector("input");
socket.emit("enter_room", { payload: input.value });
input.value = "";
}
argument개수도 상관 없이 원하는 만큼 값을 여러개 전송할 수 있다.
하지만 마지막 argument는 단순히 값을 전달하는 argument로 취급되지 않는다. 값을 전달하고자 한다면 마지막에 함수는 제외해야 한다.
// client
function backendDone(msg) {
console.log(msg);
}
function handleRoomSubmit(event) {
event.preventDefault();
const input = form.querySelector("input");
socket.emit("enter_room", { payload: input.value }, backendDone);
// 마지막 argument로 함수를 넣어주면 백엔드로 이 콜백함수가 전달된다.
input.value = "";
}
// server
wsServer.on("connection", (socket) => {
socket.on("enter_room", (roomName, done) => {
console.log(roomName);
setTimeout(() => { // 이 함수는 백엔드에 있고 호출되지만 프론트엔드에서 실행된다. 백엔드에서는 이 함수를 실행하지 않는다.
done("function controlled by server");
}, 5000);
});
});
wsServer.on("connection", (socket) => {
console.log(socket.rooms); // Set { <socket.id> } // socket.rooms하면 소켓 아이디를 알수 있다.
socket.join("room1");
console.log(socket.rooms); // Set { <socket.id>, "room1" }
});
여기서 socket.id는
A unique identifier for the session, that comes from the underlying Client.
그러니까 세션마다 바뀌는 유저의 아이디 식별자라고 이해했다.
join이라는 메소드만 쓰면 바로 방이 생성된다.
배열을 만들면 여러개의 방을 생성하는 것도 가능하다.
wsServer.on("connection", (socket) => {
socket.join("room 1");
console.log(socket.rooms); // Set { <socket.id>, "room 1" }
socket.join(["room 1", "room 2"]);
wsServer.to("room 1").emit("welcome"); // broadcast to everyone in the room
});
wsServer.on("connection", (socket) => {
// to one room
socket.to("others").emit("an event", { some: "data" });
// to multiple rooms
socket.to("room1").to("room2").emit("hello");
// or with an array
socket.to(["room1", "room2"]).emit("hello");
// a private message to another socket
socket.to(/* another socket id */).emit("hey");
// WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
// named `socket.id` but the sender. Please use the classic `socket.emit()` instead.
});
wsServer.on("connection", (socket) => {
socket.in("enter_room", (roomName) => {
socket.join(roomName);
socket.to(roomName).emit("welcome");
});
})
공식문서를 보니 socket.data라는 기능을 이용할 수 있다는데...
An arbitrary object that can be used in conjunction with the fetchSockets() utility method: 공식문서
해석해보면 socket이라는 객체를 fetchSockets()
메소드로 다른 곳에서 사용할 수 있나보다. 아래는 공식문서의 예시이다.
wsServer.on("connection", (socket) => {
socket.data.username = "alice";
}); // socket.data에 username이라는 키로 alice값을 부여
const sockets = await wsServer.fetchSockets(); // socket 상수 생성
console.log(sockets[0].data.username); // "alice"
리스너로 전달되는 파라미터가 socket자체가 기본적으로 객체이기 때문에 여기서 바로 키값을 부여해서 값을 지정하는 방법을 썼다.
// server
wsServer.on("connection", (socket) => {
socket["nickname"] = "Anonymous"; // 초기 닉네임 Anonymous로 설정
socket.on("nickname", (nickname) => (socket["nickname"] = nickname)); // nickname 변경 이벤트
socket.on("enter_room", (roomName) => {
socket.join(roomName); // 방 생성
socket.to(roomName).emit("welcome", socket.nickname); // welcome 이벤트에 닉네임 데이터 전달
});
});
// client
function handleNicknameSubmit(event) {
event.preventDefault();
const input = room.querySelector("input")
const value = input.value;
socket.emit("nickname", input.value);
}
form.addEventListener("submit", handleNicknameSubmit) // 닉네임 값 서버로 전달
socket.on("welcome", (nickname) => { // nickname 정보 전달받음
addMessage(`${nickname} arrived!`);
}); // 서버에서 프론트엔드로 전달받음, 다른 유저들에게도 [nickname]유저가 왔다고 알릴 수 있음.
개인적으로 그냥 socket자체에 키값을 넣는게 더 편한 것 같긴하다.
이것들 이외에도 socket.io에는 정말 다양한 기능들이 있다. 어떤 방에 강제로 입장하게 만들수도 있고, 특정 유저에게만 메시지를 보낼 수도 있다. 나머지는 또 공식문서를 보면서 공부해야겠다.
웹소켓의 send
와 달리 socket.io의 emit
는
첫번째 argument에 커스텀한 이벤트,
두번째 argument에 payload(백엔드에 보내고 싶은 값),
세번째 argument에 서버에서 호출할 수 있는 callback함수가 들어간다.
특히 프론트엔드에서 백엔드로 원하는 만큼, 어떤 자료형이든 값을 전달할 수 있다는 점과 백엔드에서 프론트엔드의 함수를 제어할 수도 있다는 점이 제일 재미있었고 신기했다. 오늘은 socket.io에서 제일 자주 쓰이는 것들을 정리해봤는데 공식문서에 이것 이외에도 정말 다양한 기능들이 많았다. 역시 쌍방향 연결만 해주고 기능은 단순한 websocket보다는 다양한 기능과 함께 더 연결이 끊길 걱정이 없는 socket.io라이브러리를 사용할 것 같다.👍