리액트, Node JS - Handsontable에 Socket.IO 적용하기 (Handsontable with Socket.IO)
참고
- Socket.IO로 Toast UI Editor 동시 편집하기
- 스프레드시트 공동 작업하기 Project Settings
- Handsontable에 Socket.IO 적용하기
- 변경된 셀만 데이터 전송해서 최적화하기
- 행 / 열 이동 및 크기 변경 연동하기
- 다른 클라이언트가 선택한 셀 표시하기
Handsontable에 Socket.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에 들어갈 data를 useState로 추가한다.
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에서 변경된 data를 this.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는 수정 없음