[React] 마인드맵 구현하기

Lee JuHyeon·2022년 4월 10일
1

React

목록 보기
3/6
post-thumbnail
post-custom-banner

리액트로 마인드맵을 구현해보았다.
노드 추가 버튼을 누르면 해당 노드와 연관된 노드가 생성되고 둘 사이에 라인이 그려진다.
그리고 노드를 자유롭게 이동시킬 수 있고 이동시킨 노드에 선이 따라오게 구현했다.

🖥 1. 노드 이동과 관련된 이벤트

🖱 onMouseDown, onMouseUp

해당 노드에 마우스를 누르고 있는 상태를 onPress에 저장하여 마우스로 누르고 있는 상태에만 작동하도록 만들어준다.

1 const [onPress, setOnPress] = useState(false);
2
3 const onMouseDown = () => {
4     setOnPress(true);
5   };
6 
7 const onMouseUp = () => {
8     setOnPress(false);
9   };

🖱 onMouseMove

onPress가 true일 경우에 해당 노드의 x,y값을 현재 마우스의 좌표와 일치하도록 하는 코드이다.

1  const [transX, setTransX] = useState(props.xval);
2  const [transY, setTransY] = useState(props.yval); 
3 
4  const onMouseMove = e => {
5      e.preventDefault();
6      if (onPress === true) {
7        const pos = e.target.getBoundingClientRect();
8        let mouseX = e.clientX;
9        let mouseY = e.clientY;
10       const currentX = mouseX - pos.width / 2 + 60;
11       const currentY = mouseY - pos.height / 2 + 30;
12       setTransX(currentX);
13       setTransY(currentY);
14     }
15   };

1,2번에서 state의 초기값으로 db에 저장되어있는 좌표를 넣어준다.
6번을 통해 onPress가 true일 경우에만 좌표값을 조절하는 함수를 작동시켜준다.
7번 e.target.getBoundingClientRect()로 타겟의 정보를 받아오고
8,9번으로 마우스의 x,y 좌표를 가져온다.
10, 11번에서 마우스의 좌표에 타겟의 width,height를 반으로 나눈 값을 빼주어 마우스의 중심으로 타겟이 이동하게 만들었다.
그리고 currentX,currentY를 상태값에 넣어 노드의 translateX,Y에 적용하였다.

10, 11번에서 60과 30을 추가한 이유는 좌표값의 정중앙에 노드가 위치하도록 하기 위함이다.
만약 추가하지 않으면 path를 그려넣었을때 노드의 xy좌표값을 기준으로 그리는데 path의 기준점을 노드의 왼쪽위로 잡고 그리기 때문에 노드를 좌표의 정확히 중앙값으로 맞춰주었다.

🖥 2. 노드 정보를 db에 업데이트하기

🖱 onFocus, onBlur

노드의 이동값을 어떻게 db에 업데이트할까 고민을 하다가 노드의 focus가 벗어나는 순간 db에 update되도록 설계하면 되지 않을까 생각하게 되었다.

onFocus={() => {
  setIsFocus(true);
}}
onBlur={() => {
  setIsFocus(false);
  updateNode();
}}

프로젝트에 데이터 폴링을 적용하기 전에 구성한 방법인데 이렇게 만들면 치명적인 단점이 존재한다.
onBlur 함수는 해당 엘리먼트의 포커스가 '사라졌을때'만 작동한다는것.
만약 노드를 옮기고 아무런 행동도 취하지 않으면 onFocus 함수가 작동하는 중이고 onBlur 함수는 작동하지 않은 상태가 된다. 즉, 정보를 업데이트해주지 못한다는것.

이를 해결하기 위해서는,
onMouseMove() 함수에 debounce 함수를 연결하여 입력 지연이 감지되는 순간 updateNode()함수를 실행하거나
throttle 함수를 연결하여 updateNode()함수를 일정 주기로 실행하도록 하면 해결될 것이다.

