jeong-min.com
🚫
Dev

useState를 반복문과 조건문에서 사용할 수 없는 이유

2024.09.09

리액트 공식 문서를 읽다보면 다음과 같은 문장을 마주할 수 있다.

주의사항
useState는 Hook이므로 컴포넌트의 최상위 레벨이나 직접 만든 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 안에서는 호출할 수 없습니다. 필요한 경우 새 컴포넌트를 추출하고 state를 그 안으로 옮기세요.

 

🤔 “왜 useState를 반복문이나 조건문 안에서 쓰면 안 되는 걸까?”

 

React 훅의 동작 원리

리액트 훅의 핵심 원리 중 하나는 훅의 호출 순서가 항상 같아야 한다는 것이다.
리액트는 컴포넌트가 리렌더링될 때마다 훅들을 순서대로 실행하는데,
만일 이 순서가 변경된다면 리액트는 어떤 상태가 어떤 useState 호출에 해당하는지 추적할 수 없게 된다.

만일 조건문 내에서 useState를 사용한다면,

function ExampleComponent(props) {
  if (props.someCondition) {
    const [state, setState] = useState(initialValue);
  }
  // ...
}

조건의 값에 따라 훅의 호출 순서가 바뀌게 된다.

반복문 내에서도 마찬가지다.

function ExampleComponent() {
  const items = [1, 2, 3];
  items.forEach(() => {
    const [state, setState] = useState(initialValue);
  });
  // ...
}

반복 횟수에 따라 훅의 호출 횟수가 달라지게 된다.

위 두 경우 모두 리액트가 예측할 수 없는 동작을 유발할 수 있다는 문제가 있다.

 

훅의 호출 순서가 항상 같아야, 리액트가 각 훅이 어떤 상태과 연관되어 있는지 정확히 알 수 있고,
결과적으로 컴포넌트의 동작을 예측 가능하고 일관되게 만들 수 있다.

 

🤔 “내부적으로 어떻게 상태를 관리하기에 훅의 호출 순서가 항상 같아야 하는 걸까?”

 

React 훅의 상태 관리 내부 메커니즘

리액트는 각 컴포넌트의 훅들을 LinkedList(연결 리스트)로 관리한다.

0

 

✋ 잠깐! LinkedList가 뭔데?

배열의 대표적인 자료구조, LinkedList와 ArrayList를 잠깐 짚고 넘어가보자.

  • LinkedList

    • 각 노드가 독립적인 메모리 공간에 할당되고, 포인터로 연결됨
    • 특정 위치 찾을 때 O(n)
    • 노드 참조만 있으면 추가/삭제 O(1)
    • 크기 변경이 자유롭지만, 캐시 효율성이 떨어짐
    • 추가/삭제 빈번할 때, 데이터 크기 동적으로 변할 때, 메모리 효율적으로 사용해야 할 때 유용함
  • ArrayList

    • 연속된 메모리 공간에 요소들이 저장됨
    • 인덱스를 통한 임의 접근이 O(1) 시간
    • 끝에 추가/삭제는 O(1)이지만 중간 추가/삭제는 O(n)
    • 크기 변경에 비용이 들지만, 캐시 효율성이 높음
    • 요소에 빈번하게 접근할 때, 데이터 크기를 미리 알고 있을 때, 주로 끝에서 추가/삭제 일어날 때 유용함

 

리액트 내 LinkedList의 각 노드는 다음과 같은 구조를 가진다.

{
  memoizedState: any,
  next: Hook | null
}
  • memoizedState: 훅의 현재 상태를 저장
  • next: 다음 훅을 가리키는 포인터

 

컴포넌트가 처음 렌더링될 때, 리액트는 각 훅 호출마다 새로운 노드를 생성하여 LinkedList에 추가한다.

function ExampleComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // ...
}

만일 위와 같은 컴포넌트가 렌더링되면, 리액트는 다음과 같은 LinkedList를 만든다.

[Hook(useState: count)] -> [Hook(useState: name)] -> null

 

그리고 컴포넌트 리렌더링 시, 리액트는 생성된 LinkedList를 순회하면서 각 상태를 업데이트하게 된다.
따라서 훅의 호출 순서가 변경된다면 LinkedList의 구조가 달라지게 된다.

function ExampleComponent(props) {
  if (props.isLoggedIn) {
    const [name, setName] = useState('');
  }
  const [count, setCount] = useState(0);
  
  // ...
}

위 코드의 경우, props.isLoggedIn의 값에 따라 LinkedList의 구조가 달라진다.

  • true일 때: [Hook(useState: name)] -> [Hook(useState: count)] -> null
  • false일 때: [Hook(useState: count)] -> null

리액트는 이전 렌더링의 LinkedList 구조를 기반으로 상태를 업데이트하기 때문에,
이러한 구조 변경은 상태 값들이 뒤섞이는 결과를 초래할 수 있다.

 

오늘의 결론: 안 된다는 데에는 다 이유가 있다!

1

👇 도움이 되셨다면 👇

B

u

y

M

e

A

C

o

f

f

e

e

© Powered by danmin