jhpka's blog

[React] 리액트 내부 동작

Admin User

문제 상황

도구 호출을 포함한 응답을 렌더링한 후, 재응답 버튼을 누르면 다음과 같은 에러가 발생했습니다:

text
Uncaught Error: Rendered fewer hooks than expected.
This may be caused by an accidental early return statement.
    at renderWithHooks
    at updateFunctionComponent
    at beginWork

에러 원인

ContentParts.tsx 파일에서 React Hooks 규칙을 위반했기 때문입니다:

javascript
const ContentParts = memo(({ content, ... }: ContentPartsProps) => {
  const localize = useLocalize();                                    // Hook #1
  const [showThinking, setShowThinking] = useRecoilState(...);      // Hook #2
  const [isExpanded, setIsExpanded] = useState(...);                // Hook #3
  const messageAttachmentsMap = useRecoilValue(...);                // Hook #4
  const attachmentMap = useMemo(...);                               // Hook #5
  const hasReasoningParts = useMemo(() => {
    const hasThinkPart = content?.some(...) ?? false;  // ❌ content가 undefined면?
    //...
  }, [content]);                                                    // Hook #6

  if (!content) {  // ❌ 재응답 시 content가 없어짐
    return null;   // Early return
  }

  if (edit === true && enterEdit && setSiblingIdx) {
    return <>...</>;
  }

  // ...
});

문제:

  • 재응답 버튼 클릭 시 메시지 상태가 초기화되면서 content가 일시적으로 undefined
  • 모든 Hook은 호출되지만 if (!content)로 인해 early return
  • 다음 렌더링에서 content가 채워지면서 Hook 개수 변경
  • React: "어? Hook 개수가 달라졌네?" → 에러 발생

해결 방법

모든 Hook이 항상 같은 순서로 호출되도록 수정:

javascript
const hasReasoningParts = useMemo(() => {
  if (!content) {
    return false;  // ✅ useMemo 내부에서 처리
  }
  const hasThinkPart = content.some(...);
  const allThinkPartsHaveContent = content.every(...);
  return hasThinkPart && allThinkPartsHaveContent;
}, [content]);

// ✅ 모든 Hook 호출 완료 후 early return
if (!content) {
  return null;
}

Early Return이란?

Early Return은 함수 중간에 조건에 따라 일찍 반환하는 패턴입니다.

일반적인 사용 예시

javascript
function processUser(user) {
  // Early return으로 예외 케이스 먼저 처리
  if (!user) {
    return null;
  }

  if (!user.isActive) {
    return { error: 'User is inactive' };
  }

  // 정상 로직
  return {
    name: user.name,
    email: user.email
  };
}

일반적으로는 코드 가독성을 높이는 좋은 패턴이지만, React Hooks와 함께 사용하면 문제가 발생합니다.

React에서 문제가 되는 경우

javascript
function BadComponent({ data }) {
  const [count, setCount] = useState(0);        // Hook #1
  const [name, setName] = useState('');         // Hook #2

  // ❌ Early return - 조건부로 함수를 빠져나감
  if (!data) {
    return null;
  }

  const value = useMemo(() => data.value, [data]); // Hook #3

  return <div>{count}</div>;
}

시나리오:

  1. 첫 렌더링 (data 있음)

    text
    Hook 순서: [useState, useState, useMemo]
    총 3개의 Hook 호출
  2. 두 번째 렌더링 (data 없음)

    text
    Hook 순서: [useState, useState]
    총 2개의 Hook 호출 (useMemo는 건너뜀!)

React는 3번째 Hook이 사라진 것을 감지하고 "Rendered fewer hooks than expected" 에러를 발생시킵니다.


Hooks 규칙

React가 Hook을 관리하는 방식

React Hooks는 호출 순서를 기반으로 상태를 관리합니다.

React는 각 컴포넌트의 Hook들을 배열로 관리합니다:

javascript
// 첫 번째 렌더링
컴포넌트의 Hook 배열 = [
  { type: 'useState', value: 0 },      // index 0
  { type: 'useState', value: '' },     // index 1
  { type: 'useMemo', value: {...} },   // index 2
]

// 두 번째 렌더링 - 같은 순서여야 함!
컴포넌트의 Hook 배열 = [
  { type: 'useState', value: 5 },      // index 0 ✅
  { type: 'useState', value: 'abc' },  // index 1 ✅
  { type: 'useMemo', value: {...} },   // index 2 ✅
]

