Rendered more hooks than during the previous render

react

에러를 해결해보자

Error: Rendered more hooks than during the previous render.

문제 상황

밴픽 시뮬레이터를 개발하던 중, 방 입장 시 유효성을 검증하는 커스텀 훅을 만들었다.

function useVerifyBanPickRoom() {
  const [isValid, setIsValid] = useState<null | boolean>(null);

  useEffect(() => {
    const verifyDoc = async () => {
      const docSnap = await getDoc(...);
      setIsValid(...);
    };
    verifyDoc();
  }, []);

  return isValid;
}

그리고 컴포넌트에서 이렇게 사용했다.

function BanPickRoom() {
  const isValid = useVerifyBanPickRoom();

  if (isValid === null) {
    return <Loading />;
  }

  // isValid 검증 후에만 실행되는 훅들
  const someState = useSomeHook();
  const anotherState = useAnotherHook();

  return <div>...</div>;
}

언뜻 보면 문제가 없어 보인다. 하지만 이 코드는 에러를 발생시킨다.

왜 에러가 발생하는가?

React의 훅은 호출 순서에 의존한다. React는 내부적으로 훅을 연결 리스트로 관리하며, 매 렌더링마다 같은 순서로 훅이 호출될 것이라고 가정한다.

위 코드의 실행 흐름을 따라가보자.

첫 번째 렌더링

  1. useVerifyBanPickRoom() 실행
    • useState → 훅 #1
    • useEffect 등록 → 훅 #2
    • isValidnull 반환
  2. isValid === null이므로 <Loading /> 반환
  3. 이 시점에서 훅은 2개만 호출됨

두 번째 렌더링 (useEffect 실행 후)

  1. useEffect 콜백에서 비동기 검증 완료
  2. setIsValid(true) 호출 → 리렌더링 트리거
  3. useVerifyBanPickRoom() 실행
    • useState → 훅 #1
    • useEffect → 훅 #2
    • isValidtrue 반환
  4. isValid === null 조건 통과
  5. useSomeHook() → 훅 #3 (새로 등장!)
  6. useAnotherHook() → 훅 #4 (새로 등장!)

React는 이전 렌더에서 2개의 훅을 기억하고 있는데, 갑자기 4개의 훅이 호출되니 혼란에 빠진다.

해결 방법

훅은 항상 컴포넌트의 최상위 레벨에서, 조건문 없이 호출해야 한다.

function BanPickRoom() {
  const isValid = useVerifyBanPickRoom();

  // 모든 훅을 조건문 위에서 호출
  const someState = useSomeHook();
  const anotherState = useAnotherHook();

  if (isValid === null) {
    return <Loading />;
  }

  if (!isValid) {
    return <InvalidRoom />;
  }

  return <div>...</div>;
}

또는 컴포넌트를 분리하는 방법도 있다.

function BanPickRoomWrapper() {
  const isValid = useVerifyBanPickRoom();

  if (isValid === null) {
    return <Loading />;
  }

  if (!isValid) {
    return <InvalidRoom />;
  }

  return <BanPickRoom />;
}

function BanPickRoom() {
  // 이 컴포넌트는 isValid가 true일 때만 마운트됨
  const someState = useSomeHook();
  const anotherState = useAnotherHook();

  return <div>...</div>;
}