대수적 효과라는 멘탈 모델은 댄에게 새로운 길을 열어주었다. 그는 더 이상 어설픈 전역 변수에 의존할 필요가 없었다. 해답의 열쇠는 ‘호출 순서’에 있었다.
그는 다시 한번 리액트 코어의 동작을 흉내 내는 실험 환경을 구축했다. 이번에는 개념을 좀 더 명확히 하기 위해 useState
대신 createState
라는 이름으로 첫 실험을 시작했다.
그의 머릿속에 그려진 새로운 리액트의 내부 구조는 다음과 같았다.
- 리액트는 현재 렌더링 중인 컴포넌트가 무엇인지 알고 있다.
- 각 컴포넌트는 자신만의 ‘상태 저장 배열’을 가진다.
- 또한, 각 컴포넌트는 다음
createState
가 몇 번째 호출인지를 기억하는 ‘인덱스 카운터’를 가진다.
댄은 이 가설을 바탕으로 코드를 재구성했다.
// --- 가상의 React Core ---
let currentlyRenderingComponent = null; // 현재 렌더링 중인 컴포넌트를 가리키는 포인터
const componentState = new Map(); // 각 컴포넌트의 상태를 저장하는 곳
// 컴포넌트를 렌더링하는 가상의 함수
function render(Component) {
currentlyRenderingComponent = Component; // 렌더링 시작을 알림
componentState.get(Component).hookIndex = 0; // 인덱스 카운터를 0으로 리셋
const componentInstance = Component();
// ... DOM에 렌더링하는 로직 ...
}
function createState(initialValue) {
const componentHooks = componentState.get(currentlyRenderingComponent);
const currentIndex = componentHooks.hookIndex;
// 이전에 저장된 상태가 있는지 확인한다.
if (componentHooks.state[currentIndex] === undefined) {
// 없다면, 초기값으로 새로 저장한다.
componentHooks.state[currentIndex] = initialValue;
}
const setState = (newValue) => {
componentHooks.state[currentIndex] = newValue;
render(currentlyRenderingComponent); // 상태 변경 후 리렌더링
};
// 다음 호출을 위해 인덱스를 증가시킨다.
componentHooks.hookIndex++;
return [componentHooks.state[currentIndex], setState];
}
이전 프로토타입보다 훨씬 정교해졌다.
렌더링이 시작될 때마다 hookIndex
를 0으로 초기화하는 것이 핵심이었다. 덕분에 컴포넌트가 리렌더링 될 때도, createState
는 항상 0번 인덱스부터 차례대로 상태 값을 읽어올 수 있었다.
이제 이 실험적인 코어가 하나의 컴포넌트 안에서 여러 상태를 제대로 관리할 수 있는지 시험해 볼 차례였다. 그는 UserProfile
이라는 새로운 함수형 컴포넌트를 만들었다.
function UserProfile() {
const [name, setName] = createState("Dan");
const [age, setAge] = createState(30);
return (
<div>
<p>Name: {name}</p>
<input onChange={e => setName(e.target.value)} value={name} />
<p>Age: {age}</p>
<button onClick={() => setAge(age + 1)}>Increase Age</button>
</div>
);
}
// 최초 실행을 위한 준비
componentState.set(UserProfile, { state: [], hookIndex: 0 });
render(UserProfile);
첫 번째 createState("Dan")
호출.
리액트는 현재 렌더링 중인 UserProfile
의 상태 배열 0번 인덱스에 "Dan"을 저장한다. 그리고 인덱스 카운터를 1로 증가시킨다.
두 번째 createState(30)
호출.
리액트는 상태 배열 1번 인덱스에 30을 저장한다. 인덱스 카운터는 2가 된다.
사용자가 Increase Age
버튼을 클릭하면, 두 번째 createState
가 반환했던 setAge
함수가 호출된다. 리액트는 UserProfile
의 상태 배열 1번 인덱스 값을 31로 업데이트하고, UserProfile
컴포넌트를 리렌더링한다.
리렌더링이 시작되면, hookIndex
는 다시 0으로 초기화된다.
첫 번째 createState
가 호출되면, 리액트는 상태 배열 0번 인덱스에서 "Dan"을 꺼내 반환한다.
두 번째 createState
가 호출되면, 리액트는 상태 배열 1번 인덱스에서 방금 업데이트된 31을 꺼내 반환한다.
화면에 Age: 31
이 표시되었다.
성공이었다.
댄은 희미한 미소를 지었다.
이 투박하고 허점 많은 프로토타입은, 클래스 없이도 함수가 여러 개의 상태를 가질 수 있다는 가설을 완벽하게 증명해냈다.
하지만 그는 동시에 이 방식이 가진 치명적인 약점도 깨닫고 있었다.
이 모든 마법은 단 하나의 ‘약속’ 위에 서 있었다.
‘컴포넌트가 렌더링될 때마다, createState
의 호출 순서는 반드시 동일해야 한다.’
만약 조건문 안에서 createState
를 호출한다면 어떻게 될까?
그 약속이 깨지는 순간, 이 모든 시스템은 와르르 무너져 내릴 터였다.
그가 가능성의 문을 열자마자, 그 문 바로 뒤에서 새로운 문제가 그를 기다리고 있었다. 그리고 그 문제를 가장 먼저, 가장 날카롭게 지적할 사람은 정해져 있었다.