"React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render."
최근에 리액트로 팀 프로젝트를 하면서 Invalid hook call 에러나 위와 같은 Eslint error를 종종 봤는데요.
매번 아차차 하고 넘어갔던 에러이지만, 왜 훅이 렌더링 될 때마다 같은 순서를 유지해야 하는지 궁금해서 찾아보다가 좋은 참고 자료를 발견하고 이 글을 쓰게 되었습니다.
리액트 공식문서에는 훅의 규칙을 명시하고 있는데, 일단 훅이란 뭔지 간단히 설명해 볼게요.
Hook 이란?
함수 컴포넌트에서 상태와 생명주기 기능을 "연동"(hook)할 수 있게 해주는 특별한 함수
쉽게 말하면, 훅은 함수 컴포넌트에 '기억력'을 주고 웹사이트가 변화할 때 자동으로 무언가를 할 수 있게 도와주는 특별한 도구입니다. 좋아요 버튼을 누를 때마다 숫자가 1씩 증가하는 것도, 이전 숫자를 기억하고 업데이트할 수 있게 훅이 도와주기 때문이에요.
리액트에서는 use로 시작하는 함수를 보통 Hook으로 간주합니다.
다시 돌아와서, 훅의 규칙은 크게 두 가지가 있습니다.
Hook의 규칙
- Hook을 최상위 레벨에서만 호출한다.
반복문, 조건문, 중첩 함수, 또는 try/ catch/ finally 블록 내부에서 호출하지 않는다. - Hook을 React 함수에서만 호출한다.
Hook을 React 함수 컴포넌트나 커스텀 Hook에서 호출한다.
이번 글에서는 첫 번째 규칙 중 조건문에서 훅을 사용하면 어떤 일이 일어나는지 알아볼게요.
리액트에서는 왜 조건부로 훅을 쓰면 안 될까?
그 이유를 알기 위해서, 먼저 Fiber Tree의 개념에 대해 알아야 합니다.
Fiber Tree
애플리케이션의 UI 업데이트를 효율적으로 관리하기 위한 데이터 구조
Fiber Tree는 Fiber라는 자바스크립트 객체들을 트리 구조로 연결하여 UI를 표현하고 관리하는 데이터 구조입니다. Fiber Tree는 컴포넌트의 상태(states, data)와 Props를 메모리에 저장합니다. 그리고 리액트는 Fiber Tree를 통해 계산된(가상 DOM과 실제 DOM을 비교) 변경 사항들을 찾고 이를 기반으로 리렌더링 합니다. 즉, Fiber Tree는 렌더링 과정 최적화를 돕는 역할을 합니다.
그다음으로, Fiber Tree가 어떻게 생성되는지 예시로 살펴볼게요.
Fiber Tree 생성 방식
Fiber Tree는 JSX 문법 요소들이 호출되면 생성이 됩니다.

App 컴포넌트가 처음 렌더링이 일어나면 아래와 같은 순서로 트리구조가 생성됩니다.
- <App /> 컴포넌트가 호출되면, A 노드가 Fiber Tree에 생성됩니다.
- <Greetings /> 컴포넌트가 호출되고, B 노드가 A노드 하위에 생성됩니다.
- button 태그의 JSX 요소가 호출되고, C 노드가 A노드 하위에 생성됩니다.
- p 태그의 JSX 요소가 호출되고, D 노드가 B 노드 하위에 생성됩니다.
이후, 리액트는 생성된 Fiber Tree를 바탕으로 DOM Tree를 생성합니다. 이때 HTML Element가 아닌 <App />이나 <Greetings />와 같은 Pure React Component들은 제외됩니다.
위에서 언급했듯이, Fiber Tree는 내부에 컴포넌트의 상태, props와 같은 데이터들을 저장합니다. 그런데 보통 한 컴포넌트에는 여러 개의 데이터들이 존재하는데, Fiber Tree는 이 많은 데이터들을 어떻게 관리할까요?
Fiber Tree에서 훅 데이터 저장 구조
훅을 구성하는 데이터들은 단일 연결리스트로 Fiber Tree 내부에서 관리됩니다. 그리고 이 연결리스트의 순서는 훅이 호출된 순서에 의존합니다.

