Back

react useEffect와 useSelector 무한루프

고민을 많이 하다보면 머릿속에서 혼자만의 생각의 도약이 큰데, 이런 정신없는 생각을 글로 정리하고 작성해본 경험이 적다. 이것을 다 작성하려면 시간이 많이 들어서다. 그런데 이것은 작성을 해놔야 할 것 같다.

글을 남기는 이유는 더 좋은 방법이 있는 것 같아서다. 개발당시 이런 문제가 있었고, 언젠가는 또 읽어보고 더 좋은 방법이 나올 것만 같다. 글이 없으면 뭔지조차 까먹을 것 같지만 이렇게라도 남겨놓으면 나중에 성장하였을 때 더 좋은 방법으로 개선이 가능할 것 같다.

그리고 생각도 정리가 된다.


상황, 배경

  • 상위 컴포넌트에서 react-redux useSelector훅을 통해 변경되는 store의 값을 사용하여 grid의 크기를 변경하려고 했다. 사용하는 액션의 데이터는 boolean값 하나만 관리되며, 이 값을 통해 grid의 크기를 두가지의 형태로 변경한다.

    const 상태값 = useSelector((state) => state.그리드상태.value);
    return (
    <>
      <Grid 스타일={상태값 ? A : B}>ㅎㅇ</Grid>
    </>
    );
  • 내부에서 사용하는 자식 컴포넌트는 마운트 시점에 이 값을 변경했다가 언마운트 시점에 다시 원래대로 돌려놓는다. (useEffect를 사용했다. 함수 두 번째 인자로 빈 배열을 넣었으며, return을 사용하여 componentDidMount와 ComponentWillUnmount와 동일한 시점이다.)

    useEffect(() => {
    스토어.dispatch(true로변경하는리듀서()); //action creator에서 초기값은 false였음
    return () => {
      store.dispatch(false로변경하는리듀서());
    };
    }, []);
  • 해당 자식 컴포넌트를 마운트하는 순간 무한으로 리렌더링된다.

처음에는 문제가 있는줄도 모르고 다른 것들을 개발하고 있었다. 사용하는데도 문제가 없었다. 그런데, 해당 화면을 켜놓고 작업을 하고있었는데, 맥북 팬 돌아가는 소리가 심하게 나고, 발열이 나며 뭔가 이상함을 느끼고 개발자도구를 열어보니 무한루프를 돌고있었다. 브라우저 화면상으로는 멀쩡했는데, 뒤에서는 계속 리렌더링을 하고있었다.

이렇게 개발을 다 해놓기도 했고, 앞으로 동일한 작업을 하는 컴포넌트가 앞으로 많이 생길 것 같아서 해본적도 없는 커스텀 훅을 만들어 따로 빼놓기도 했으며 무엇보다 내가 허접이었기에 어디에서부터 문제가 있는 것인지 파악하느라 고치는데 많은 시간이 필요했다.

원인

처음에 추측했던 원인은 다음과 같았다.

  1. 하위 컴포넌트가 마운트되는 시점에 스토어의 리듀서 함수를 실행시켜 grid를 줄이는 액션을 통해 새로운 상태를 만든다.
  2. 상위컴포넌트에서 useSelector를 통해 조건부 css를 사용하는 값이 변경이 되었기에 하위에 속한 자식컴포넌트가 무조건 리렌더링된다.
  3. 리렌더링 과정에서 하위 컴포넌트는 언마운트되며, useEffect에 의해 언마운트 시점에 1번에서 진행했던것이 다시 원래대로 돌아온다.
  4. 기존에 진행하던 것 즉, 새로그리기 위해 자식 컴포넌트를 언마운트 하고 이제 다시 렌더링하려는 과정과 새롭게 언마운트되어 상태가 변했기에 다시 자식컴포넌트를 언마운트하고 새롭게 그리려는 과정도 생겨 평행세계마냥 동시에 해야할 일이 하나 더 늘어났다.
  5. 내부적으로 어떤 처리가 이루어지는지는 모르겠고, 브라우저에 어떻게 그려주는지, 둘중에 하나를 포기하는지, 아니면 어딘가에 쌓아놓고있는지, 동시에 다 처리하는지 모르겠으나 아무튼 이런일이 생긴다.
  6. 최선의 상황이라고 생각하고 react dom이 하나를 포기한다고 하더라도 처음으로 돌아간 스토어의 상태값 때문에 작성한 1번 부터 다시 진행이 되야하기에 끝나지않는 무한루프를 만나는 것은 동일하다. 최악이라면 박테리아처럼 거듭제곱으로 해야할 일이 단계마다 늘어날 것 같다.

