React

[ React ] 무한 렌더링 탈출기

yebeen 2024. 8. 19. 15:57

문제

프로젝트를 진행하면서 만났던 무한 렌더링 문제

문제의 코드는 다음과 같다.

export default function SideBar(){
    const [isChat,setIsChat] = useState(true);
    return (
        <View>
            <Header>
                <div>
                    <button onClick={setIsChat(!isChat)}>채팅</button>
                    <button onClick={setIsChat(!isChat)}>내담자 정보</button>
                </div>
                <div>
                    <FontAwesomeIcon icon={faAnglesRight} />
                </div>
            </Header>
            {
                isChat?
                <Chat/>
                :
                <UserInfoComponent/>                    
            }
        </View>
    )
}

 

 

 

원인

사전지식

React 컴포넌트는 상태(state)나 속성(props)이 변경될 때마다 다시 렌더링된다.

 

이 문제는 최대한 코드 짧게 짜고 함수 하나 더 안만들겠다고 욕심 부리기 + 기본 개념을 놓쳐버린 부분으로

내 코드에선 onClick 이벤트 핸들러에 함수 호출(setIsChat(!isChat))을 직접 전달하고있다.

여기서 직접 전달이 문제였다.

 

이렇게 되면 처음 렌더링 될 때 (setIsChat(!isChat))함수가 호출된다.

여기서 상태값이 반전되었기에 렌더링이 일어나고 또 다시 setIsChat이 호출되어

상태값이 반전되고 그로 인해 렌더링이 다시 일어나는 ... 이 과정이 무한으로 반복된다🥲

 

 

 

해결방법

방법1. 함수 참조 전달하기

<button onClick={() => setIsChat(!isChat)}>채팅</button>

 

방법2. 새로운 함수를 생성해 해당 함수 안에 setIsChat을 호출하기

const toggleChat = () => {
  setIsChat(prev => !prev);
};

<button onClick={toggleChat}>채팅</button>

 

 

 


 

 

 

여기까지가 내가 만난 무한 렌더링문제이고

무한 렌더링 해결하면서 이 외에 다른 원인은 뭐가 있을까싶어서 알아봤는데

추가 내용은 일단 이 글을 올리고 수정하면서 추가할 예정이다 🐥 to be continued ...

 

죽지않고 돌아온 업데이트

어떤 한 분이 잘 정리해주셔서 보면서 공부하고 정리해보았다.

 

🔥 React: 리액트 useEffect 무한 루프 탈출하기

useEffect 무한 루프 탈출하기

velog.io

 

 


 

원인 1.  종속성 배열에 종속성을 전달하지 않을 경우

useEffect 함수에 종속성이 없으면 무한 루프가 발생한다.

종속성이 없는 경우 모든 업데이트 주기에 기본적으로 트리거를 사용하게되고

결과적으로 여기의 앱은 모든 렌더에 대해 setCount 의 기능을 실행하게 되고 이는 무한루프로 이어진다.

 

예시 코드

function Test() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount((count) => count + 1); 
  }); //종속성 배열 없음
  return (
      <p> value of count: {count} </p>
  );
}

 

 

발생과정

  1. 첫 번째 렌더에서 count 값을 확인한다.  이 후 프로그램은 useEffect 함수를 실행한다.
  2. 그러고 나서 useEffect는 setCount 메소드를 호출하고 count Hook의 값을 업데이트한다.
  3. 값이 업데이트 되었기에 UI가 재렌더링된다.
  4. UI가 재렌더링 되었기 때문에, useEffect는 모든 렌더 사이클에서 실행되므로 또 다시 setCount 함수를 호출한다.
  5. 위의 단계는 모든 렌더에서 발생하므로 앱이 충돌하게 된다

 

해결책

useEffect의 두 번째 인수로 빈 배열을 준다 > 첫 마운트에서만 useEffect가 실행된다.

 

 

 


 

 

원인 2. 함수를 종속성으로 사용할 경우

바로 코드를 보며 알아보자

 

예시코드

function Test() {
  const [count, setCount] = useState(0);

  function logResult() {
    return 2 + 2;
  }
  useEffect(() => {
    setCount((count) => count + 1);
  }, [logResult]);
  return (
    <div className="App">
      <p> value of count: {count} </p> {/*count의 값 보여줌*/}
    </div>
  );
}

 

이 케이스에서 기억해야하는 것은 useEffect는 얕은 비교이다.

디펜던시가 업데이트되었는지 확인하기 위해 이 작업을 수행한다.