왜 순서가 중요한가?

React는 Hook을 변수명이 아닌 호출 순서로 구분합니다:

javascript
function MyComponent() {
  const [name, setName] = useState('철수');      // 1번째 Hook
  const [age, setAge] = useState(25);            // 2번째 Hook
  const [city, setCity] = useState('서울');      // 3번째 Hook

  return <div>{name}, {age}, {city}</div>;
}

React 내부에서 저장하는 방식:

javascript
// React는 변수명(name, age, city)을 모릅니다!
컴포넌트의 Hook 배열 = [
  '철수',  // index 0 - "1번째 useState"
  25,      // index 1 - "2번째 useState"
  '서울'   // index 2 - "3번째 useState"
]

React는 다음과 같이 기억합니다:

javascript
✅ "1번째 Hook은 '철수'야"    (변수명 name은 모름)
✅ "2번째 Hook은 25야"        (변수명 age는 모름)
✅ "3번째 Hook은 '서울'이야"  (변수명 city는 모름)

이렇게는 기억하지 않습니다:

javascript
❌ "name Hook은 '철수'야"
❌ "age Hook은 25야"
❌ "city Hook은 '서울'이야"

재렌더링 시 동작 방식

버튼을 클릭해서 setAge(26)을 호출하면:

javascript
// 1번째 렌더링
1번째 useState 호출 → 배열[0]에서 '철수' 반환
2번째 useState 호출 → 배열[1]에서 25 반환
3번째 useState 호출 → 배열[2]에서 '서울' 반환

// 2번째 렌더링 (age가 26으로 변경됨)
1번째 useState 호출 → 배열[0]에서 '철수' 반환  ✅
2번째 useState 호출 → 배열[1]에서 26 반환      ✅ (업데이트됨!)
3번째 useState 호출 → 배열[2]에서 '서울' 반환  ✅

순서가 바뀌면 발생하는 문제

