리액트, Node JS - webSocket으로 로그인 유저 관리하기 (Managing Logged-in Users with WebSocket)
참고
- webSocket으로 로그인 유저 관리하기
다음과 같이 각 페이지 별로 로그인 한 사용자가 누구인지 web socket을 이용해서 관리해 보자.
Socket 라우터 페이지에서 ID를 입력하고 로그인 버튼을 누르면,
현재 페이지에 로그인 한 User ID를 모두 보여주고, 페이지를 나가거나, React App을 종료하면 로그아웃 한다.
Simple Login
리액트에서 로그인 버튼을 누르면 input과 버튼이 비활성화 되도록 하자.
import React, { useState } from "react";
const WebSocketClient = () => {
const [user, setUser] = useState("");
const [loginCheck, setLoginCheck] = useState(false);
const login = () => {
if (user === "") return;
setLoginCheck(true);
console.log(user);
};
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>
</div>
</div>
);
};
export default WebSocketClient;
login 버튼을 누르면 아래와 같이 비활성화 된다.
Web Socket Server
Node JS로 웹 소켓 서버를 만들기 위해 ws를 설치한다.
npm install ws
소켓 서버의 예시는 다음과 같다.
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 3333 });
wss.on("connection", (ws) => { // 최초 연결
ws.on("message", (res) => { // 메시지 전달
});
ws.on("close", () => { // 연결 종료
});
});
서버에서 로그인 정보와 소켓에 접속한 클라이언트를 관리할 Map을 선언한다.
const loginInfo = new Map();
const clients = new Map();
클라이언트에 unique한 ID를 할당하기 위해 아래의 함수를 만든다.
let ID_COUNTER = 0;
const generateClientId = () => {
return `CLIENT_${ID_COUNTER++}`;
};
연결(connection) 이벤트가 발생하면 클라이언트(리액트)의 ID가 할당된다.
wss.on("connection", (ws) => {
const clientId = generateClientId();
clients.set(clientId, ws);
console.log(`Client Login ${clientId}`);
...
message 이벤트는 다음과 같다.
먼저 type에 따라 메시지를 경우를 나눈다. (type은 리액트에서 설정한다.)
login message는 리액트에서 로그인 한 ID와 언제 로그인 했는지에 대한 timestamp를 만들면 된다.
그리고 해당 정보를 Map에 설정하고, 모든 클라이언트에게 보낸다. (handleBroadCast)
리액트에서 다루기 쉽게 Map을 오브젝트로 변경하였다. (Object.fromEntries([...loginInfo]))
logout message는 Map에서 로그아웃한 클라이언트의 정보를 지우면 된다.
ws.on("message", (res) => {
const { type, data } = JSON.parse(res);
switch (type) {
case "login":
loginInfo.set(clientId, {
userId: data,
timestamp: new Date(),
});
handleBroadCast(
JSON.stringify({
type: "login",
loginInfo: Object.fromEntries([...loginInfo]),
})
);
break;
case "logout":
handleLogout(clientId);
break;
default:
break;
}
});
BroadCast와 Logout 함수는 다음과 같다.
const handleBroadCast = (msg) => {
wss.clients.forEach(function each(client, i) { // 모든 클라이언트에게 send
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
};
const handleLogout = (clientId) => {
loginInfo.delete(clientId);
clients.delete(clientId);
handleBroadCast(
JSON.stringify({
type: "login",
loginInfo: Object.fromEntries([...loginInfo]),
})
);
};
참고 : 여기서는 전체 로그인 사용자를 확인하기 위해 모든 사용자에게 broadcasting 했지만,
만약 자기 자신은 data를 받을 수 없다면 조건문에 (client !== ws) 를 추가하면 된다.
if (client !== ws && client.readyState === WebSocket.OPEN) { ... }
마지막으로, 연결이 해제된 경우에도 로그아웃을 하면 된다.
ws.on("close", () => {
handleLogout(clientId);
console.log(`Client Logout ${clientId}`);
});
참고로 React App을 종료하면 close 이벤트가 발생하지만, 페이지가 바뀐다고 해서 close가 발생하지 않는다.
그래서 message에 logout을 따로 추가하였다.
Web Socket Client
유저 목록을 관리할 배열을 useState로 선언한다.
const [userList, setUserList] = useState([]);
그리고 webSocket과 currentUser는 전역으로 선언한다.
currentUser는 useEffect의 cleanup 함수에서 사용하기 위해 전역으로 선언한다.
리액트 라우터 페이지에서 나간다고 해서 socket 연결이 끊기는 것이 아니기 때문에
useEffect의 cleanup에서 로그아웃 message를 보낸다.
let webSocket = new WebSocket("ws://localhost:3333");
let currentUser = ""; // for useEffect cleanup function
const WebSocketClient = () => {
useEffect(= init)는 다음과 같다.
onmessage는 Server에서 BroadCast 된 메시지를 받아서 처리한다.
useEffect(() => {
if((webSocket.readyState !== WebSocket.OPEN)) { // 재연결을 위한 방어 코드
webSocket = new WebSocket("ws://localhost:3333");
}
webSocket.onopen = function () {
console.log("open", webSocket);
};
webSocket.onmessage = function (e) {
let { type, loginInfo } = JSON.parse(e.data);
let value = Object.values(loginInfo);
setUserList([...value]);
};
webSocket.onclose = function () { // react app이 종료되는 경우
console.log("socket closed");
};
return () => { // 다른 라우터로 이동하는 경우
let userData = {
type: "logout",
data: currentUser,
};
webSocket.send(JSON.stringify(userData));
webSocket.close();
};
}, []);
message를 통해서 받은 data를 이용해 아래와 같이 로그인 한 사용자를 갱신할 수 있다.
자신의 ID와 같은 경우에 [ME] 라고 표시하였다.
{userList.map((item, index) => (
<p key={index}>
{item.userId === user ? `${user} [ME]` : item.userId} (
{item.timestamp}){" "}
</p>
))}
전체 코드는 다음과 같다.
webSocketServer.js
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 3333 });
const loginInfo = new Map();
const clients = new Map();
let ID_COUNTER = 0;
const generateClientId = () => {
return `CLIENT_${ID_COUNTER++}`;
};
wss.on("connection", (ws) => {
const clientId = generateClientId();
clients.set(clientId, ws);
console.log(`Client Login ${clientId}`);
const handleBroadCast = (msg) => {
wss.clients.forEach(function each(client, i) { // 모든 클라이언트에게 send
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
};
const handleLogout = (clientId) => {
loginInfo.delete(clientId);
clients.delete(clientId);
handleBroadCast(
JSON.stringify({
type: "login",
loginInfo: Object.fromEntries([...loginInfo]),
})
);
};
ws.on("message", (res) => {
const { type, data } = JSON.parse(res);
switch (type) {
case "login":
loginInfo.set(clientId, {
userId: data,
timestamp: new Date(),
});
handleBroadCast(
JSON.stringify({
type: "login",
loginInfo: Object.fromEntries([...loginInfo]),
})
);
break;
case "logout":
handleLogout(clientId);
break;
default:
break;
}
});
ws.on("close", () => {
handleLogout(clientId);
console.log(`Client Logout ${clientId}`);
});
});
WebSocketClient.js
import React, { useEffect, useState } from "react";
let webSocket = new WebSocket("ws://localhost:3333");
let currentUser = ""; // for useEffect cleanup function
const WebSocketClient = () => {
const [user, setUser] = useState("");
const [userList, setUserList] = useState([]);
const [loginCheck, setLoginCheck] = useState(false);
const login = () => {
if (user === "") return;
setLoginCheck(true);
currentUser = user;
let userData = {
type: "login",
data: user,
};
webSocket.send(JSON.stringify(userData));
};
useEffect(() => {
if((webSocket.readyState !== WebSocket.OPEN)) { // 재연결을 위한 방어 코드
webSocket = new WebSocket("ws://localhost:3333");
}
webSocket.onopen = function () {
console.log("open", webSocket);
};
webSocket.onmessage = function (e) {
let { type, loginInfo } = JSON.parse(e.data);
let value = Object.values(loginInfo);
setUserList([...value]);
};
webSocket.onclose = function () { // react app이 종료되는 경우
console.log("socket closed");
};
return () => { // 다른 라우터로 이동하는 경우
let userData = {
type: "logout",
data: currentUser,
};
webSocket.send(JSON.stringify(userData));
webSocket.close();
};
}, []);
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 WebSocketClient;