개요

막상 사용성과 재미를 위해 캔버스 요소들에 대해서 충돌 방지를 달아놨는데, 문제는 dragmove이벤트는 매 프레임마다 실행하기 때문에 1초에 수십, 수백번이 이런남에도 불구하고 이 모든 프레임마다 충돌 방지 스크립트를 실행하기 때문에 노드가 많아질 수록 점점 프레임 드랍이 일어나는 듯한 느낌이 들었다.

따라서 이를 개선하기 위해 여러 가지 방법을 시도해보았다.

zustand를 활용한 리렌더링

Zustand는 최근에 인기가 많아지고 있는 전역 상태 라이브러리로, selector 패턴을 사용하면 데이터가 바뀌는 부분만 렌더링이 가능하다는 장점을 가지고 있다.

스크린샷 2024-11-14 오후 11.27.39.png

화면 기록 2024-11-14 오후 11.27.57.mov

왜 문제일까?

나는 useEffect에 data가 바뀔 때마다 checkCollision을 실행할 수 있도록 했다.

Zustand는 상태 관리를 간결하게 하고 전역 상태를 쉽게 관리할 수 있지만, 특정한 성능 최적화가 이루어지지 않은 상태에서 빈번하게 상태를 업데이트할 경우 성능 문제가 발생할 수 있다고 한다

특히 useEffect가 의존성 배열을 통해 data 변경을 감지하여 checkCollision을 호출할 때, Zustandstore에서 데이터를 가져와 업데이트하는 과정에서 추가적인 메모리와 연산이 요구될 수 있기 때문에 이러한 과정 자체가 오히려 빠르고 반복적으로 호출되는 상황에서는 오버헤드로 작용할 수 있다.

반면 Context API는 기본적으로 Reactmemoization 최적화를 통해 상태 변경을 효율적으로 관리하며, 컴포넌트 트리에서 관련된 부분에 대해서만 변경을 감지하고 리렌더링을 수행하기 때문에 더 빠르게 반응할 수 있다고 생각하여 다시 Conetext API를 이용한 방식으로 롤백하기로 했다.

requestAnimationFrame의 사용

Context API를 활용한 방식으로 회귀한 뒤, 어떻게 다른 방식으로 조금 더 렌더링을 최적화 할 수 있는 방식이 있을까 생각해보았다.

현재 우리는 각각의 노드에 대해 움직이는 모션과 충돌하는 로직을 넣기 위해 지금까지 이렇게 고생을 해왔다. 이 모든 것은 충돌하는, 움직이는 듯한 애니메이션을 위해서였다. 따라서 requestAnimationFrame을 활용하면 조금이라도 더 자연스러워지지 않을까 생각했다.

export function useCollisionDetection(nodeData: NodeData, updateNode: (id: number, updates: Partial<Node>) => void) {
  const layer = useRef<Konva.Layer>(null);

  const handleCollision = useCallback(
    (base, target) => {
      const newTargetPosition = moveOnCollision(target, base);

      updateNode(target.attrs.id, { location: newTargetPosition });
    },
    [nodeData, updateNode],
  );

  const detectCollisions = useCallback(
    (layer: Konva.Layer) => {
      const nodes = layer.children.filter((child) => child.attrs.name === "node");
      nodes.forEach((base) => {
        nodes.forEach((target) => {
          if (base !== target) {
            if (isCollided(base.getClientRect(), target.getClientRect())) {
              handleCollision(base, target);
            }
          }
        });
      });
    },
    [handleCollision],
  );

  useEffect(() => {
    if (layer.current) {
      requestAnimationFrame(() => {
        detectCollisions(layer.current);
      });
    }
  }, [layer, detectCollisions]);

  return layer;
}

충돌을 감지하는 로직을 따로 분리해도 좋을 것 같아서 커스텀 훅으로 분리해보았다. 기본 로직은 똑같으며, layer라는 ref를 넘겨주어 캔버스의 layer에 ref로 연결하는 방식이다.

여기서 달라진 점이라고는 RequestAnimationFrame을 적용한 점이다.