본문 바로가기

Front-End: Web/React.js

[React] Error: Rendered more hooks than during the previous render

반응형

 

 

 

이슈/원인

Rendered more hooks than during the previous render

이전 렌더링에서 호출된 hooks보다, 현재 렌더링에서 호출된 hooks가 더 많은 경우에 발생하는 에러

 

React는 Hook이 호출되는 순서에 의존한다

  • React는 Hook이 호출되는 순서에 의존한다.
  • Hook이 함수의 상태를 기억하기 위해서, 호출 순서를 이용하여 함수의 상태를 기억했다가 이전의 상태를 가져온다.
  • 따라서 렌더링되는 순서가 동일해야 React가 locale state를 각 Hook에 연동할 수 있다.
  • 즉, Hook가 매 렌더링마다 동일한 순서로 동일한 개수만큼 호출을 보장 받아야한다.
  • 그래서 Hook을 조건문, 반복문, 함수 안에서 작성하는게 금지되어 있고, 컴포넌트 최상단에서 호출하라고 권장하는 것이다.

 

예제

예로 들어서 다음과 같은 코드가 있다고 하자.

function Form() {
  // 1. name이라는 state 변수를 사용하세요.
  const [name, setName] = useState('Mary');

  // 2. Effect를 사용해 폼 데이터를 저장하세요.
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. surname이라는 state 변수를 사용하세요.
  const [surname, setSurname] = useState('Poppins');

  // 4. Effect를 사용해서 제목을 업데이트합니다.
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

첫 번째 렌더링

  1. "Mary"라는 name state 변수를 선언한다.
  2. name(=Mary)을 localeStorage에 저장하는 effect를 추가한다.
  3. "Poppins"라는 surname state 변수를 선언한다.
  4. 제목을 name(=Mary) + '  ' + surname(=Poppins) 으로 업데이트하는 effect를 추가한다.

두 번째 렌더링 (ex. setName("Hey")를 하여 리렌더링됐을 때)

  1. "Hey"라는 name state 변수 값을 읽는다. ("Mary"인자는 무시함)
  2. name(=Hey)을 localeStorage에 저장도록 effect가 대체된다. (Mary -> Hey)
  3. "Poppins"라는 surname state 변수를 읽는다. ("Poppins"인자는 무시함)
  4. 제목을 name(=Hey) + ' ' + surname(=Poppins) 으로 업데이트하도록 effect가 대체된다.

 

만약 if문 안에 Hook을 넣는다면?

function Form() {
  // 1. name이라는 state 변수를 사용하세요.
  const [name, setName] = useState('Mary');

  // 2. Effect를 사용해 폼 데이터를 저장하세요.
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

  // 3. surname이라는 state 변수를 사용하세요.
  const [surname, setSurname] = useState('Poppins');

  // 4. Effect를 사용해서 제목을 업데이트합니다.
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

첫 번째 렌더링에서 if 조건은 true이므로 Hook이 동작하여, 위와 동일하게 동작한다.

두 번째 렌더링 (ex. setName("")를 하여 리렌더링됐을 때)

1. 위와 동일하게 동작
 - 🔴  if문을 통과하지 못하므로 useEffect Hook을 건너뛴다.
2. 🔴 (3이었던). surname state 변수를 읽는데 실패한다.
3. 🔴 (4였던). 제목을 업데이트하기 위한 effect가 대체되는 데 실패했습니다

이전의 첫 번째 렌더링과 순서가 동일할 것을 기대했는데, 그렇지 않다. 그래서 건너뛴 Hook 다음에 호출되는 Hook이 순서가 하나씩 밀리면서 버그를 발생시키게 된다.

이것이 컴포넌트 최상위(the top of level)에서 Hook이 호출되어야하는 이유다!

 

만약 조건부로 effect를 실행하고 싶다면, Hook 내부에 조건문을 작성하도록 하자.

useEffect(function persistForm() {
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

 

 

 

해결

전체 코드를 살펴본 결과, 다음과 같은 형태의 문제 코드가 있었다. 즉, early return이 문제였던 것.

const Component = () => {
    ...
    if (!queryGetPlansWithInfinityScroll) return <></>
    ...
    
    useEffect(() => {
    	...
    }, [])
    
    ...
	
}

 

queryGetPlansWithInfinityScroll 부분과 Hook를 컴포넌트 분리하여 해당 이슈를 해결하였다.

const ParentComponent = () => {
    ...
    if (!queryGetPlansWithInfinityScroll) return <></>
    return <Component />;
}
const Component = () => {
    useEffect(() => {
    	...
    }, [])
    
    ...
}

 

 

 

 


참고 자료

https://ko.legacy.reactjs.org/docs/hooks-rules.html#explanation

반응형