여기서 발생하는 문제는 렌더하는 동안 react에서 logResult(함수)의 참조를 재정의한다.

재정의하면 결국 사이클이 다시 돌기때문에 useEffect 함수가 다시 트리거된다.

그래서 오류가 발생할 때까지 setCount Hook을 호출한다.

이로인해 버그와 불안정성을 가져온다.

 

 

해결책

이를 해결하기 위해선 useCallback Hook을 사용하는 방법이 있다.

const logResult = useCallback(() => {
  return 2 + 2;
}, []); //이제 logResult가 메모된다.
useEffect(()=> {
  setCount((count)=> count+1);
},[logResult]); //logResult 참조가 동일하게 유지되므로 무한 루프 오류가 없어진다.

 

 

 


 

 

원인 3. 배열을 종속성으로 사용할 경우

const [count, setCount] = useState(0);
const myArray = ["one", "two", "three"];

useEffect(() => {
  setCount((count) => count + 1); 
}, [myArray]);

 

위의 코드를 보면 배열 myArray를 종속성으로 사용했다.

하지만 배열의 값이 바뀌지도 않는데 왜 무한루프가 발생하는 걸까?

 

발생 원인

1. React는 디펜던시의 참조가 변경되었는지 확인하기 위해 얕은 비교를 한다. 

    이때 각 렌더에서 myArray에 대한 참조는 계속 변하게 되고 useEffect는 setCount 콜백을 트리거한다.

2. 따라서 myArray의 불안정한 참조 값으로 인해 React는 모든 렌더 사이클에서 useEffect를 호출한다...

 

 

해결책

useRef Hook 사용하기

이를 사용하면 참조가 변경되지 않도록 변경 가능한 객체가 반환된다.

const [count, setCount] = useState(0);

const { current: myArray } = useRef(["one", "two", "three"]);

useEffect(() => {
  setCount((count) => count + 1);
}, [myArray]);

 

 

 


 

 

원인 4. 객체를 종속성으로 사용할 경우

이 경우도 배열을 종속성으로 사용한 경우와 똑같이 얕은 비교를 통해 person의 참조 값이 변경되었는지 확인한다.

당연히 렌더링할 때마다 person 객체의 참조 값이 변경되므로 react는 useEffect를 다시 실행한다.

그렇기에 모든 업데이트 주기마다 setCount가 호출되어 무한 루프가 발생한다.

const [count, setCount] = useState(0);
const person = { name: "Rue", age: 17 }; //객체 생성
useEffect(() => {
  //매번 count 값을 증가시킴 
  //person'의 값을 변경함
  setCount((count) => count + 1);
}, [person]); //종속 배열은 객체를 인수로 포함함
return (
  <div className="App">
    <p> Value of {count} </p>
  </div>
);

 

 

해결책

useMemo사용하기

useMemo는 디펜던시가 변경될 때 메모된 값을 계산한다.

또한 메모된 변수가 있기 때문에 각 렌더링 중에 상태의 참조 값이 변경되지 않도록 해준다.

const person = useMemo(
  () => ({ name: "Rue", age: 17 }),
  [] //디펜던시가 없으므로 값이 변경되지 않는다. 즉, useEffect에서 person객체를 디펜던시로 사용해도 참조값이 변경되지 않기 때문에 유지가 된다는 뜻이다.
);
useEffect(() => {
  setCount((count) => count + 1);
}, [person]);

 

 


 

 

원인 5. 잘못된 종속성을 전달할 경우

useEffect 함수에 잘못된 변수를 전달하면 React가 오류를 발생시킨다.

const [count, setCount] = useState(0);

useEffect(() => {
  setCount((count) => count + 1);
}, [count]); //종속성 배열에 count 전달함

return (
  <div className="App">
    <button onClick={() => setCount((count) => count + 1)}>+</button>
    <p> Value of count{count} </p>
  </div>
);

 

발생과정

  1. 위 코드에 useEffect 메소드 내에서 카운트 값을 업데이트하라고 작성하였다.
  2. 문제는 count Hook을 의존성 배열에도 전달해 두었다.
  3. 즉, count 값이 업데이트 될 때마다, React가 useEffect를 호출한다.
  4. 결과적으로 useEffect Hook은 setCount를 호출하므로 count를 다시 업데이트한다.
  5. 이 때문에 React는 무한 루프에 빠진다

해결책

빈 종속성 배열 사용하기

 

 

참고자료