React의 무한 루프 문제 해결: 조건부 상태 업데이트로 성능 최적화하기 (문제 해결)

Devinix·2023년 12월 13일
0

[문제 해결]

목록 보기
12/29
post-thumbnail

개요

https://velog.io/@dpldpl/%EB%B6%80%EB%AA%A8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-width-%EA%B0%92%EC%97%90-%EB%A7%9E%EA%B2%8C-%EC%9E%90%EC%8B%9D-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%8F%99%EC%A0%81-%ED%81%AC%EA%B8%B0-%EC%A1%B0%EC%A0%88-React-Typescript
에서 다룬 내용이 무한루프의 문제가 발생하여 문제를 해결하는 과정을 다룰 것이다.

아래의 코드는 위 블로그의 내용에 해당하는 로직을 커스텀 훅으로 뺀 것이다.

interface IProps {
  parentRef: RefObject<HTMLDivElement>;
  childSize: React.CSSProperties;
  setChildSize: Dispatch<SetStateAction<{ width: number; height: number }>>;
  setIsResized: Dispatch<SetStateAction<boolean>>;
}

function useHandleResizeProject({
  parentRef,
  childSize,
  setChildSize,
  setIsResized,
}: IProps) {
  useEffect(() => {
    const updateSize = () => {
      if (window.innerWidth >= 992) {
        setIsResized(() => true);
      } else {
        setIsResized(() => false);
      }
      if (parentRef.current) {
        const parentWidth = parentRef.current.offsetWidth;
        setChildSize(() => ({
          width: parentWidth / 2,
          height: parentWidth / 2,
        }));
      }
    };

    window.addEventListener("resize", updateSize);
    updateSize();

    return () => window.removeEventListener("resize", updateSize);
  }, [childSize]);
}

export default useHandleResizeProject;

문제 상황

콘솔 창을 확인해보니, "Maximum update depth exceeded" 에러가 발생했다. 이 에러는 컴포넌트가 무한 루프에 빠졌을 때 나타나는 것으로, 특정 컴포넌트의 상태 업데이트 로직이 주요 원인으로 의심되었다.

에러문

