본문 바로가기
개발/React

리액트, Node JS - 변경된 셀만 데이터 전송해서 최적화하기 (Data Optimization with Socket.IO)

by 피로물든딸기 2024. 4. 13.
반응형

리액트 전체 링크

Node JS 전체 링크

 

참고

- 2차원 배열 빈 행 / 열 추가, 삭제하기

 

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

 

이전 글에서 전체 데이터한 번에 서버로 전송하였다.

  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());
  },
  ...

 

그리고 서버다른 클라이언트에게 broadcast전체 데이터를 전송하였다.

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

 

데이터가 적으면 상관없지만, 클라이언트가 많을수록, 데이터가 크면 클수록 서버에 부담이 되게 된다.

 

필요한 데이터만 전송하여 handsontable공동 편집해 보자.


최신 정보 전달

 

먼저 나중에 들어온 클라이언트도 최신 정보를 알 수 있도록 코드를 보완하자.

 

서버에 currentDataundefined인 경우라면, 최초로 서버에 연결된 클라이언트가 된다.

이 클라이언트가 현재 tableData를 통째로 보내도록 하자.

undefined가 아닌 경우는 나중에 들어온 클라이언트이므로, setTableData로 데이터를 갱신한다.

  useEffect(() => {
    socketIO.once("initData", (data) => {
      console.log(data);
      if(data.currentData) setTableData(data.currentData);
      else {
        socketIO.emit("setCurrentData", tableData);
      }
    });
  }, []);

 

서버에서는 현재 data를 설정하기만 하면 된다.

  socket.on("setCurrentData", (data) => {   
    console.log("setCurrentData");
    currentData = data; 
  });

handsontable의 edit hook

 

현재 사용하는 hook은 총 5가지다. (데이터 변경 + 행열 추가 / 삭제)

각각 로그를 추가해서 hook이 어떻게 동작하는지 알아보자.

  afterChange: function (changes, source) {
    socketIO.emit("changeData", changes);
  },
  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
  },

 

데이터를 변경할 경우, 변경된 데이터는 [row, col, before, after]에 대한 정보로 알 수 있고, 배열로 제공된다.

행/열 추가/삭제의 경우 index, amount로 어떤 index 얼만큼 수정이 되었는지 알 수 있다.

그리고 data 붙여넣기로 테이블이 변경되는 경우, 행/열이 추가된 후에 데이터 편집이 이루어진다.

 

즉, 해당되는 정보를 그대로 socket 서버에 보내면 된다.

다른 클라이언트에게는 changesindex, amount만 정보를 보내면 된다.

그리고 이 정보를 이용해서 서버와 클라이언트 모두 현재 data를 갱신하면, data를 통째로 보낼 필요가 없다.

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

    console.log("Changed Data :", source, changes);
    socketIO.emit("changeData", changes);
  },
  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
    socketIO.emit("createRow", index, amount);
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
    socketIO.emit("removeRow", index, amount);
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
    socketIO.emit("createCol", index, amount);
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
    socketIO.emit("removeCol", index, amount);
  },

Socket Server

 

2차원 배열 빈 행/열 추가, 삭제하기를 참고하여 2차원 배열을 변경하는 메서드를 추가한다.

const insertRows = (table, rowIndex, amount) => { ... };
const deleteRows = (table, rowIndex, amount) => { ... };
const insertColumns = (table, colIndex, amount) => { ... };
const deleteColumns = (table, colIndex, amount) => { ... };

 

만약 행 추가 이벤트가 발생하면, 서버는 indexamountcurrentData에 행을 추가하면 된다.

그리고 indexamount만 클라이언트에게 보내면 된다.

  socket.on("createRow", (index, amount) => {    
    console.log("createRow", index, amount);
    currentData = insertRows(currentData, index, amount);
    socket.broadcast.emit("resCreateRow", index, amount);
  });

 

데이터 변경은 changes for문으로 순회하면서 data를 직접 수정하면 된다.

그리고 changes만 클라이언트에게 보내면 된다.

  socket.on("changeData", (changes) => {    
    console.log("changeData", changes);
    for(let change of changes) {
      let [row, col, before, after] = change;
      currentData[row][col] = after;
    }
    socket.broadcast.emit("resChangeData", changes);
  });

 