위의 사진처럼, Rachel의 데이터가 먼저 저장된 후, 29라는 데이터가 저장되어 연결리스트가 구성됩니다.
바로 이 ‘훅의 호출 순서 의존성’이 조건부로 훅을 호출할 수 없는 이유입니다. 왜냐하면, 리액트에서 리렌더가 발생할 때 이 연결리스트의 순서에 의존하여 데이터를 구별하고, 업데이트시킵니다. 그런데 리렌더 시 처음 렌더된 훅의 호출 순서와 달라진다면, 리액트가 의도하는 데이터를 잘 찾아서 업데이트할 수 있을까요?
어떤 일이 일어나는지 예시로 살펴볼게요 🤔
조건부에서 훅을 호출하면 어떤 일이 일어날까?

처음 렌더링 시, showName은 false로 저장이 되어, if문은 실행되지 않습니다. 따라서 다음 State인 age가 29로 저장이 되어 2개의 노드를 가진 연결리스트가 생성됩니다.
이제 사용자가 Show name 버튼을 클릭했다고 가정해 보고 상황을 파악해 봅시다.

showName 값이 true로 변경되어, 리액트는 앱을 리렌더링 하게 됩니다. 그럼 처음 호출되는 훅은 여전히 showName에 대한 State이므로, 연결리스트에 저장된 첫 번째 데이터인 false는 true로 변경됩니다.
하지만 두 번째로 호출되는 훅이 달라집니다. showName 값이 true로 변경되어, if문이 실행되고 name에 대한 State가 생성됩니다. 이 훅은 두번째로 호출이 되었으니, 두 번째 데이터인 29를 가리키게 됩니다. 즉, name은 ‘Rachel’로 초기화되지 않고 age 값이었던 29를 읽게 됩니다.
여기부터 데이터가 꼬이게 됩니다.... 이후 age에 대한 훅이 실행되지만, 세 번째 데이터 값은 존재하지 않기 때문에 Invalid hook call 에러가 발생하게 됩니다. 😱

결론
훅의 순서를 일관되게 유지하는 것은 정확한 리렌더가 일어나게 하고, 예상치 못한 동작을 방지하기 위해 필요합니다. 따라서 훅의 호출 순서를 유지해야 하므로 조건부에서 훅을 호출할 수 없습니다.
하지만 조건부로 사용될 수 있는 훅도 있습니다.
결론부터 말하자면 이 훅들은 연결리스트로 관리되지 않고, 다른 방식으로 관리가 되기 때문에 조건부에서 호출이 가능합니다.
useContext
useContext에 의해 생성되는 객체는 다른 hook과는 달리 연결리스트에 의해 관리되지 않고 Fiber Tree 밖 별도의 영역에서 관리됩니다. 때문에, 조건부로 호출되어도 다른 hook에 영향을 끼치지 않습니다.

use (React 19)
React 19에서 새롭게 등장할 use 훅은 콜백 함수와 렌더링 일시 중단 기술을 사용하여 조건에 따라 데이터 로드 또는 작업을 수행하도록 설계되었습니다. 이는 기존 훅과 달리 연결 리스트 구조에 영향을 미치지 않아 조건부 사용이 가능합니다.
이외 useMemo , useCallback 도 조건부에서 사용 가능합니다.
참고
https://portal.gitnation.org/contents/you-cant-use-hooks-conditionally-or-can-you
'FE > React' 카테고리의 다른 글
[React] useState로 댓글 수정 구현하기 (0) | 2023.12.14 |
---|---|
[React] 로그인-Recoil로 localStorage에 토큰 저장하기 (0) | 2023.11.19 |
[React] Axios로 회원가입 POST요청하기(CORS 에러) (0) | 2023.11.12 |
[React]리액트 훅(React Hook) - useCallback과 useEffect (0) | 2023.11.05 |
[React] react-router-dom@v6의 createBrowserRouter와 RouterProvider (0) | 2023.09.24 |