해결방안

당시 해결했던 방법은 다음과 같다. store 값 변경을 사용하는 컴포넌트들은 react router를 통해서만 사용했기 때문에 아래와 같은 방법으로 해결이 가능했다.

  1. 커스텀 훅에서 하나의 파라미터를 받는다.
  2. 커스텀 훅 내부에서 사용하는 useEffect가 return을 할 때 (언마운트) 파라미터에서 받은 문자열과 window.location.pathname를 비교한다.
  3. 비교한 값이 다를 경우에만 dispatch를 하지 않고, 마운트 시점에 변경된 상태를 유지시켜준다.

이렇게 하면, 이를 사용한 자식 컴포넌트는 처음에 렌더링 이후 store의 값을 변경하고, 부모가 이를 감지하고 상태를 변경한 뒤 자식을 다시 렌더링 해준다. 전과는 다르게 언마운트를 할 때 파라미터로 받은 pathname이랑 일치하는지를 테스트하여 마운트 했을 때 변경했던 store의 상태를 그대로 유지시켜줘서 렌더링을 다시 하더라도 다시하면서 생기는 컴포넌트 언마운트에서 store의 상태를 변경하지 않아 무한루프에 빠지지 않는다.

하지만, 단점이 있다.

  • react router를 통해 불러온 컴포넌트가 아니면, 동일한 pathname에서 두가지 상태를 사용 불가능하다. (그런데 이건 진행하는 프로젝트에서 이렇게 사용할 일은 없을 것 같다.)
  • 해당 컴포넌트가 사용하는 pathname을 파라미터를 통해 직접 넣어줘야만 한다.
  • 자식 컴포넌트가 마운트된 이후에 부모 컴포넌트가 참조하는 store 값을 변경하여 다시 자식 컴포넌트가 렌더링이 되는데, 그래서 두 번 렌더링이 된다.
  • 언마운트 시점에 무조건 store가 초기상태로 돌아가기 때문에 store를 변경시키고싶은 화면에서 store를 변경시키고싶은 화면으로 넘어가더라도 무조건 초기값으로 돌아갔다가 다시 변경시킨다.

단점을 해결하기 위한 방법

두 번 렌더링이 되는 것을 해결하기 위해서는 자식 컴포넌트를 브라우저가 렌더링을 하기 전에 미리 부모가 가지고있는 것을 변경하면 된다.

언마운트 시점에 초기값으로 돌리는 것도 모든 초기값으로 돌릴 화면을 렌더링 하기 전에 체크하고 변경하도록 진행하면 된다.

그렇게 하면, 어떤 화면에서 다른 화면으로 넘어가는 모든 경우의 수를 따졌을 때 두 번 렌더링할 필요도 없으며, 의식해서 보지 않으면 모르겠지만, 찰나의 순간 깜빡이는 화면도 더 이상 보이지 않을 것이다.

하지만, 이렇게 이를 컴포넌트 안의 useEffect를 사용하지 않고 밖으로 빼게된다면, 현재 내 머리에서의 한계일지는 모르겠으나, 변경해야 하는 코드들이 많아진다. 현재 만든 모든 컴포넌트와 앞으로 생성하는 컴포넌트가 어떤 store의 상태를 원하는지 모두 명시해줘야한다.

이렇게 하려고 하면, 단점을 해결하려다가 더 복잡한 구조를 가져가게 된다. 그래서 현재는 위와같은 상태로 놓고 변경하지 않고있다.

추가적으로 알게된 것

처음에 이 문제를 해결하기 위해서 파악을 했었는데, 머릿속에서 나온 추측만 있었고 뭐가 문제인지는 정확하게 몰라 커스텀훅으로 만들은 것을 해당 컴포넌트에서 그대로 실행하도록 되돌리기도 하고, 언마운트 시점에 진행되는 작업도 지워보는 등 여러가지 시도를 했었다.

그러던 중 조건부 css를 빼는 작업도 하게되었는데, 이렇게 되면 store의 상태값이 바뀌더라도 렌더링을 할 필요가 없어 언마운트 시점에 진행하는 일들을 만나지 않을줄 알았다. 그런데 굳이 useSelector를 통해 가져온 store의 값을 사용하지 않더라도 무조건 해당 컴포넌트에서 이 값을 불러오기만 한다면 store의 상태값이 변경될 때마다 전부 리렌더링 되는 경험을 했다. 검색을 해보니 원래 이런 것이라고 한다.