javascript
function BadComponent({ showAge }) {
  const [name, setName] = useState('철수');      // 1번째 Hook

  if (showAge) {
    const [age, setAge] = useState(25);          // 2번째 Hook (조건부!)
  }

  const [city, setCity] = useState('서울');      // 2번째 또는 3번째 Hook??

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

showAge = true일 때

javascript
React 내부 배열:
[
  '철수',    // index 0 - name
  25,        // index 1 - age
  '서울',    // index 2 - city
]

1번째 useState() → index 0 → '철수' 반환
2번째 useState() → index 1 → 25 반환
3번째 useState() → index 2 → '서울' 반환

showAge = false일 때 (재렌더링)

javascript
React 내부 배열은 그대로:
[
  '철수',    // index 0
  25,        // index 1
  '서울',    // index 2
]

1번째 useState() → index 0 → '철수' 반환  ✅
// age Hook은 건너뜀!
2번째 useState() → index 1 → 25 반환      ❌❌❌ 엉망!

문제:

  • city useState가 2번째로 호출됨
  • 하지만 React는 "2번째 Hook = index 1 = age의 값(25)"이라고 생각함
  • 결과: city25가 들어감! 💥

React 내부 구현 (개념적)

javascript
// React 내부 (단순화)
let currentHookIndex = 0;
let hooksArray = [];

function useState(initialValue) {
  const hookIndex = currentHookIndex;  // 현재 몇 번째인지 기록
  currentHookIndex++;                   // 다음 Hook을 위해 증가

  // 이전에 저장된 값이 있으면 사용, 없으면 초기값
  if (hooksArray[hookIndex] === undefined) {
    hooksArray[hookIndex] = initialValue;
  }

  const setState = (newValue) => {
    hooksArray[hookIndex] = newValue;  // 순서로 저장
    리렌더링();
  };

  return [hooksArray[hookIndex], setState];
}

// 컴포넌트 렌더링 시작 시 인덱스 초기화
function renderComponent() {
  currentHookIndex = 0;  // 매번 0부터 시작!
  MyComponent();
}

핵심:

  • currentHookIndex는 매 렌더링마다 0부터 시작
  • 각 Hook 호출마다 1씩 증가
  • 이전 렌더링과 같은 순서로 호출되어야 같은 인덱스를 가짐

올바른 패턴

✅ 방법 1: Hook을 먼저 호출

javascript
function GoodComponent({ data }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const value = useMemo(() => data?.value, [data]);

  // Hook 호출 후 early return
  if (!data) {
    return null;
  }

  return <div>{count}</div>;
}

✅ 방법 2: useMemo 내부에서 처리

javascript
function GoodComponent({ data }) {
  const [count, setCount] = useState(0);

  const value = useMemo(() => {
    if (!data) return null;  // Hook 내부에서 조건 처리
    return data.value;
  }, [data]);

  if (!data) {
    return null;
  }

  return <div>{count}</div>;
}

호출 순서 관리

"호출 순서만 기억한다"는 의미

React는 변수명을 모릅니다. 오직 몇 번째 Hook인지만 기억합니다.

변수명은 JavaScript 런타임에서 사라집니다

javascript
const [name, setName] = useState('철수');

이 코드가 실행되면:

  • namesetName은 그냥 지역 변수
  • React는 이 변수명을 알 수 없음
  • 구조분해할당은 컴파일 후 사라짐

실제 트랜스파일 결과:

javascript
// 바벨/TypeScript 컴파일 후
var _useState = useState('철수');
var name = _useState[0];
var setName = _useState[1];

React 입장에서는 "누가 이 값을 받아갔는지" 알 수 없습니다.

클로저(Closure) 활용

React는 JavaScript의 클로저를 활용합니다:

javascript
function useState(initialValue) {
  const index = currentIndex++;  // 현재 인덱스를 '기억'

  if (!hooks[index]) {
    hooks[index] = initialValue;
  }

  const setState = (newValue) => {
    hooks[index] = newValue;  // 클로저로 'index'를 기억!
    rerender();
  };

  return [hooks[index], setState];
}

setState 함수는 자신이 몇 번째 Hook인지 클로저로 기억합니다.

javascript
const [count, setCount] = useState(0);  // index 0
const [name, setName] = useState('');   // index 1

setCount(5);  // "0번째 Hook을 5로 바꿔!" (클로저로 0을 기억)
setName('a'); // "1번째 Hook을 'a'로 바꿔!" (클로저로 1을 기억)

심지어 비동기 환경에서도 작동합니다:

javascript
const [count, setCount] = useState(0);  // currentIndex = 0
const [name, setName] = useState('');   // currentIndex = 1

// setCount는 "0번째 인덱스를 바꾸는 함수"를 기억
// setName은 "1번째 인덱스를 바꾸는 함수"를 기억

setTimeout(() => {
  setCount(10);  // 3초 후에도 "0번째"를 정확히 기억!
}, 3000);

설계 이유

왜 변수명이 아닌 순서로 관리할까?

1. JavaScript의 한계

JavaScript에서는 변수명을 런타임에 추적할 수 없습니다.

만약 변수명으로 관리한다면:

javascript
// 가상의 시나리오 (실제로는 불가능)
{
  'name': '철수',
  'age': 25,
  'city': '서울'
}

문제점:

  • 구조분해할당 후 변수명 정보 손실
  • 압축(minify) 후 변수명 변경
  • 같은 이름의 변수 충돌 가능
javascript
// 압축 전
const [userName] = useState('철수');

// 압축 후
const[a]=useState('철수');  // userName → a

// 배포 후 동작이 달라질 수 있음!

2. 성능 문제

객체 키 방식 (느림):

javascript
{
  'name': '철수',
  'age': 25,
  'city': '서울'
}
// 키로 검색: O(1) but 해시 연산 필요

배열 인덱스 방식 (빠름):

javascript
[
  '철수',  // index 0 - 직접 접근
  25,      // index 1
  '서울'   // index 2
]
// 인덱스 접근: O(1) and 해시 불필요
  • 배열 인덱스 접근이 객체 키 접근보다 빠름
  • 메모리도 적게 사용
  • CPU 캐시 친화적

3. 단순함과 예측 가능성

Hook 순서 규칙은:

  • 구현이 단순합니다
  • 예측 가능합니다
  • 버그를 줄입니다
javascript
// 간단한 카운터
let hookIndex = 0;

function useState(init) {
  const index = hookIndex++;
  return hooks[index] || (hooks[index] = init);
}

function render() {
  hookIndex = 0;  // 리셋
  MyComponent();
}

불과 몇 줄로 구현 가능합니다!

다른 방식은 왜 안 될까?

시도 1: 객체 키로 관리?

javascript
const [name] = useState('name', '철수');  // 키를 명시

문제:

  • 모든 Hook에 고유 키를 수동으로 지정해야 함 (번거로움)
  • 키 충돌 가능성
  • 개발자 실수 가능성 높음
javascript
const [name1] = useState('name', '철수');
const [name2] = useState('name', '영희');  // 같은 키! 충돌!

시도 2: 변수명 자동 추출?

javascript
const [name] = useState('철수');
// React가 'name'을 자동으로 알아내면?

불가능한 이유:

javascript
// 이렇게 쓰면?
const myState = useState('철수');
const [a, b] = myState;

// 'a'의 이름을 어떻게 알아낼까?

시도 3: Hook을 객체로 선언?

Vue나 Svelte의 방식:

javascript
// Vue 스타일 (가상)
export default {
  data() {
    return {
      name: '철수',
      age: 25
    }
  }
}

React가 이 방식을 안 쓴 이유:

  1. Hooks 이전에는 class 컴포넌트였습니다
  2. Hooks는 기존 함수 컴포넌트에 상태를 추가하기 위해 만들어졌습니다
  3. 함수형 프로그래밍 철학을 유지하고 싶었습니다
javascript
// 함수형 - 자유롭고 유연
function MyComponent() {
  const [a] = useState(1);
  const [b] = useState(2);
  const c = a + b;  // 자유롭게 조합 가능
  return <div>{c}</div>;
}

// 객체 기반 - 구조가 정해짐
export default {
  data: { a: 1, b: 2 },  // 정해진 형식
  computed: { c() { return this.a + this.b } }
}

트레이드오프

React의 Hook 순서 규칙은 의도적인 설계 선택입니다:

장점 ✅

  • 단순한 구현 (코드가 간결)
  • 빠른 성능 (배열 인덱스 접근)
  • 예측 가능 (같은 순서면 항상 같은 결과)
  • JavaScript 기본 문법만 사용 (특별한 문법 없음)

단점 ❌

  • 순서를 지켜야 함 (조건부/반복문 안에서 사용 불가)
  • 초보자에게 혼란스러울 수 있음

React 팀은 단순함과 성능을 선택했고, 대신 개발자가 순서 규칙을 지키도록 했습니다.

그래서 ESLint 플러그인(eslint-plugin-react-hooks)을 제공해서 자동으로 규칙 위반을 잡아줍니다:

javascript
// ESLint가 자동으로 에러 표시
if (condition) {
  const [state] = useState();  // ❌ ESLint 경고!
}

useState 동작 원리

useState는 단순한 변수 할당이 아닙니다

일반 변수 vs useState

javascript
// ❌ 일반 변수 - 작동하지 않음
function BadComponent() {
  let count = 0;  // 매 렌더링마다 0으로 초기화됨

  const increment = () => {
    count = count + 1;
    console.log(count);  // 1 출력됨
  };

  return <button onClick={increment}>{count}</button>;
  // 화면에는 항상 0! 리렌더링이 안됨!
}
javascript
// ✅ useState - 정상 작동
function GoodComponent() {
  const [count, setCount] = useState(0);  // React가 관리

  const increment = () => {
    setCount(count + 1);  // React에게 "값 바꾸고 다시 그려줘!"
  };

  return <button onClick={increment}>{count}</button>;
  // 1, 2, 3... 제대로 증가!
}

useState가 실제로 하는 일

1. 상태를 React 외부에 저장

javascript
// React 내부 (개념적)
const componentStates = new Map();  // React가 관리하는 저장소

function MyComponent() {
  const [count, setCount] = useState(0);
  // count는 컴포넌트 안에 있지만
  // 실제 값은 React 외부(componentStates)에 저장됨!
}

왜 외부에 저장?

함수 컴포넌트는 매번 새로 실행됩니다:

javascript
// 첫 번째 렌더링
function MyComponent() {
  let count = 0;  // 생성
  return <div>{count}</div>;
}  // count는 여기서 사라짐!

// 두 번째 렌더링
function MyComponent() {
  let count = 0;  // 다시 생성 (완전히 새로운 변수)
  return <div>{count}</div>;
}  // 또 사라짐!

함수 내부 변수는 실행이 끝나면 사라지므로, React가 값을 보관해줘야 다음 렌더링에서도 사용 가능합니다.

2. 값이 바뀌면 리렌더링 트리거

javascript
const [count, setCount] = useState(0);

setCount(5);  // 이 한 줄이 하는 일:
// 1. React 저장소에 5 저장
// 2. "이 컴포넌트 다시 그려!" 스케줄링
// 3. 렌더 큐에 추가
// 4. 브라우저가 다음 프레임에 리렌더링

일반 변수는 값을 바꿔도 화면이 안 바뀝니다:

javascript
let count = 0;
count = 5;  // 값만 바뀜, 화면은 그대로!

3. 클로저로 상태 연결

javascript
function useState(initialValue) {
  // 현재 컴포넌트의 Hook 인덱스
  const currentIndex = hookIndex++;

  // 이전 값이 있으면 사용, 없으면 초기값
  if (hooks[currentIndex] === undefined) {
    hooks[currentIndex] = initialValue;
  }

  // setState는 클로저로 currentIndex를 '기억'
  const setState = (newValue) => {
    hooks[currentIndex] = newValue;  // 클로저!
    scheduleRerender();  // 리렌더링 예약
  };

  return [hooks[currentIndex], setState];
}

실제 동작 과정

javascript
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

첫 렌더링

text
1. Counter() 함수 실행
2. useState(0) 호출
   → hookIndex = 0
   → hooks[0] = 0 (초기값 저장)
   → [0, setState함수] 반환
3. count = 0, setCount = setState함수
4. <button>0</button> 반환
5. 화면에 "0" 표시

버튼 클릭

text
1. setCount(count + 1) 실행
   → setCount(0 + 1) → setCount(1)
2. setState 내부:
   hooks[0] = 1  (값 업데이트)
   scheduleRerender()  (리렌더링 예약)
3. React: "Counter 컴포넌트 다시 그려야겠다"

두 번째 렌더링

text
1. Counter() 함수 다시 실행
2. useState(0) 호출
   → hookIndex = 0 (다시 0부터 시작!)
   → hooks[0]이 이미 있음 (값: 1)
   → 초기값 무시, 저장된 값 사용
   → [1, setState함수] 반환
3. count = 1, setCount = setState함수
4. <button>1</button> 반환
5. 화면에 "1" 표시

useState vs 일반 객체

일반 객체로 상태 관리하면?

javascript
// 전역 객체
const state = { count: 0 };

function BadCounter() {
  return (
    <button onClick={() => {
      state.count++;  // 값은 바뀜
      console.log(state.count);  // 콘솔엔 출력됨
    }}>
      {state.count}
    </button>
  );
}
// 문제: 화면이 안 바뀜! React가 변경을 몰라요

왜 안 될까?

  • React는 state.count가 바뀐 걸 모릅니다
  • React는 setState를 호출해야만 변경을 감지합니다

useState의 진짜 구조

javascript
// React 내부 (단순화)
const React = (function() {
  let hooks = [];
  let currentHook = 0;

  return {
    render(Component) {
      const App = Component();
      App.render();
      currentHook = 0;  // 다음 렌더링을 위해 리셋
      return App;
    },

    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue;
      const setStateHookIndex = currentHook;  // 클로저로 캡처!

      const setState = (newState) => {
        hooks[setStateHookIndex] = newState;
        render();  // 리렌더링!
      };

      return [hooks[currentHook++], setState];
    }
  };
})();

