본문 바로가기
FRONT-END/React

React Hook Rule에 대해 이해하기

by 랄라J 2024. 10. 30.

react-hooks/rules-of-hooks error를 마주해 보신 적 있으신가요?

  const [a, setA] = useState('a');
  if (a === 'a') {
    const [b, setB] = useState('b'); // eslint error
  }

위와 같이 코드를 선언하면 아래와 같은 error를 마주하게 됩니다.

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. eslint react-hooks/rules-of-hooks

React Hooks는 모든 컴포넌트 렌더링마다 정확하게 같은 순서로 호출되어야 한다고 합니다.

 

React Hook Rule 공식문서를 보면 지켜야 할 2가지 조건이 있습니다.

1. 최상위(at the Top Level)에서만 Hook을 호출해야 한다.

2. 오직 React 함수 내에서 Hook을 호출해야 한다.

 

그중 첫 번째 조건에 대한 설명을 더 살펴보겠습니다.

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안 된다.
early return이 실행되기 전에 항상 React 함수의 최상위에서 Hook을 호출해야 한다.
이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
이러한 점은 React가 useState와 useEffect가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있다.

 

위 글만 읽고도 바로 이해가 되셨을까요? 

왜 React Hook은 최상위에서 호출되어야 하고, 컴포넌트가 렌더링 될 때마다 동일한 순서로 호출되는 것이 보장되어야 할까요?

React가 특정 state가 어떤 useState 호출에 해당하는지 알 수 있는 이유는 React가 Hook이 호출되는 순서에 의존하기 때문입니다.

React는 첫 번째 실행한 순서대로 Hook을 기억하고 있기 때문에, 만약 조건문 등으로 특정 Hook이 실행되지 않는다면 Hook의 순서가 틀어져 React가 이전 상태값을 제대로 매칭하지 못해 문제가 발생하게 되는 것입니다.

 

조금 더 자세히 이해하기 위해 리액트의 내부 구현 방식을 통해 알아보겠습니다.

자주 등장하죠? 나만의 React 만들기를 진행했던 코드 기반으로 다시 이해해 보겠습니다.

 

나만의 React 만들기(Build your own react) - STEP 8

https://pomb.us/build-your-own-react/ 을 기반으로 학습 내용을 정리한 글입니다.STEP 8 - Hooks이제 function components가 있으니 state를 추가해보는 작업을 해보겠다.const Didact = { createElement, render, useState,};/** @j

rarla-j.tistory.com

Hooks를 구현하는 부분은 위 글에 정리해 놨습니다.

 

useState(hook)를 구현하기 위해 작성했던 코드 중 hook을 선언하는 부분의 코드를 다시 살펴보겠습니다.

let wipFiber = null
let hookIndex = null
​
function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function useState(initial) {
  // TODO
}

순서를 어떻게 관리하는가에 대한 의문의 해결점이 바로 등장했습니다. hook Index입니다.

hookIndex는 fiber(컴포넌트)에서 훅이 사용된 순서를 추적하는 인덱스입니다.

fiber(컴포넌트)가 사용할 훅을 저장할 hooks 배열도 초기화하는 코드를 확인할 수 있죠.

사실 벌써 정답이 유추가 됩니다. hooks에 순차적으로 저장하고 hookIndex로 접근하겠구나!

 

 

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : initial,
  }
​
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}

setState를 반환하기 전 코드까지만 봐도 충분합니다.

useState가 호출되면 hookIndex를 통해 이전에 초기화되었다면 이전 값을, 아니라면 initial을 새 hook의 state로 설정합니다.

 

실제 코드를 보면서 다시 정리를 해보겠습니다.

const Main = () => {
    const [a, setA] = useState('a') // 1
    const [b, setB] = useState('b') // 2
    
    return (
        <div>{a}, {b}</div>
    )
}

간단하고 설명을 위한 코드스럽게 컴포넌트 코드를 작성해 봤습니다.

Main이라는 컴포넌트가 있고, useState를 2개 선언해 단순히 보여주는 컴포넌트입니다.

 

이제 내부적으로 동작하는 것을 순서대로 적어보겠습니다.

[컴포넌트가 처음 렌더링 된 경우]

- wipFiber.alternate.hooks 배열은 비어 있으므로 oldHook은 존재하지 않습니다.

- 때문에 1번 코드가 실행되면 useState에서 initial 값으로 전달한 'a'로 초기화됩니다. 그리고 hooks 배열에 추가합니다.
   -> 여기까지 Main 컴포넌트의 hooks는 [{state: 'a'}]가 됩니다. hookIndex는 1 증가해 1이 되었습니다.

- 2번 코드가 실행되면 useState에서 initial 값으로 전달한 'b'로 초기화됩니다. 그리고 hooks 배열에 추가합니다.
   -> 여기까지 하면 Main 컴포넌트의 hooks는 [{state: 'a'}, {state: 'b'}]가 됩니다. hookIndex는 1 증가해 2가 되었습니다.

 

[컴포넌트가 두 번째로 렌더링 된 경우]

- wipFiber.alternate.hooks 배열은 [{state: 'a'}, {state: 'b'}] 값을 가지고 있습니다.

- 1번 코드가 실행되면 wipFiber.alternate.hooks[hookIndex]로 접근해 oldHook을 통해 이전 렌더링에서 사용된 상태 값을 가져오게 됩니다. 2번 코드도 마찬가지겠죠?

이 방식으로 리액트의 Hook 시스템은 컴포넌트가 매번 새로 호출되더라도 상태가 연속적으로 유지되도록 해주며, 컴포넌트가 변경된 부분만 효율적으로 다시 렌더링 할 수 있게 합니다.

React Hook 룰에서 최상단에 위치해야 하고, 조건문 등에 영향을 받지 않고 순서대로 실행되어야 하는 이유가 바로 index로 참조되기 때문이라는 점 아주 확실하게 이해되었습니다!

 

문제 될 상황까지 한번 살펴보고 마무리하겠습니다~!

const Test = () => {
  const [a, setA] = useState("a");

  if (a === "a") {
    const [b, setB] = useState("b");
  }
  
  const [c, setC] = useState("c");
};

만약, 이 코드가 실행이 된 후 첫 번째 렌더링이 되면 hooks에 순차적으로 [{state: 'a'}, {state: 'b'}, {state: 'c'}]가 될 텐데요.

setA('a1')으로 실행되면 useState('b')는 실행되지 않기 때문에 index 1에 useState('c')가 실행되어 들어가겠죠?

그럼 내부 로직에 따라 oldHook이 있다면 oldHook에 index에 있는 state를 사용하게 되는데 {state: 'b'}가 사용되는 문제가 발생합니다.

 

앞으로는 react-hooks/rules-of-hooks error를 마주해도 이해하고 바로 수정할 수 있게 되었습니다 :)

 

728x90

댓글