개발/React

리액트, Node JS - webSocket으로 로그인 유저 관리하기 (Managing Logged-in Users with WebSocket)

피로물든딸기 2024. 3. 27. 21:32
반응형

리액트 전체 링크

Node JS 전체 링크

 

참고

- webSocket으로 로그인 유저 관리하기

- Socket.IO로 로그인 유저 관리하기

 

다음과 같이 각 페이지 별로 로그인 한 사용자가 누구인지 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 messageMap에서 로그아웃한 클라이언트의 정보를 지우면 된다.

  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;
    }
  });

 

BroadCastLogout 함수는 다음과 같다.

  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([]);

 

그리고 webSocketcurrentUser는 전역으로 선언한다.

 

currentUseruseEffectcleanup 함수에서 사용하기 위해 전역으로 선언한다.

리액트 라우터 페이지에서 나간다고 해서 socket 연결이 끊기는 것이 아니기 때문에

useEffectcleanup에서 로그아웃 message를 보낸다.

let webSocket = new WebSocket("ws://localhost:3333");
let currentUser = ""; // for useEffect cleanup function

const WebSocketClient = () => {

 

useEffect(= init)는 다음과 같다.

onmessageServer에서 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;
반응형