핵심 정리

useState는:

  1. 값을 React 외부에 저장 (함수가 끝나도 사라지지 않게)
  2. 순서(인덱스)로 상태 추적 (변수명 필요 없음)
  3. 클로저로 setState와 인덱스 연결 (어떤 상태를 바꿀지 기억)
  4. setState 호출 시 리렌더링 트리거 (화면 업데이트)
  5. 배치 업데이트 최적화 (여러 setState를 한 번에 처리)
javascript
const [count, setCount] = useState(0);

이 한 줄은:

  • "React야, 내 상태 0 보관해줘"
  • "다음 렌더링 때도 이 값 유지해줘"
  • "setCount 부르면 리렌더링해줘"
  • "그리고 항상 같은 순서로 불러줘"

라는 의미입니다!


컴포넌트별 Hook 관리

React는 컴포넌트별로 Hook을 관리합니다

저장 구조

javascript
// React 내부 (개념적)
{
  컴포넌트A_인스턴스1: [Hook0, Hook1, Hook2],
  컴포넌트A_인스턴스2: [Hook0, Hook1, Hook2],
  컴포넌트B_인스턴스1: [Hook0, Hook1],
  컴포넌트C_인스턴스1: [Hook0, Hook1, Hook2, Hook3]
}

각 컴포넌트 인스턴스마다 독립적인 Hook 배열을 가집니다.

