소피의 날카로운 지적은 팀을 다시 원점으로 되돌려 놓았다. 조건문이나 반복문 안에서 createState
를 호출하는 순간, 상태 배열의 인덱스는 걷잡을 수 없이 꼬여버렸다. 이 치명적인 약점을 해결하지 못한다면, 이 모든 아이디어는 한낱 실험으로 끝날 터였다.
회의실의 무거운 침묵 속에서, 팀원들은 온갖 대안을 쏟아내기 시작했다.
“createState
를 호출할 때, 개발자가 직접 고유한 키(key)를 넘겨주게 하면 어떨까요? createState('age', 30)
처럼요. 그러면 순서가 아니라 키를 기준으로 상태를 저장하고 찾을 수 있을 겁니다.”
그럴듯한 아이디어였다. 하지만 곧바로 반론이 제기되었다.
“개발자가 모든 상태마다 고유한 이름을 짓고 관리해야 한다는 건가요? 그것 자체가 또 다른 부담입니다. 이름이 충돌할 위험도 있고요.”
“그렇다면 리액트가 컴파일 시점에 코드 구조를 분석해서, 각 createState
호출에 자동으로 고유 ID를 부여하는 건 어떨까요?”
더 진보된 생각이었지만, 이 역시 현실적인 장벽에 부딪혔다.
“모든 개발자가 바벨(Babel) 같은 컴파일러를 쓴다고 보장할 수 없습니다. 리액트는 순수한 자바스크립트 환경에서도 동작해야 하죠. 특정 도구에 의존하는 기능은 피해야 합니다.”
토론은 몇 시간 동안 이어졌지만, 뾰족한 해답은 보이지 않았다. 모든 대안은 ‘호출 순서’라는 제약을 피하려다, 또 다른 종류의 복잡성이나 제약을 만들어냈다.
모두가 지쳐갈 무렵, 댄이 조용히 입을 열었다.
그는 마치 역발상처럼, 모두가 피하려고 했던 바로 그 지점을 다시 파고들었다.
“만약… 만약 우리가 이 문제를 기술로 푸는 게 아니라, ‘규칙’으로 풀면 어떨까요?”
모두의 시선이 그에게 향했다.
“소피가 지적한 문제는 ‘호출 순서가 렌더링마다 바뀔 수 있다’는 것입니다. 그렇다면, 우리가 개발자들에게 ‘호출 순서는 렌더링마다 절대 바뀌어서는 안 된다’는 규칙을 강제하는 겁니다.”
회의실에 미묘한 정적이 흘렀다.
한 개발자가 조심스럽게 반문했다.
“그게 가능한가요? 어떻게 개발자가 조건문 안에서 상태를 쓰고 싶은 유혹을 이겨내게 만들죠?”
“바로 그 지점입니다.”
댄의 목소리에 힘이 실렸다.
“‘컴포넌트의 최상위 레벨에서만 createState
를 호출해야 한다.’ 이것을 리액트의 제1규칙으로 만드는 겁니다. 조건문, 반복문, 혹은 중첩된 함수 안에서는 절대 호출할 수 없도록요.”
그의 주장은 대담했다. 기술적 한계를 인정하고, 대신 그 한계를 우회하는 강력한 ‘컨벤션(Convention)’을 만들자는 것이었다.
function UserProfile({ showAge }) {
// OK: 컴포넌트 최상위 레벨에서 호출
const [name, setName] = createState("Dan");
const [age, setAge] = createState(30);
if (!showAge) {
return <p>Name: {name}</p>; // 나이를 렌더링하지 않을 순 있지만,
}
// const [age, setAge] = createState(30); // ERROR: 여기서 호출하면 안 됨!
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
}
이 규칙을 따른다면, createState
의 호출 순서는 렌더링마다 항상 [name, age]
로 동일하게 유지된다. 조건부 렌더링은 상태 호출 이후의 로직에서 처리하면 되었다.
세바스티안의 얼굴에 희미한 미소가 번졌다.
“마치… 은행 창구와 같군요.”
그가 비유를 들어 설명했다.
“은행에 가서 번호표를 뽑는다고 상상해 보세요. 댄이 1번, 소피가 2번 표를 뽑았다면, 은행원은 1번 창구로 댄을, 2번 창구로 소피를 부를 겁니다. 다음 날 은행에 다시 가도, 두 사람이 똑같은 순서로 번호표를 뽑는 한, 은행원은 어제와 똑같은 순서로 두 사람을 부를 수 있죠. 리액트가 은행원이고, 우리가 상태입니다. 우리가 순서만 잘 지켜준다면, 은행원은 우리를 헷갈릴 일이 없는 겁니다.”
이 비유는 복잡했던 개념을 명쾌하게 만들었다.
‘호출 순서의 불변성’.
이것은 기술적 제약이 아니라, 개발자와 리액트 간의 신뢰에 기반한 ‘약속’이었다.
물론, 인간은 약속을 잊거나 어기기 마련이다.
“개발자들이 이 규칙을 어떻게 항상 기억하죠? 실수가 나올 수밖에 없을 텐데요.”
소피의 현실적인 우려에, 댄은 이미 생각해 둔 해답을 내놓았다.
“우리가 도구를 만들면 됩니다. 이 규칙을 어기는 코드가 작성되는 순간, 개발자의 코드 에디터에서 바로 경고를 띄워주는 린트(Lint) 플러그인을 만드는 겁니다.”
그 순간, 닫혔던 문이 다시 열리는 듯했다.
기술로 풀 수 없는 문제는 규칙으로 풀고, 인간의 실수는 도구로 막는다.
완벽한 아이디어의 연쇄였다.
팀은 마침내 나아갈 방향을 찾았다.
그들은 더 이상 ‘호출 순서’라는 제약을 피하지 않기로 했다. 오히려 그것을 새로운 패러다임의 핵심 원칙으로 받아들이고, 그 위에 새로운 세계를 건설하기로 결심했다.
이제 그들의 첫 번째 과제는, createState
라는 투박한 실험체에 useState
라는 공식적인 이름을 부여하고, 이 약속의 메커니즘을 리액트의 심장부에 정교하게 이식하는 일이었다.