학습 목표:
React는 각 컴포넌트마다 Fiber라는 자바스크립트 객체를 생성합니다. Fiber는 DOM이 아닌 React 내부의 자료구조입니다.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
function App() {
return (
<>
<Counter /> {/* Fiber 객체 #1 */}
<Counter /> {/* Fiber 객체 #2 */}
</>
);
}
Fiber 구조:
App Fiber
├─ Counter Fiber #1
│ └─ memoizedState → Hook {value: 0, next: null}
│
└─ Counter Fiber #2
└─ memoizedState → Hook {value: 0, next: null}
각 Fiber 객체는 memoizedState라는 필드를 가지며, 이는 Hook 연결 리스트의 첫 번째 노드를 가리킵니다.
여러 개의 Hook을 사용하는 경우:
function Component() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState('React'); // Hook 2
const [age, setAge] = useState(25); // Hook 3
}
내부 구조:
Fiber.memoizedState → Hook1 {value: 0, next: Hook2}
↓
Hook2 {value: 'React', next: Hook3}
↓
Hook3 {value: 25, next: null}
이유:
React는 Hook의 이름이나 ID를 사용하지 않고, 오직 호출 순서에만 의존합니다.
왜 변수명으로 구분하지 않을까?
JavaScript는 런타임에 변수명 정보가 사라지기 때문입니다:
const [count, setCount] = useState(0);
// 컴파일 후 변수명은 없어짐
function Counter() {
if (Math.random() > 0.5) {
const [count, setCount] = useState(0); // 조건부 Hook!
}
const [name, setName] = useState('React');
return <div>{name}</div>;
}
문제 상황:
// 첫 번째 렌더링 (조건 true)
Hook 1: count (0)
Hook 2: name ('React')
// 두 번째 렌더링 (조건 false)
Hook 1: name ('React') // 어? 이전엔 count였는데!
React는 hookIndex를 0으로 리셋하고, useState가 호출될 때마다 증가시키면서 해당 인덱스의 값을 가져옵니다. 순서가 바뀌면 Hook 1번 위치에 저장된 값(0)을 name에게 줘버려서 타입도 꼬이고 값도 엉망이 됩니다.
React는 이 문제를 방지하기 위해 두 가지 규칙을 강제합니다:
최상위에서만 Hook 호출
React 함수에서만 Hook 호출
올바른 예:
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
if (count > 10) {
// 조건문은 Hook 호출 후에!
return <div>Too many!</div>;
}
return <div>{count} - {name}</div>;
}
잘못된 예:
function Counter() {
if (condition) {
const [count, setCount] = useState(0); // ❌ 조건부 Hook
}
for (let i = 0; i < 5; i++) {
const [value, setValue] = useState(i); // ❌ 반복문 안의 Hook
}
}
React는 이 규칙을 자동으로 검사하는 ESLint 플러그인을 제공합니다:
npm install eslint-plugin-react-hooks
const [count, setCount] = useState(0);
console.log(count); // 0
setCount(5);
console.log(count); // 여전히 0!
setCount(5)를 호출해도 즉시 변경되지 않습니다.
setState를 호출하면:
setState(5) → 업데이트 객체 생성 → Fiber 큐에 추가 → 스케줄러 → 렌더링
React는 여러 상태 업데이트를 모아서 한 번만 렌더링합니다.
function handleClick() {
setCount(count + 1); // 큐에 추가
setName('React'); // 큐에 추가
setAge(25); // 큐에 추가
// 이벤트 핸들러가 끝난 후 → 단 1번만 렌더링!
}
Batching의 이점:
// Batching이 없다면?
setFirstName('John'); // 렌더링 1: John Doe (깜빡)
setLastName('Smith'); // 렌더링 2: John Smith
// Batching이 있으면
setFirstName('John'); // 큐에 추가
setLastName('Smith'); // 큐에 추가
// → 1번만 렌더링: John Smith
React 17 이전:
React 18 이후:
// React 17: 2번 렌더링
setTimeout(() => {
setCount(1); // 렌더링 1
setName('A'); // 렌더링 2
}, 1000);
// React 18: 1번 렌더링
setTimeout(() => {
setCount(1); // 큐에 추가
setName('A'); // 큐에 추가
// → 1번만 렌더링
}, 1000);
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // setCount(0 + 1)
setCount(count + 1); // setCount(0 + 1)
setCount(count + 1); // setCount(0 + 1)
}
// 기대: count = 3
// 실제: count = 1
왜 1일까?
각 setCount 호출이 동일한 count 값(0)을 참조하기 때문입니다. 모두 "1로 교체"를 큐에 추가하게 됩니다.
const [count, setCount] = useState(0);
function handleClick() {
setCount(c => c + 1); // 0 + 1 = 1
setCount(c => c + 1); // 1 + 1 = 2
setCount(c => c + 1); // 2 + 1 = 3
}
// 결과: count = 3 ✅
동작 원리:
함수를 전달하면 React는 큐의 이전 결과를 다음 함수에 전달합니다.
큐: [c => c + 1, c => c + 1, c => c + 1]
처리:
1. 0 → (c => c + 1) → 1
2. 1 → (c => c + 1) → 2
3. 2 → (c => c + 1) → 3
최종 결과: 3
사용해야 하는 경우:
// ❌ 나쁜 예: count를 의존성에 추가해야 함
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
// ✅ 좋은 예: 의존성 배열이 비어있어도 됨
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
memoizedState (연결 리스트)setState는 즉시 반영되지 않음// Fiber 구조
Fiber {
memoizedState: Hook1 → Hook2 → Hook3 → null,
updateQueue: [update1, update2, ...],
...
}
// Hook 구조
Hook {
memoizedState: value,
next: nextHook
}
// 업데이트 흐름
setState → Update Queue → Scheduler → Batching → Reconciliation → Render
setState(prev => ...))