같은 컴포넌트를 여러 번 사용

javascript
function Counter() {
  const [count, setCount] = useState(0);  // index 0
  const [name, setName] = useState('');   // index 1

  return <div>{count}</div>;
}

function App() {
  return (
    <div>
      <Counter />  {/* 첫 번째 인스턴스 */}
      <Counter />  {/* 두 번째 인스턴스 */}
      <Counter />  {/* 세 번째 인스턴스 */}
    </div>
  );
}

React 내부 저장소:

javascript
{
  Counter_instance_1: [
    0,   // count (index 0)
    ''   // name (index 1)
  ],
  Counter_instance_2: [
    0,   // count (index 0)
    ''   // name (index 1)
  ],
  Counter_instance_3: [
    0,   // count (index 0)
    ''   // name (index 1)
  ]
}

<Counter />독립적인 상태를 가집니다:

javascript
// 첫 번째 Counter의 버튼 클릭
→ Counter_instance_1: [5, '']

// 다른 Counter들은 영향 없음
Counter_instance_2: [0, '']  // 그대로
Counter_instance_3: [0, '']  // 그대로

실제 구조: Fiber Tree

React는 Fiber라는 구조로 컴포넌트를 관리합니다:

javascript
// 실제 React 내부 (단순화)
{
  tag: FunctionComponent,
  type: Counter,
  key: null,

  // 이 컴포넌트의 Hook들!
  memoizedState: {
    // 첫 번째 Hook (useState)
    memoizedState: 0,
    next: {
      // 두 번째 Hook (useState)
      memoizedState: '',
      next: {
        // 세 번째 Hook (useEffect)
        memoizedState: { /* effect 정보 */ },
        next: null
      }
    }
  }
}