useHandleResizeProject.ts:21 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
    at ProjcetContent (http://localhost:3000/static/js/bundle.js:3504:6)
    at div
    at O (http://localhost:3000/static/js/bundle.js:59793:6)
    at Project (http://localhost:3000/static/js/bundle.js:3732:6)
    at div
    at ProjectContainer
    at div
    at Projects
    at div
    at main
    at O (http://localhost:3000/static/js/bundle.js:59793:6)
    at HomeMain
    at div
    at O (http://localhost:3000/static/js/bundle.js:59793:6)
    at div
    at O (http://localhost:3000/static/js/bundle.js:59793:6)
    at HomeTemplate
    at Home
    at RenderedRoute (http://localhost:3000/static/js/bundle.js:52371:5)
    at RenderErrorBoundary (http://localhost:3000/static/js/bundle.js:52318:5)
    at DataRoutes (http://localhost:3000/static/js/bundle.js:50973:5)
    at Router (http://localhost:3000/static/js/bundle.js:52930:15)
    at RouterProvider (http://localhost:3000/static/js/bundle.js:50769:5)
    at Xe (http://localhost:3000/static/js/bundle.js:59711:56)

원인

문제의 근본적인 원인은 React의 useState와 useEffect 훅을 사용하는 방식에 있었다. useEffect 내에서 setChildSize를 호출하면서 childSize 상태를 업데이트했다. 상태 업데이트는 컴포넌트를 재렌더링하게 만들고, childSize가 의존성 배열에 포함되어 있었기 때문에, useEffect가 다시 실행되게 했다. 즉, childSize 상태의 변화가 다시 useEffect의 실행을 유발하는 무한 루프를 만들게 된 것이다.

해결 과정

의존성 배열 최적화
먼저, useEffect의 의존성 배열에서 childSize를 제거했다. 이 변경은 childSize 상태의 변경이 useEffect를 다시 실행하지 않도록 했다. 이로써 childSize의 변경이 더 이상 무한 루프를 유발하지 않게 되었다.

useEffect(() => {
  // ...
}, [parentRef, setIsResized]); // childSize 제거

조건부 상태 업데이트 구현
상태 업데이트를 조건부로 만들었다. 새로운 너비와 높이를 계산한 후, 이 값들이 현재 childSize 상태와 다를 경우에만 setChildSize를 호출한다. 이 방법은 상태가 실제로 변경될 때만 상태 업데이트를 수행하게 하여, 불필요한 재렌더링을 방지한다.

useEffect(() => {
  // ...
  if (childSize.width !== newWidth || childSize.height !== newHeight) {
    setChildSize({ width: newWidth, height: newHeight });
  }
  // ...
}, [parentRef, setIsResized]);

전체 코드

interface IProps {
  parentRef: RefObject<HTMLDivElement>;
  childSize: React.CSSProperties;
  setChildSize: Dispatch<SetStateAction<{ width: number; height: number }>>;
  setIsResized: Dispatch<SetStateAction<boolean>>;
}

function useHandleResizeProject({
  parentRef,
  childSize,
  setChildSize,
  setIsResized,
}: IProps) {
  useEffect(() => {
    const updateSize = () => {
      if (window.innerWidth >= 992) {
        setIsResized(true);
      } else {
        setIsResized(false);
      }
      if (parentRef.current) {
        const parentWidth = parentRef.current.offsetWidth;
        const newWidth = parentWidth / 2;
        const newHeight = parentWidth / 2;
        
        // 조건부 상태 업데이트
        if (childSize.width !== newWidth || childSize.height !== newHeight) {
          setChildSize({
            width: newWidth,
            height: newHeight,
          });
        }
      }
    };

    window.addEventListener("resize", updateSize);
    updateSize();

    return () => window.removeEventListener("resize", updateSize);
  }, [parentRef, setIsResized]); // 의존성 배열에서 childSize 제거
}

export default useHandleResizeProject;

이 두 단계를 통해, 상태 업데이트 로직이 더욱 효율적이고 안정적으로 작동하게 되었다. 이는 React 컴포넌트의 성능을 개선하고 무한 루프로 인한 잠재적인 문제를 해결하는 데 중요한 역할을 했다.

결론

이번 경험을 통해, React 애플리케이션에서 무한 루프 문제를 해결하는 방법을 발견했다. 이 방법은 웹 애플리케이션의 성능을 크게 향상시키며, 무한 루프와 같은 잠재적인 문제를 예방하는 데 핵심적인 역할을 한다. 특히, 조건부 상태 업데이트의 중요성을 깊이 이해하게 되었고, 의존성 배열을 효과적으로 관리하는 방법에 대해서도 다시 한 번 생각해 볼 수 있는 좋은 기회였다. 앞으로 useEffect 훅을 사용할 때 무한루프의 위험에 대해서 한번 더 생각해 보게 될 것 같다.

수정!!

resize 함수로는 브라우저 상단 더블클릭 시 width크기가 동적으로 변하지 않기 때문에, ResizeObserver로 수정하였습니다.

interface IProps {
  parentRef: RefObject<HTMLDivElement>;
  childSize: React.CSSProperties;
  setChildSize: Dispatch<SetStateAction<{ width: number; height: number }>>;
  setIsResized: Dispatch<SetStateAction<boolean>>;
}

function useHandleResizeProject({
  parentRef,
  childSize,
  setChildSize,
  setIsResized,
}: IProps) {
  useEffect(() => {
    // ResizeObserver 인스턴스 생성: 부모 요소의 크기 변화를 감지함
    const observer = new ResizeObserver(entries => {
        // 브라우저 창의 너비에 따라 setIsResized 상태를 업데이트
        if (window.innerWidth >= 992) {
          setIsResized(true);
        } else {
          setIsResized(false);
        }     
      // 감지된 모든 요소에 대해 반복
      for (let entry of entries) {
        // 감지된 요소의 현재 너비를 가져옴
        const parentWidth = entry.contentRect.width;
        // 새로운 자식 요소의 크기 계산
        const newWidth = parentWidth / 2;
        const newHeight = parentWidth / 2;

        // 자식 요소의 현재 크기와 새 크기가 다른 경우, 크기를 업데이트
        if (childSize.width !== newWidth || childSize.height !== newHeight) {
          setChildSize({
            width: newWidth,
            height: newHeight,
          });
        }
      }
    });

    // 부모 요소에 대한 참조가 있을 경우, 감지 시작
    if (parentRef.current) {
      observer.observe(parentRef.current);
    }

    // 컴포넌트 언마운트 시, 감지를 중단
    return () => {
      if (parentRef.current) {
        observer.unobserve(parentRef.current);
      }
    };
  }, [parentRef, childSize, setChildSize, setIsResized]);
}

export default useHandleResizeProject;

profile
프론트엔드 개발

0개의 댓글