장애물과 돌파구
제8화
발행일: 2025년 05월 09일
Zustand의 기본적인 뼈대는 갖춰졌다. 외부 스토어, 상태 변경 함수, 그리고 useStore
훅. 하지만 가장 핵심적인 기능, 카토가 이 모든 여정을 시작하게 만든 바로 그 이유, ‘선택적 구독’의 효율적인 구현이라는 가장 높은 허들이 아직 남아 있었다.
‘이론은 완벽해. 하지만 이걸 어떻게 코드로 녹여내지?’
카토는 머리를 쥐어짰다. 문제는 디테일에 있었다.
각 useStore
훅은 자신만의 셀렉터 함수를 가지고 스토어를 구독한다. 스토어의 상태가 변경될 때마다, 각 구독자는 자신의 셀렉터로 새로운 상태 조각을 계산하고, 이전 값과 비교해서 다를 경우에만 리렌더링을 트리거해야 한다.
여기서 두 가지 난관이 발생했다.
첫째, 비교의 정확성. 단순히 ===
연산자로 비교하는 것은 원시 값(primitive value)에는 통하지만, 객체나 배열에는 통하지 않는다. 매번 새로운 객체나 배열을 반환하는 셀렉터라면, 내용물이 같아도 다르다고 판단하여 불필요한 렌더링을 유발할 수 있었다. 그렇다고 깊은 비교(deep comparison)를 매번 수행하는 것은 성능 저하를 일으킬 수 있었다.
둘째, 리스너 관리의 효율성. 수백, 수천 개의 컴포넌트가 각기 다른 셀렉터로 스토어를 구독한다고 상상해보자. 상태가 한 번 변경될 때마다 이 모든 셀렉터를 다시 실행하고 비교하는 것은 엄청난 부담이 될 수 있었다. 마치 수천 개의 눈이 하나의 변화를 동시에 감지하고 반응해야 하는 상황. 자칫 잘못하면 Context보다 더 비효율적인 시스템이 될 수도 있었다.
“크윽… 역시 쉽지 않군.”
카토는 몇 시간 동안 다양한 접근법을 시도했다. 메모이제이션 라이브러리를 도입해볼까? 아니면 복잡한 리스너 관리 로직을 직접 짜야 하나? 하지만 그는 고개를 저었다. Zustand의 핵심 철학은 ‘간결함’이었다. 외부 의존성을 늘리거나 내부 로직을 과도하게 복잡하게 만들고 싶지 않았다.
‘가장 기본적인 도구로… 자바스크립트의 본질적인 힘을 이용할 수는 없을까?’
그는 다시 원점으로 돌아가 자바스크립트의 기본 개념들을 되짚어보기 시작했다. 함수, 객체, 스코프… 그리고 그의 눈길이 한 곳에 머물렀다.
‘클로저(Closure).’
함수가 선언될 때의 렉시컬 환경(Lexical Environment)을 기억하는 것. 함수는 자신이 정의된 위치의 변수에 접근할 수 있다. 어쩌면… 이걸 활용할 수 있지 않을까?
순간, 카토의 머릿속에 새로운 회로가 연결되었다.
useStore
훅이 호출될 때, 그 훅의 스코프 안에 이전 상태 값(previous selected state)과 셀렉터 함수를 함께 기억하는 클로저를 만드는 것이다! 그리고 스토어의 subscribe
함수는 단순히 ‘상태가 바뀌었다’는 신호만 보내는 것이 아니라, 변경된 전체 상태(next state)를 리스너에게 전달한다.
각 리스너(클로저)는 전달받은 전체 상태를 자신의 셀렉터로 처리하여 다음 상태 값(next selected state)을 계산한다. 그리고 자신이 기억하고 있던 이전 상태 값과 비교한다. 이 비교 작업은 각 리스너 내부에서 독립적으로 수행된다!
// 클로저를 활용한 리스너 아이디어 스케치
useEffect(() => {
let previousState = selector(store.getState()); // 훅 스코프에 이전 상태 기억!
const listener = (nextState) => {
// 리스너는 다음 상태를 받음
const nextSelectedState = selector(nextState); // 다음 상태 계산
if (previousState !== nextSelectedState) {
// 내부에서 비교!
previousState = nextSelectedState; // 이전 상태 갱신
forceUpdate(nextSelectedState); // 리렌더링 트리거!
}
};
const unsubscribe = store.subscribe(listener); // 리스너 등록
return unsubscribe;
}, [store, selector]); // 의존성 간소화 가능성
“이거다…!”
카토는 무릎을 탁 쳤다. 이 방식이라면 비교 로직은 각 구독자에게 분산되고, 스토어는 단순히 상태 변경 알림만 보내면 된다. 리스너 관리도 자바스크립트의 기본적인 Set
객체와 클로저를 활용하면 충분히 효율적으로 구현할 수 있었다. 외부 라이브러리 없이, 순수 자바스크립트의 힘만으로!
그는 다시 광적인 속도로 코드를 수정하기 시작했다. 클로저와 리스너 패턴을 정교하게 엮어, 선택적 구독 메커니즘을 완성해 나갔다. 몇 번의 시행착오 끝에, 마침내 그의 눈앞에 펼쳐진 것은 놀랍도록 간결하면서도 효율적으로 작동하는 코드였다.
테스트를 위해 복잡한 상태 구조와 여러 개의 컴포넌트를 만들었다. 그리고 상태의 특정 부분만 변경했을 때… React DevTools에는 정확히 해당 상태를 구독한 컴포넌트만 번쩍이는 것을 확인할 수 있었다! 불필요한 렌더링은 완벽하게 제어되고 있었다.
“해냈어… 해냈다고!”
카토는 자신도 모르게 주먹을 불끈 쥐었다. 기술적 난관을 돌파했을 때의 희열이 온몸으로 퍼져나갔다.
다음 날, 카토는 약간의 설렘과 긴장을 안고 동료 켄지에게 다가갔다.
“켄지 상, 잠시 시간 괜찮으신가요? 주말 동안 재미있는 걸 좀 만들어봤는데…”
켄지는 잠시 하던 일을 멈추고 카토의 모니터를 바라보았다. 카토는 Zustand의 기본 개념과 사용법, 그리고 Context API의 문제점을 어떻게 해결했는지 간략하게 설명하며 간단한 데모 코드를 보여주었다.
켄지는 처음에는 반신반의하는 표정이었지만, 카토의 설명을 듣고 코드를 살펴볼수록 그의 눈이 점점 커졌다. 특히 useStore(state => state.cart.totalCount)
와 같이 훅 안에서 필요한 상태만 직접 선택하는 부분에서 그는 잠시 말을 잃었다. 복잡한 설정이나 Provider 감싸기 없이, 단 한 줄의 훅 호출로 상태를 가져오고 자동으로 최적화된 구독까지 처리된다는 사실에 놀란 듯했다.
켄지는 잠시 코드를 더 훑어보더니, 이윽고 안경을 고쳐 쓰며 말했다.
“카토 상… 이거,”
그는 잠시 뜸을 들였다. 카토의 심장이 살짝 빠르게 뛰었다.
“믿을 수 없을 만큼 간단하군.”
그 한마디였다. 하지만 그 어떤 찬사보다 카토에게는 큰 의미로 다가왔다. 현실적이고 비판적인 시각을 가진 켄지로부터 받은 인정. 그것은 Zustand가 나아갈 방향이 틀리지 않았다는 강력한 증거였다.
장애물은 돌파구를 만났고, 아이디어는 코드로 증명되었다. Zustand는 이제 단순한 프로토타입을 넘어, 실질적인 가능성을 품은 존재로 거듭나고 있었다. 하지만 카토는 알고 있었다. 이것은 또 다른 시작일 뿐이라는 것을. 진정한 여정은 이제부터였다.