Fiber 노드마다:

  • memoizedState: Hook들의 연결 리스트
  • 각 Hook은 next로 다음 Hook을 가리킴

컴포넌트 인스턴스 구분

1. 위치로 구분

javascript
function App() {
  return (
    <div>
      <Counter />  {/* 위치 1 */}
      <Counter />  {/* 위치 2 */}
    </div>
  );
}

React는 트리 위치로 인스턴스를 구분:

text
App
├─ div
   ├─ Counter (첫 번째 자식) → 인스턴스 1
   └─ Counter (두 번째 자식) → 인스턴스 2

2. key로 구분

javascript
function App() {
  return (
    <div>
      <Counter key="a" />  {/* key로 구분 */}
      <Counter key="b" />
      <Counter key="c" />
    </div>
  );
}

key가 있으면:

  • 위치가 바뀌어도 같은 인스턴스로 인식
  • 상태가 유지됨
javascript
// 순서 바뀜
<Counter key="b" />  {/* 상태 유지! */}
<Counter key="a" />  {/* 상태 유지! */}
<Counter key="c" />  {/* 상태 유지! */}

key가 없으면:

  • 위치로만 구분
  • 순서 바뀌면 상태가 섞임!

컴포넌트 스코프별 인덱스

중첩 컴포넌트 예시

javascript
function Parent() {
  const [parentCount, setParentCount] = useState(0);     // Parent의 index 0
  const [parentName, setParentName] = useState('');      // Parent의 index 1

  return (
    <div>
      <Child />
    </div>
  );
}

function Child() {
  const [childCount, setChildCount] = useState(0);       // Child의 index 0
  const [childAge, setChildAge] = useState(0);           // Child의 index 1
  const [childCity, setChildCity] = useState('');        // Child의 index 2

  return <div>{childCount}</div>;
}

React 내부:

javascript
{
  Parent_instance_1: {
    hooks: [
      0,   // parentCount (index 0)
      ''   // parentName (index 1)
    ]
  },
  Child_instance_1: {
    hooks: [
      0,   // childCount (index 0) ← Parent와 독립적!
      0,   // childAge (index 1)
      ''   // childCity (index 2)
    ]
  }
}
  • Parent의 index 0 ≠ Child의 index 0
  • 완전히 별개입니다!