마인드맵의 특성상 이동이 잦고 실시간 연동에서 더 좋은 사용성을 위해서는 throttle을 적용하는 쪽이 더 괜찮지 않을까 생각한다. debounce와 throttle에 관한 실험은 다음 글에서 포스팅하겠다!

🖥 3. 노드에 path 연결하기

🖱 설계

node에 path를 연결하기 위해서는 parantNode의 id와 childNode의 id를 알아야한다.

이미 만들어진 두 노드의 정보를 긁어와서 연과성을 지어주는 것은 당장 구현하기 어려울 것 같아서 node에 +버튼을 따로 만들어 +를 누르면 해당 node와 연관된 childNode를 만들도록 구현했다.
백엔드에게는 Node에 추가버튼을 누르면 해당 node의 id는 parentNode가 되고 생성된 node는 childNode가 되도록 설계를 부탁했다.

🖱 좌표 array 만들기

node의 좌표정보가 담긴 array nodeList와 parentNode의 id, childNode의 id가 담긴 array pathList를 이용해 각 id값에 맞는 좌표를 담은 새로운 array를 만들어주었다.

1   const { data: nodeList } = useNode(nodeTableId);
2   const { data: pathList } = usePath(nodeTableId);
3 
4   let mergedArray = [];
5   if (pathList) {
6     for (let i = 0; i < pathList.length; i++) {
7       let targetParentNode = pathList[i].parentNode;
8       let targetChildNode = pathList[i].childNode;
9 
10      let parentNode = nodeList.find(e => e.nodeId === targetParentNode);
11      let childNode = nodeList.find(e => e.nodeId === targetChildNode);
12
13      let resultObject = {
14        parent: parentNode === undefined ? 0 : parentNode,
15        child: childNode === undefined ? 0 : childNode,
16      };
17
18      mergedArray.push(resultObject);
19    }
20  }

react-query로 리스트의 data를 받아왔다.
새로운 array를 만들고 i번째 배열에 있는 parentNode와 childNode의 id를 nodeList에서 find하여 nodeList에 있는 좌표값을 부모와 자식 node에 배치해주었다.

node를 삭제할 경우 좌표값에 undefined가 들어와 프론트가 터져버리길래 undefined가 들어오면 0으로 값을 바꾸어주었다.

🖱 path 그리기

path를 그리기 위해 svg에 대해 공부해보았다. svg 태그 안에 line, path, circle 등등 여러가지가 있던데 내가 사용한 것은 path 태그이다.

path태그에서 d 속성을 통해 path의 시작점과 끝점을 정할 수 있다.

 return (
    <StyledDiv>
      <svg width="100%" height="100%">
        {mergedArray.length > 0 &&
          mergedArray.map((data, index) => (
            <path
              key={index}
              d={`M${data.parent.xval} ${data.parent.yval} L ${data.child.xval} ${data.child.yval}`}
              fill="transparent"
              strokeWidth="4"
              stroke="#f3f3f3"
            />
          ))}
      </svg>
    </StyledDiv>
  );

svg태그를 마인드맵 전체화면 아래에 100% 크기로 배치하였다.
그리고 path 태그 하나하나를 node의 연결관계로 이용했다.
d="M100 100 L 200 200"이라고 할때 M100 100은 path의 시작점 L200 200은 path의 끝점이다.
그래서 'M'에 parentNode의 좌표값을 'L'에 childNode의 좌표값을 넣어주었다.

마무리

프로젝트를 진행하면서 글을 적을 시간이 없어서 이제야 정리중인데
정리할게 산더미라서 기억이 잘 안난다. ㅠㅠ
혹시라도 이 글을 볼 사람들한테 제대로 설명이 되었으면 좋겠당
궁금한 점은 댓글 남겨주세용

profile
인터랙티브 웹에 관심이 많습니다
post-custom-banner

0개의 댓글