그 외 코드는 전체 코드를 참고하자.


Socket Client

 

클라이언트(리액트)도 2차원 배열 빈 행/열 추가, 삭제하기 메서드를 추가한다.

const insertRows = (table, rowIndex, amount) => { ... };
const deleteRows = (table, rowIndex, amount) => { ... };
const insertColumns = (table, colIndex, amount) => { ... };
const deleteColumns = (table, colIndex, amount) => { ... };

 

hook에서 서버에 해당되는 메시지를 전송한다.

  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
    socketIO.emit("createRow", index, amount);
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
    socketIO.emit("removeRow", index, amount);
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
    socketIO.emit("createCol", index, amount);
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
    socketIO.emit("removeCol", index, amount);
  },

 

서버 코드와 마찬가지로 callback 함수에서 data를 갱신하자.

  const resCreateRow = (index, amount) => {
    console.log("createRow", index, amount);
    setTableData((prev) => {
      let newTable = insertRows(prev, index, amount);
      return newTable;
    });
  };
  
  ...
  
  const resChangeData = (changes) => {
    console.log("changeData",changes);
    setTableData((prev) => {
      let newTable = [...prev];
      for(let change of changes) {
        let [row, col, before, after] = change;
        newTable[row][col] = after;
      }
      return newTable;
    });
  };

 

그리고 이벤트를 등록하면 된다.

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

    // socketIO.on("respondData", respondDataCallback);
    socketIO.on("resCreateRow", resCreateRow);
    socketIO.on("resRemoveRow", resRemoveRow);
    socketIO.on("resCreateCol", resCreateCol);
    socketIO.on("resRemoveCol", resRemoveCol);
    socketIO.on("resChangeData", resChangeData);
        
    return () => {
      // socketIO.off("respondData", respondDataCallback);
      socketIO.off("resCreateRow", resCreateRow);
      socketIO.off("resRemoveRow", resRemoveRow);
      socketIO.off("resCreateCol", resCreateCol);
      socketIO.off("resRemoveCol", resRemoveCol);  
      socketIO.off("resChangeData", resChangeData);            
    };
  }, []);

전체 코드는 다음과 같다.

 

socketIOServer.js

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

let currentData = undefined;

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

const insertRows = (table, rowIndex, amount) => {
  const afterIndex = table.slice(rowIndex);
  const emptyArray = Array.from({ length: table[0].length }, () => "");
  const newRows = Array.from({ length: amount }, () => emptyArray);  
  const newArray = table.slice(0, rowIndex).concat(newRows, afterIndex);

  return newArray;
};

const deleteRows = (table, rowIndex, amount) => {
  const newTable = [...table]; 

  newTable.splice(rowIndex, amount);

  return newTable;
};

const insertColumns = (table, colIndex, amount) => {
  const newMatrix = [];

  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 

    for (let k = 0; k < amount; k++) {
      newRow.splice(colIndex + k, 0, "");
    }

    newMatrix.push(newRow); 
  }

  return newMatrix;
};

