React Resize 최적화하기

JONGEE KIM·2024년 8월 3일

최근 윈도우 내 위치 조정이 가능한 Draggable한 모달 컴포넌트를
피그마나 슬랙 같이 크기 조절이 가능한 파셀 모달로 업데이트 하는
기능 고도화 업무가 있었습니다.

기존 모달 형태

바뀐 파셀형 모달 형태

파셀 조절 버튼을 클릭해서 드래그 & 드롭하는 위치에 따라
모달의 width가 변경되어야 했습니다.

해당 요구조건을 충족하기 위해서 처음에는 state에 변경되는 width 값을 저장한 모달을 구현하였습니다.
하지만 state에 값을 저장하니 모달이 resize 되는 속도가 너무 느렸습니다. 또한 resize 동작이 일어날 때 동작이 부드럽지 않고 뚝뚝 끊겨보이는 이슈가 있었습니다.

리사이즈 기능의 반응 속도가 느렸던 주된 이유는 handleMouseMove 함수에서 setWidth를 호출할 때마다 컴포넌트가 리렌더링되기 때문이었습니니다.
이 과정에서 매번 상태 업데이트가 발생하고, 따라서 리렌더링 비용이 크게 증가하는 로직이었습니다.

const DraggableList: FC = () => {

  /** resize 관련 함수 */
  const [isResizing, setIsResizing] = useState(false);
  const [initialX, setInitialX] = useState(0);
  const [width, setWidth] = useState(835);

  const handleMouseDown = (e: React.MouseEvent) => {
    e.preventDefault();
    setIsResizing(true);
    setInitialX(e.clientX);
  };

  const handleMouseUp = () => {
    setIsResizing(false);
  };

  const handleMouseMove = useCallback(
    (e: MouseEvent) => {
      if (isResizing) {
        const newWidth = width + e.clientX - initialX;
        setInitialX(e.clientX);
        if (newWidth >= 300 && newWidth <= 835) {
          setWidth(newWidth);
        }
      }
    },
    [initialX, isResizing, width]
  );

  useEffect(() => {
    if (isResizing) {
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    } else {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    }

    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isResizing, handleMouseMove]);

  if (!visible) {
    return null;
  }

  return <ResizeComponent />;
};

렌더링을 최적화 하기 위해서 값이 변경되더라도 리렌더링을 방지하는 ref에 width 값을 저장하는 방식으로 변경하여 handleMouseMove가 width가 변경 될 때마다 재생성 되는 것을 방지하도록 로직을 변경했습니다.
state에 저장했던 값을 ref에 저장하도록 변경하는 것만으로 개선된 resize 동작을 확인할 수 있었습니다.

  const [isResizing, setIsResizing] = useState(false);
  const initialXRef = useRef(0);
  const widthRef = useRef(835);
  const [width, setWidth] = useState(835);

이에 더해 requestAnimationFrame을 사용하여 handleMouseMove 함수를 개선하였습니다. requestAnimationFrame을 사용하면 브라우저가 다음 리페인트를 수행하기 전에 지정된 함수를 호출하도록 예약할 수 있습니다. (브라우저의 리페인트 주기에 맞추어 상태를 업데이트)

보통 모니터의 주사율은 60hz인데, 이 말인 즉슨 자바스크립트로 사용자에게 부드러운 애니메이션을 구현하려면 16.6ms밀리초(1000ms / 60fps) 마다 코드를 호출하는 식으로 구현해야 한다는 뜻입니다. 브라우저는 애니메이션 프레임을 출력할 때마다 requestAnimationFrame 에 등록된 콜백 함수들을 비동기로 호출하기 때문에, 애니메이션을 부드럽게 출력합니다.

handleMouseMove 내에서 상태 업데이트가 필요한 로직에 requestAnimationFrame을 사용하여 width 리사이징 동작이 일어날 때 부드러운 애니메이션 업데이트가 수행되도록 최적화할 수 있었습니다.

const handleMouseMove = useCallback(
    (e: MouseEvent) => {
      if (isResizing) {
        requestAnimationFrame(() => {
          const deltaX = e.clientX - initialXRef.current;
          const newWidth = widthRef.current + deltaX;

          if (newWidth >= 300 && newWidth <= 835) {
            widthRef.current = newWidth;
            initialXRef.current = e.clientX;
            setWidth(newWidth);
          }
        });
      }
    },
    [isResizing]
  );
profile
Code On Paper 📝

0개의 댓글