개발/React

리액트, Node JS - Handsontable에 Socket.IO 적용하기 (Handsontable with Socket.IO)

피로물든딸기 2024. 4. 13. 02:05
반응형

리액트 전체 링크

Node JS 전체 링크

 

참고

- Socket.IO로 Toast UI Editor 동시 편집하기

 

스프레드시트 공동 작업하기 Project Settings
Handsontable에 Socket.IO 적용하기
변경된 셀만 데이터 전송해서 최적화하기
행 / 열 이동 및 크기 변경 연동하기
다른 클라이언트가 선택한 셀 표시하기

 

HandsontableSocket.IO를 적용해서 편집된 데이터를 공유해 보자.


Socket Server

 

소켓 서버Socket.IO로 Toast UI Editor 동시 편집하기와 동일하다.

기본적으로 전체 data를 broadcast로 전송하며, 최초 진입 클라이언트에게는 initData로 현재 데이터를 전송한다.

const { Server } = require("socket.io");

let currentData = undefined;

const io = new Server("3333", {
  cors: {
    origin: "http://localhost:3000",
  },
});

io.sockets.on("connection", (socket) => {
  socket.emit("initData", {    
    currentData,
  });

  socket.on("sendData", (data) => {    
    console.log(data);
    currentData = data;
    socket.broadcast.emit("respondData", currentData);
  });

  socket.on("disconnect", () => {});
});

Socket Client

 

table에 들어갈 datauseState로 추가한다.

  const [tableData, setTableData] = useState(data);
  
  ...
  
  <SimpleHandsOnTable
    data={tableData}
    customOptions={customOptions}
  />

 

그리고 소켓 클라이언트 객체를 추가하자.

import { io } from "socket.io-client";

let socketIO = io("http://localhost:3333", { autoConnect: false });

 

테이블에서 사용되는 2D Array를 통째로 보낼 예정이므로,

다른 클라이언트는 이 데이터를 받아 setTableData갱신하기 위해 콜백 함수를 추가한다.

  const respondDataCallback = (newData) => {
    setTableData(newData);
  };

  useEffect(() => {
    socketIO.connect();
    if (!socketIO) return;

    socketIO.on("respondData", respondDataCallback);

    return () => {
      socketIO.off("respondData", respondDataCallback);
    };
  }, []);

 

그리고 소켓 서버에 현재 수정 중인 table이 있다면, 클라이언트는 해당 data로 갱신하는 초기화 코드도 추가한다.

  useEffect(() => {
    socketIO.once("initData", (data) => {
      if(data.currentData) setTableData(data.currentData);
    });
  }, []);

 

데이터 변경 + 행열 추가 / 삭제 hook에서 변경된 datathis.getData()로 얻을 수 있다.

이 데이터를 sendData서버에 전송하면 스프레드시트를 동시에 작업할 수 있다.

const customOptions = {
  afterChange: function (changes, source) {
    // changes : 변경된 데이터 정보, source : 변경을 발생시킨 원인
    if (source === "loadData") return;

    console.log("Changed Data :", source, changes);
    socketIO.emit("sendData", this.getData());
  },
  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
};

전체 코드는 다음과 같다.

 

socketIOServer.js

const { Server } = require("socket.io");

let currentData = undefined;

const io = new Server("3333", {
  cors: {
    origin: "http://localhost:3000",
  },
});

io.sockets.on("connection", (socket) => {
  socket.on("sendData", (data) => {    
    console.log(data);
    currentData = data;
    socket.broadcast.emit("respondData", currentData);
  });

  socket.on("disconnect", () => {});
});

 

SocketTable.js (Client)

import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";

import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";

import { io } from "socket.io-client";

import {
  Avatar,
  Conversation,
  MessageSeparator,
} from "@chatscope/chat-ui-kit-react";

import SimpleHandsOnTable from "./SimpleHandsOnTable";

const AVATAR_MAP = {
  Lilly: "https://chatscope.io/storybook/react/assets/lilly-aj6lnGPk.svg",
  Joe: "https://chatscope.io/storybook/react/assets/joe-v8Vy3KOS.svg",
  Emily: "https://chatscope.io/storybook/react/assets/emily-xzL8sDL2.svg",
  Akane: "https://chatscope.io/storybook/react/assets/akane-MXhWvx63.svg",
  Eliot: "https://chatscope.io/storybook/react/assets/eliot-JNkqSAth.svg",
  Zoe: "https://chatscope.io/storybook/react/assets/zoe-E7ZdmXF0.svg",
};

const defaultData = [
  ["", "Tesla", "Nissan", "Toyota", "Honda", "Mazda", "Ford"],
  ["2017", 10, 11, 12, 13, 15, 16],
  ["2018", 10, 11, 12, 13, 15, 16],
  ["2019", 10, 11, 12, 13, 15, 16],
  ["2020", 10, 11, 12, 13, 15, 16],
  ["2021", 10, 11, 12, 13, 15, 16],
];

let socketIO = io("http://localhost:3333", { autoConnect: false });

const customOptions = {
  afterChange: function (changes, source) {
    // changes : 변경된 데이터 정보, source : 변경을 발생시킨 원인
    if (source === "loadData") return;

    console.log("Changed Data :", source, changes);
    socketIO.emit("sendData", this.getData());
  },
  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
    socketIO.emit("sendData", this.getData());
  },
};

const SocketTable = () => {
  const location = useLocation();
  const [loginID, setLoginID] = useState("");

  const [tableData, setTableData] = useState(defaultData);

  const init = () => {
    setLoginID(location.state.loginID);
  };

  useEffect(init, []);

  const respondDataCallback = (newData) => {
    setTableData(newData);
  };

  useEffect(() => {
    socketIO.connect();
    if (!socketIO) return;

    socketIO.on("respondData", respondDataCallback);

    return () => {
      socketIO.off("respondData", respondDataCallback);
    };
  }, []);

  useEffect(() => {
    socketIO.once("initData", (data) => {
      if(data.currentData) setTableData(data.currentData);
    });
  }, []);

  return (
    <div>
      <Conversation
        info="I'm fine, thank you, and you?"
        lastSenderName={loginID}
        name={loginID}
      >
        <Avatar name={loginID} src={AVATAR_MAP[loginID]} status="available" />
      </Conversation>
      <MessageSeparator style={{ marginTop: 5, marginBottom: 5 }} />
      <SimpleHandsOnTable
        data={tableData}
        customOptions={customOptions}
      />
    </div>
  );
};

export default SocketTable;

 

SimpleHandsOnTable.js는 수정 없음

반응형