“이름을 정해야겠습니다.”
회의실의 분위기는 이전과 사뭇 달랐다. 방향이 정해지자, 팀의 에너지는 논쟁이 아닌 창조를 향해 집중되고 있었다. 댄이 화이트보드에 createState
라는 글자를 지우며 말했다.
“createState
는 너무 길고, 동사로 시작하는 것이 어색합니다. 이 새로운 기능들은 함수형 컴포넌트에 ‘연결(hook into)’되어 상태나 생명주기 같은 리액트의 특성을 사용하는 방식이죠.”
그의 설명에 소피가 고개를 끄덕였다.
“‘Hook(훅, 갈고리)’이라는 단어가 본질을 잘 설명하는 것 같네요. 함수가 리액트의 내부에 갈고리를 걸어 무언가를 끌어오는 이미지니까요.”
몇 가지 이름이 오고 간 끝에, 마침내 결정이 내려졌다.
useState
.
상태(State)를 사용(use)한다. 간결하고, 직관적이며, 새로운 개념의 정체성을 명확히 드러내는 이름이었다. 훗날 이 use
라는 접두사는 새로운 기능군의 시작을 알리는 상징적인 표식이 될 터였다.
이름이 정해지자, 댄과 핵심 엔지니어들은 프로토타입을 실제 리액트 코드베이스에 이식하는 작업에 착수했다. 그것은 가상의 실험 환경에서 코드를 짜는 것과는 차원이 다른, 섬세하고 위험한 수술이었다.
그들은 리액트의 심장부인 ‘리콘실러(Reconciler)’—가상 DOM과 실제 DOM의 차이를 비교하고 업데이트를 관장하는 엔진—에 손을 대야 했다.
가장 큰 변화는 ‘파이버(Fiber)’ 객체의 구조를 수정하는 일이었다. 파이버는 리액트가 컴포넌트 하나하나를 표현하는 내부 데이터 구조였다. 이전까지 파이버 객체는 주로 클래스 컴포넌트의 인스턴스나 DOM 노드 정보를 담고 있었다.
이제, 함수형 컴포넌트를 위한 파이버 객체에는 새로운 속성이 추가되어야 했다.
memoizedState
.
이름 그대로 ‘메모리제이션된 상태’를 의미했다. 이 속성이 바로 각 컴포넌트의 상태 값들을 순서대로 저장할 연결 리스트(Linked List)의 시작점이 될 것이었다. 배열 대신 연결 리스트를 사용한 것은, 상태가 동적으로 추가되거나 할 때의 유연성을 고려한 설계였다.
렌더링 과정은 다음과 같이 재설계되었다.
- 리액트가 함수형 컴포넌트를 렌더링하기 시작하면, 해당 컴포넌트의 파이버 객체를 찾는다.
- 현재 처리할 훅(hook)을 가리키는 내부 포인터를 파이버의
memoizedState
리스트의 첫 번째 노드로 설정한다. - 개발자의 코드에서
useState
가 호출된다. - 리액트는 내부 포인터가 가리키는 훅 노드에서 상태 값을 꺼내 반환한다.
- 그리고 포인터를 다음 훅 노드로 이동시킨다.
- 다음
useState
가 호출되면, 4번과 5번 과정을 반복한다.
리렌더링이 끝나면, 각 훅 노드에는 최신 상태 값이 저장된 채로 다음 렌더링을 기다리게 된다.
이 메커니즘은 ‘호출 순서의 불변성’이라는 대전제 위에서 완벽하게 작동했다. 개발자가 규칙을 지키는 한, 리액트는 렌더링마다 정확한 순서로 상태 값을 제공할 수 있었다.
댄은 코드를 작성하며, 이 새로운 모델이 가진 우아함에 감탄했다. 클래스 컴포넌트에서 상태를 관리하기 위해 필요했던 그 모든 복잡한 코드—인스턴스 생성, this
바인딩, setState
의 비동기 처리—가 사라졌다. 대신, 함수형 컴포넌트와 파이버 객체 사이의 단순한 연결 리스트 포인터가 그 모든 것을 대체했다.
며칠간의 밤샘 작업 끝에, 마침내 useState
는 리액트의 실제 코드베이스 위에서 처음으로 안정적으로 숨을 쉬기 시작했다.
하나의 거대한 벽이 무너지고 있었다.
클래스라는 무거운 갑옷 없이도, 함수는 이제 당당히 자신만의 상태를 가질 수 있게 된 것이다.
하지만 이것은 시작에 불과했다. 상태 문제는 해결되었지만, 여전히 거대한 퍼즐 조각 하나가 남아있었다.
componentDidMount
나 componentWillUnmount
에서 처리하던, 서버 통신이나 이벤트 구독 같은 ‘부수 효과(Side Effects)’는 이제 어디서 처리해야 할까?
상태를 담을 그릇은 마련되었지만, 예측 불가능한 세상과 상호작용할 통로는 아직 열리지 않았다. 리액트 팀의 다음 목표는 명확했다. 두 번째 훅(Hook)을 설계하는 것이었다.