리액트, Node JS - Socket.IO로 로그인 유저 관리하기 (Managing Logged-in Users with Socket.IO)
참고
- Socket.IO로 로그인 유저 관리하기
- Socket.IO로 Toast UI Editor 동시 편집하기
이전 글에서 webSocket을 이용하여 로그인 유저를 처리하였다.
여기서는 socket.io를 사용해 보자.
Socket.IO Server
Node 서버에서 socket.io를 사용하기 위해 라이브러리를 설치한다.
npm install socket.io
예시 코드는 다음과 같다.
const { Server } = require("socket.io");
const io = new Server("3333", {
cors: {
origin: "http://localhost:3000",
},
});
io.sockets.on("connection", (socket) => {
socket.on("login", () => { // 사용자 정의
});
socket.on("logout", () => { // 사용자 정의
});
socket.on("disconnect", () => {
});
});
참고로 cors의 origin 설정 시, http://localhost:3000/ 끝에 "/"가 있는 경우 에러가 발생할 수 있다.
cors: {
origin: "http://localhost:3000/", // error
},
리액트(클라이언트)에서 "login"을 emit 하고 parameter로 user를 보낸다고 가정하자.
webSocket.emit("login", user);
webSocket에서는 type으로 case를 나눴지만, socket.io의 on에서 login을 정의해서 처리하면 된다.
socket.on("login", (data) => {
const clientId = socket.id;
loginInfo.set(clientId, {
userId: data,
timestamp: new Date(),
});
io.sockets.emit("loginRespond", { // 모든 클라이언트에 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
또한 클라이언트 ID를 따로 만들 필요 없이 socket.io에서 직접 만들어준다.
clientId = socket.id;
리액트에서 서버와 연결될 때, id가 생성되는 것을 확인할 수 있다.
그리고 서버에서 emit된 loginResponde의 data는 리액트에서 callback 함수로 처리할 수 있다.
socketIO.on("loginRespond", callback);
webSocket 서버에서 처리했던 로직은 socket.io에서 아래와 같이 구현될 수 있다.
로그아웃을 한 클라이언트에게는 메시지를 보낼 필요가 없으므로, broadcast.emit을 이용하였다.
io.sockets.on("connection", (socket) => {
socket.on("login", (data) => {
const clientId = socket.id;
loginInfo.set(clientId, {
userId: data,
timestamp: new Date(),
});
io.sockets.emit("loginRespond", { // 모든 클라이언트에 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
socket.on("logout", (clientId) => {
loginInfo.delete(clientId);
socket.broadcast.emit("logoutRespond", { // 자기 자신을 제외하고 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
socket.on("disconnect", () => {
const clientId = socket.id;
loginInfo.delete(clientId);
socket.broadcast.emit("logoutRespond", { // 자기 자신을 제외하고 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
});
Socket.IO Client
socket.io 라이브러리를 설치한다.
npm install socket.io-client
login과 logout 예시는 다음과 같다.
useEffect(() => {
if (!socketIO) return;
socketIO.on("loginRespond", respondCallback);
return () => {
socketIO.off("loginRespond", respondCallback);
};
}, []);
useEffect(() => {
if (!socketIO) return;
socketIO.on("logoutRespond", respondCallback);
return () => {
socketIO.off("logoutRespond", respondCallback);
socketIO.emit("logout", socketIO.id);
};
}, []);
로그인, 로그아웃으로 서버에서 로그인된 유저를 처리하므로 callback 함수는 두 경우 동일하다.
const respondCallback = (message) => {
let { loginInfo } = message;
let value = Object.values(loginInfo);
setUserList([...value]);
}
전체 코드는 다음과 같다.
socketIOServer.js
const { Server } = require("socket.io");
const io = new Server("3333", {
cors: {
origin: "http://localhost:3000",
},
});
const loginInfo = new Map();
io.sockets.on("connection", (socket) => {
socket.on("login", (data) => {
const clientId = socket.id;
loginInfo.set(clientId, {
userId: data,
timestamp: new Date(),
});
io.sockets.emit("loginRespond", { // 모든 클라이언트에 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
socket.on("logout", (clientId) => {
loginInfo.delete(clientId);
socket.broadcast.emit("logoutRespond", { // 자기 자신을 제외하고 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
socket.on("disconnect", () => {
const clientId = socket.id;
loginInfo.delete(clientId);
socket.broadcast.emit("logoutRespond", { // 자기 자신을 제외하고 전송
loginInfo: Object.fromEntries([...loginInfo]),
});
});
});
SocketIOClient.js
import React, { useEffect, useState } from "react";
import { io } from "socket.io-client";
let socketIO = io("http://localhost:3333");
const SocketIOClient = () => {
const [user, setUser] = useState("");
const [userList, setUserList] = useState([]);
const [loginCheck, setLoginCheck] = useState(false);
const respondCallback = (message) => {
let { loginInfo } = message;
let value = Object.values(loginInfo);
setUserList([...value]);
}
const login = () => {
if (user === "") return;
setLoginCheck(true);
socketIO.emit("login", user);
};
useEffect(() => {
if (!socketIO) return;
console.log(socketIO);
socketIO.on("loginRespond", respondCallback);
return () => {
socketIO.off("loginRespond", respondCallback);
};
}, []);
useEffect(() => {
if (!socketIO) return;
socketIO.on("logoutRespond", respondCallback);
return () => {
socketIO.off("logoutRespond", respondCallback);
socketIO.emit("logout", socketIO.id);
};
}, []);
return (
<div style={{ margin: 10 }}>
<input
value={user}
onChange={(e) => setUser(e.target.value)}
disabled={loginCheck}
/>
<button onClick={login} disabled={loginCheck}>
login
</button>
<div>
<p>현재 로그인한 사람</p>
{userList.map((item, index) => (
<p key={index}>
{item.userId === user ? `${user} [ME]` : item.userId} (
{item.timestamp}){" "}
</p>
))}
</div>
</div>
);
};
export default SocketIOClient;