렌더링 시 동작 과정

javascript
function App() {
  return (
    <>
      <CounterA />
      <CounterB />
    </>
  );
}

렌더링 순서:

javascript
// 1. CounterA 렌더링 시작
React.currentFiber = CounterA_fiber;  // 현재 컴포넌트 설정
hookIndex = 0;  // 인덱스 초기화

CounterA() {
  useState(0);  // CounterA_fiber.hooks[0]
  useState(''); // CounterA_fiber.hooks[1]
}

// 2. CounterB 렌더링 시작
React.currentFiber = CounterB_fiber;  // 컴포넌트 전환!
hookIndex = 0;  // 인덱스 다시 초기화!

CounterB() {
  useState(0);  // CounterB_fiber.hooks[0] ← 다른 배열!
  useState(''); // CounterB_fiber.hooks[1]
}

핵심:

  • currentFiber로 "지금 어떤 컴포넌트 렌더링 중인지" 추적
  • 각 컴포넌트마다 hookIndex를 0부터 다시 시작
  • Hook 호출 시 currentFiber.hooks[hookIndex] 사용

실제 React 코드 (단순화)

javascript
// React 내부
let currentlyRenderingFiber = null;  // 현재 렌더링 중인 컴포넌트
let currentHook = null;              // 현재 Hook
let workInProgressHook = null;       // 작업 중인 Hook

function renderWithHooks(fiber, Component) {
  currentlyRenderingFiber = fiber;   // 이 컴포넌트의 Fiber 설정
  fiber.memoizedState = null;        // Hook 리스트 초기화

  // 컴포넌트 실행 (Hook 호출됨)
  const children = Component(fiber.props);

  currentlyRenderingFiber = null;    // 정리
  return children;
}

function useState(initialState) {
  const fiber = currentlyRenderingFiber;  // 현재 컴포넌트

  // 이 컴포넌트의 Hook 리스트에서 다음 Hook 가져오기
  const hook = {
    memoizedState: initialState,
    next: null
  };

  // Fiber의 Hook 연결 리스트에 추가
  if (fiber.memoizedState === null) {
    fiber.memoizedState = hook;  // 첫 번째 Hook
  } else {
    // 마지막 Hook에 연결
    let lastHook = fiber.memoizedState;
    while (lastHook.next !== null) {
      lastHook = lastHook.next;
    }
    lastHook.next = hook;
  }

  return [hook.memoizedState, setState];
}

정리

컴포넌트별 Hook 관리:

  1. 컴포넌트별로 Hook을 관리합니다
  2. 인스턴스별로 독립적인 Hook 배열을 가집니다
  3. 인덱스로 Hook을 구분합니다
  4. 스코프별로 관리됩니다

구조:

text
React 내부
└─ Fiber Tree
   ├─ ComponentA_instance1
   │  └─ hooks: [hook0, hook1, hook2]
   ├─ ComponentA_instance2
   │  └─ hooks: [hook0, hook1, hook2]  (독립적!)
   └─ ComponentB_instance1
      └─ hooks: [hook0, hook1]

각 컴포넌트 인스턴스는:

  • 자신만의 Hook 배열 보유
  • 인덱스는 0부터 시작
  • 다른 인스턴스와 완전히 독립적

결론

React Hooks는 단순하면서도 강력한 시스템입니다:

핵심 원칙

  1. Hook은 항상 같은 순서로 호출되어야 합니다
  2. 조건문이나 반복문 안에서 Hook을 호출하면 안 됩니다
  3. 컴포넌트 최상위 레벨에서만 Hook을 호출해야 합니다

설계 철학

  • 단순한 구현: 배열과 인덱스로 간단하게 관리
  • 빠른 성능: 배열 인덱스 접근의 효율성
  • 예측 가능성: 같은 순서면 항상 같은 결과
  • 함수형 프로그래밍: JavaScript 기본 문법만 사용

개발자 도구

ESLint 플러그인을 활용하면 Hook 규칙 위반을 자동으로 감지할 수 있습니다:

bash
npm install eslint-plugin-react-hooks --save-dev
json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}
댓글을 불러오는 중...