[React] 커스텀 훅 성능 개선

김태민·2025년 12월 16일

문제 상황

사이드바의 너비를 드래그로 조정하는 기능을 구현 중이였습니다. 재사용을 위해 Hook으로 분리했고, 페이지 이동에도 너비가 변하지 않게 로컬스토리지에 너비를 저장해놓고 불러와서 사용하는 방식으로 구현했습니다.

초기 코드

export const useResizableSidebar = ({
  defaultWidth = 412,
  minWidth = 300,
  maxWidth = 800,
  storageKey = "sidebar-width",
}: UseResizableSidebarOptions = {}) => {
  const [width, setWidth] = useState(getInitialWidth);
  const [isResizing, setIsResizing] = useState(false);

  //  문제: width가 변경될 때마다 localStorage에 쓰기
  useEffect(() => {
    try {
      localStorage.setItem(storageKey, width.toString());
    } catch (error) {
      console.error("Failed to save sidebar width to localStorage:", error);
    }
  }, [width, storageKey]);

  // ... 나머지 코드
};

문제점 분석

사이드 바를 드래그하면서 로컬스토리지를 확인해보니, 값이 아주 빠르게 변하는 것을 확인했습니다. useEffect로 너비가 변할때마다 localStorage를 업데이트 하다 보니 발생한 일이였고, 너무 과도한 호출이라는 생각이 들었습니다.

  • 마우스 드래그 중 mousemove 이벤트가 초당 수십~수백 번 발생
  • 매번 setWidth가 호출되고, 이에 따라 localStorage 쓰기가 발생
마우스 이동 → setWidth 호출 → 리렌더링 → useEffect 실행 → localStorage 쓰기
(이 과정이 초당 수십 번 반복!)

해결 방법

드래그하는 동안에는 localStorage에 저장하지 않고, 드래그가 끝났을 때만 저장합니다.

개선된 코드

export const useResizableSidebar = ({
  defaultWidth = 412,
  minWidth = 300,
  maxWidth = 800,
  storageKey = "sidebar-width",
}: UseResizableSidebarOptions = {}) => {
  const [width, setWidth] = useState(defaultWidth);
  const [isResizing, setIsResizing] = useState(false);
  const startXRef = useRef(0);
  const startWidthRef = useRef(0);

  useEffect(() => {
    try {
      const saved = localStorage.getItem(storageKey);
      if (saved) {
        const parsedWidth = parseInt(saved, 10);
        if (parsedWidth >= minWidth && parsedWidth <= maxWidth) {
          setWidth(parsedWidth);
        }
      }
    } catch (error) {
      console.error("Failed to load sidebar width from localStorage:", error);
    }
  }, [storageKey, minWidth, maxWidth]);

  const handleMouseDown = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      setIsResizing(true);
      startXRef.current = e.clientX;
      startWidthRef.current = width;
    },
    [width]
  );

  useEffect(() => {
    if (!isResizing) return;

    const handleMouseMove = (e: MouseEvent) => {
      const diff = startXRef.current - e.clientX;
      const newWidth = startWidthRef.current + diff;

      if (newWidth >= minWidth && newWidth <= maxWidth) {
        setWidth(newWidth);
      }
    };

    const handleMouseUp = () => {
      setIsResizing(false);
      // 드래그 종료 시에만 localStorage에 저장
      try {
        localStorage.setItem(storageKey, width.toString());
      } catch (error) {
        console.error("Failed to save sidebar width to localStorage:", error);
      }
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);

    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [isResizing, minWidth, maxWidth, width, storageKey]);

  return {
    width,
    isResizing,
    handleMouseDown,
  };
};

개선 사항

localStorage 저장 시점 변경

Before

useEffect(() => {
  localStorage.setItem(storageKey, width.toString());
}, [width]); // width 변경마다 실행

After

const handleMouseUp = () => {
  setIsResizing(false);
  localStorage.setItem(storageKey, width.toString()); // mouseup 시 한 번만 실행
};

결론

  • localStorage의 과도한 호출은 성능에 악영향을 줍니다
  • 사용자 인터랙션 중에는 상태만 업데이트하고, 완료 시점에 저장하는 것이 효율적입니다
profile
빠르게 성장하는 개발자

0개의 댓글