const deleteColumns = (table, colIndex, amount) => {
  const newTable = [];
  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 
    newRow.splice(colIndex, amount); 
    newTable.push(newRow); 
  }

  return newTable;
};

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

  socket.on("setCurrentData", (data) => {   
    console.log("setCurrentData");
    currentData = data; 
  });

  socket.on("createRow", (index, amount) => {    
    console.log("createRow", index, amount);
    currentData = insertRows(currentData, index, amount);
    socket.broadcast.emit("resCreateRow", index, amount);
  });
  
  socket.on("removeRow", (index, amount) => {    
    console.log("removeRow", index, amount);
    currentData = deleteRows(currentData, index, amount);
    socket.broadcast.emit("resRemoveRow", index, amount);
  });
  
  socket.on("createCol", (index, amount) => {   
    console.log("createCol", index, amount); 
    currentData = insertColumns(currentData, index, amount);
    socket.broadcast.emit("resCreateCol", index, amount);
  });
  
  socket.on("removeCol", (index, amount) => {    
    console.log("removeCol", index, amount);
    currentData = deleteColumns(currentData, index, amount);
    socket.broadcast.emit("resRemoveCol", index, amount);
  });
  
  socket.on("changeData", (changes) => {    
    console.log("changeData", changes);
    for(let change of changes) {
      let [row, col, before, after] = change;
      currentData[row][col] = after;
    }
    socket.broadcast.emit("resChangeData", changes);
  });
  
  // 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("changeData", changes);
  },
  afterCreateRow: function (index, amount) {
    console.log("create row :", index, amount);
    socketIO.emit("createRow", index, amount);
  },
  afterRemoveRow: function (index, amount) {
    console.log("remove row :", index, amount);
    socketIO.emit("removeRow", index, amount);
  },
  afterCreateCol: function (index, amount) {
    console.log("create col :", index, amount);
    socketIO.emit("createCol", index, amount);
  },
  afterRemoveCol: function (index, amount) {
    console.log("remove col :", index, amount);
    socketIO.emit("removeCol", index, amount);
  },
};

const insertRows = (table, rowIndex, amount) => {
  const afterIndex = table.slice(rowIndex);
  const emptyArray = Array.from({ length: table[0].length }, () => "");
  const newRows = Array.from({ length: amount }, () => emptyArray);  
  const newArray = table.slice(0, rowIndex).concat(newRows, afterIndex);

  return newArray;
};

const deleteRows = (table, rowIndex, amount) => {
  const newTable = [...table]; 

  newTable.splice(rowIndex, amount);

  return newTable;
};

const insertColumns = (table, colIndex, amount) => {
  const newMatrix = [];

  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 

    for (let k = 0; k < amount; k++) {
      newRow.splice(colIndex + k, 0, "");
    }

    newMatrix.push(newRow); 
  }

  return newMatrix;
};

const deleteColumns = (table, colIndex, amount) => {
  const newTable = [];
  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 
    newRow.splice(colIndex, amount); 
    newTable.push(newRow); 
  }

  return newTable;
};

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

  const resCreateRow = (index, amount) => {
    console.log("createRow", index, amount);
    setTableData((prev) => {
      let newTable = insertRows(prev, index, amount);
      return newTable;
    });
  };
  
  const resRemoveRow = (index, amount) => {
    console.log("removeRow", index, amount);
    setTableData((prev) => {
      let newTable = deleteRows(prev, index, amount);
      return newTable;
    });
  };

  const resCreateCol = (index, amount) => {
    console.log("createCol", index, amount); 
    setTableData((prev) => {
      let newTable = insertColumns(prev, index, amount);
      return newTable;
    });
  };

  const resRemoveCol = (index, amount) => {
    console.log("removeCol", index, amount);
    setTableData((prev) => {
      let newTable = deleteColumns(prev, index, amount);      
      return newTable;
    });
  };

  const resChangeData = (changes) => {
    console.log("changeData",changes);
    setTableData((prev) => {
      let newTable = [...prev];
      for(let change of changes) {
        let [row, col, before, after] = change;
        newTable[row][col] = after;
      }
      return newTable;
    });
  };

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

    // socketIO.on("respondData", respondDataCallback);
    socketIO.on("resCreateRow", resCreateRow);
    socketIO.on("resRemoveRow", resRemoveRow);
    socketIO.on("resCreateCol", resCreateCol);
    socketIO.on("resRemoveCol", resRemoveCol);
    socketIO.on("resChangeData", resChangeData);
        
    return () => {
      // socketIO.off("respondData", respondDataCallback);
      socketIO.off("resCreateRow", resCreateRow);
      socketIO.off("resRemoveRow", resRemoveRow);
      socketIO.off("resCreateCol", resCreateCol);
      socketIO.off("resRemoveCol", resRemoveCol);  
      socketIO.off("resChangeData", resChangeData);            
    };
  }, []);

  useEffect(() => {
    socketIO.once("initData", (data) => {
      console.log(data);
      if(data.currentData) setTableData(data.currentData);
      else {
        socketIO.emit("setCurrentData", tableData);
      }
    });
  }, []);

  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는 수정 없음

반응형

댓글