며칠 뒤, 댄은 자신의 실험적인 createState
프로토타입을 소피와 세바스티안에게 시연했다. 클래스 없이도 여러 상태를 관리하는 UserProfile
컴포넌트가 부드럽게 작동하는 모습을 보며, 회의실에는 조용한 흥분이 감돌았다.
세바스티안은 만족스러운 듯 고개를 끄덕였다. 대수적 효과의 멘탈 모델이 자바스크립트의 세계에서 구현될 수 있다는 가능성을 댄이 증명해 보인 것이다.
하지만 소피의 표정은 달랐다. 그녀는 흥분보다는 냉철한 분석의 눈으로 코드를 뜯어보고 있었다. 그녀의 시선은 언제나 그렇듯, 이 새로운 기술이 실제 개발 현장에서 마주할 현실적인 문제들을 향해 있었다.
시연이 끝나자, 소피가 조용히 질문을 던졌다.
“훌륭한 진전이네요, 댄. 그런데 만약 코드가 이렇다면 어떻게 되죠?”
그녀는 자리에서 일어나, 댄의 UserProfile
코드를 살짝 수정했다.
function UserProfile() {
const [name, setName] = createState('Dan');
// 만약 이름이 "Dan"일 때만, 나이 상태를 보여주고 싶다면?
if (name === 'Dan') {
const [age, setAge] = createState(30);
// ... age를 사용하는 UI ...
}
// ...
}
순간, 회의실의 공기가 얼어붙었다.
댄의 머릿속이 하얘졌다. 소피는 정확하게, 그가 애써 외면하고 있던 아킬레스건을 건드렸다.
상황을 시뮬레이션해보자.
첫 번째 렌더링:
name
의 초기값은 "Dan"이다.createState("Dan")
이 호출된다. 상태 배열 0번 인덱스에 "Dan"이 저장된다.hookIndex
는 1이 된다.if (name === "Dan")
조건문은 참(true)이 된다.createState(30)
이 호출된다. 상태 배열 1번 인덱스에 30이 저장된다.hookIndex
는 2가 된다.state: ["Dan", 30]
모든 것이 정상적으로 작동한다.
이제 사용자가 입력 필드에 "Sophie"라고 입력했다고 가정해보자. setName("Sophie")
가 호출되고, 리렌더링이 발생한다.
두 번째 렌더링:
- 리렌더링이 시작되며
hookIndex
는 0으로 리셋된다. createState
가 첫 번째로 호출된다. 리액트는 상태 배열 0번 인덱스에서 "Sophie"를 꺼내 반환한다.hookIndex
는 1이 된다.if (name === "Dan")
조건문은 이제 거짓(false)이 된다.if
블록 안의createState(30)
은 호출되지 않는다.- 렌더링이 끝난다.
이제 진짜 문제가 발생한다.
사용자가 다시 이름 입력 필드의 "Sophie"를 지우고, "Dan"이라고 입력했다. setName("Dan")
이 호출되고, 세 번째 리렌더링이 일어난다.
세 번째 렌더링:
hookIndex
는 다시 0으로 리셋된다.createState
가 첫 번째로 호출된다. 상태 배열 0번 인덱스에서 "Dan"을 꺼내 반환한다.hookIndex
는 1이 된다.if (name === "Dan")
조건문은 다시 참(true)이 된다.if
블록 안의createState
가 두 번째로 호출된다. 리액트는 상태 배열의 1번 인덱스에 접근한다.- 그런데 1번 인덱스에는 무엇이 저장되어 있는가? 두 번째 렌더링 때 건너뛰었으므로, 첫 번째 렌더링 때 저장했던 30이 그대로 남아있다.
- 리액트는
age
의 현재 상태가 30이라고 착각하고,[30, setAge]
를 반환한다.
지금까지는 괜찮아 보인다. 하지만 만약, 소피가 코드를 이렇게 바꿨다면?
function UserProfile() {
const [name, setName] = createState('Dan');
const [lastName, setLastName] = createState('Abramov'); // 새로운 상태 추가
if (name === 'Dan') {
const [age, setAge] = createState(30);
}
}
이 경우, 두 번째 렌더링에서 age
상태가 건너뛰어지면서, 세 번째 렌더링 때 createState(30)
은 lastName
의 상태를 age
의 상태로 착각하게 될 것이다. 상태 배열의 인덱스가 하나씩 밀려버리는, 대재앙이 발생하는 것이다.
댄은 아무 말도 할 수 없었다.
소피의 반론은 완벽했다. ‘호출 순서의 불변성’이라는 약속은, 조건문이나 반복문, 심지어는 조기 반환(early return) 앞에서는 너무나도 쉽게 깨져버렸다. 개발자들에게 “절대로 조건문 안에서 createState
를 쓰지 마세요”라고 요구하는 것은 비현실적이었다.
“이건… 동작하지 않겠군요.”
댄이 마침내 패배를 인정했다.
가능성의 문이 열리는가 싶더니, 눈앞에서 다시 굳게 닫혀버린 기분이었다.
세바스티안마저 심각한 표정으로 턱을 괴고 생각에 잠겼다.
과연 이 ‘호출 순서’라는 위태로운 외줄을, 개발자들이 떨어지지 않고 안전하게 건너게 할 방법은 없는 것일까? 아니면, 외줄이 아닌 단단한 다리를 놓을 다른 방법이 있는 것일까?
회의실의 시계 소리만이 유난히 크게 들렸다.
그들이 마주한 기술적 난관은 생각보다 훨씬 더 깊고, 근본적이었다.