내부 검증: Zustand, Jotai와의 느낌 비교
제11화
발행일: 2025년 05월 31일
성능이라는 거대한 산맥을 넘고, 깊은 구조와 배열이라는 까다로운 계곡을 건넜다. Valtio의 핵심 기능들은 이제 꽤나 안정적인 궤도에 올라섰다. 다이시 카토는 잠시 숨을 돌리며, 자신의 세 번째 창조물을 만족스러운 눈길로 바라보았다. Proxy와 Snapshot의 조화는 이론적으로 완벽해 보였다.
하지만 그의 엔지니어로서의 본능은 여기서 멈추는 것을 허락하지 않았다. 코드가 '작동'하는 것과, 그것이 개발자에게 '좋은 느낌'을 주는 것은 전혀 다른 차원의 문제였다. 그는 다시 한번 스스로에게 엄격한 잣대를 들이댔다. 내부 검증. 자신의 이전 창조물들과의 직접적인 비교를 통해 Valtio만의 진정한 가치와 색깔을 확인해야 했다.
"같은 기능을… 세 가지 방식으로 구현해보자."
그는 결심했다. 아주 간단하지만, 상태 관리의 핵심적인 측면들을 건드릴 수 있는 기능을 선정했다. 사용자 프로필을 표시하고, 사용자가 자신의 선호하는 테마(예: 'light', 'dark')를 목록에서 추가하고 삭제할 수 있는 간단한 설정 화면. 이 기능을 Zustand, Jotai, 그리고 Valtio로 각각 밑바닥부터 구현해보며, 그 과정에서 느껴지는 미묘한 '결'의 차이를 온몸으로 흡수하기로 했다.
첫 번째, Zustand.
익숙한 손놀림이었다. 중앙 집중식 스토어를 정의하고, create
함수로 감쌌다. 사용자 정보와 테마 목록을 상태 객체 안에 넣었다. 테마를 추가하고 삭제하는 액션 함수를 정의했다. set
함수 안에서 스프레드 문법을 사용하거나 Immer를 활용하여 불변성을 유지하며 상태를 업데이트했다.
// Zustand 방식 (Immer 사용 예시)
addTheme: (theme) => set(produce((draft) => {
if (!draft.user.themes.includes(theme)) {
draft.user.themes.push(theme);
}
})),
removeTheme: (theme) => set(produce((draft) => {
draft.user.themes = draft.user.themes.filter(t => t !== theme);
})),
"역시… 예측 가능하고 안정적이야."
카토는 고개를 끄덕였다. 모든 로직이 스토어 안에 명확하게 정의되어 있었다. 상태 변경의 흐름을 추적하기 쉬웠다. 마치 잘 설계된 성채 안에서 작업하는 듯한 안정감. 하지만 동시에, 간단한 배열 조작조차도 set
함수나 produce
콜백 안에 감싸야 하는 약간의 번거로움도 여전히 느껴졌다.
두 번째, Jotai.
이번에는 사고방식을 바꿔야 했다. 상태를 원자(Atom) 단위로 분해했다. 사용자 정보를 담는 userAtom
, 선택된 테마 목록을 담는 themesAtom
, 그리고 이들을 조합하여 사용하는 컴포넌트들. 테마 추가/삭제 로직은 themesAtom
을 업데이트하는 함수로 구현했다.
// Jotai 방식
const themesAtom = atom(['light']);
const addThemeAtom = atom(null, (get, set, theme) => {
set(themesAtom, (prev) => (prev.includes(theme) ? prev : [...prev, theme]));
});
const removeThemeAtom = atom(null, (get, set, theme) => {
set(themesAtom, (prev) => prev.filter((t) => t !== theme));
});
"확실히… 더 세밀한 제어가 가능하군."
Jotai의 방식은 상태의 각 부분을 독립적으로 관리할 수 있게 해주었다. 불필요한 리렌더링을 최소화하고, 복잡한 상태 의존성을 명시적으로 표현하는 데 유리했다. 마치 정밀한 부품들을 조립하여 원하는 기계를 만드는 듯한 느낌. 하지만 동시에, 상태가 많아지고 복잡해질수록 관리해야 할 아톰의 수가 늘어나고, 아톰 간의 관계 설정에 좀 더 신경 써야 한다는 점도 인지했다.
마지막, Valtio.
심장이 미묘하게 빠르게 뛰었다. 그는 초기 상태 객체를 정의하고, proxy()
함수로 감쌌다.
// Valtio 방식
const state = proxy({
user: { name: '카토' },
themes: ['light'],
});
컴포넌트에서는 useSnapshot
으로 스냅샷을 받아 사용했다. 그리고 대망의 업데이트 로직. 그는 잠시 숨을 멈추고 키보드 위에 손가락을 올렸다.
// Valtio 업데이트 로직
const addTheme = (theme) => {
if (!state.themes.includes(theme)) {
state.themes.push(theme); // 그냥 push!
}
};
const removeTheme = (theme) => {
state.themes = state.themes.filter((t) => t !== theme); // 그냥 filter 후 할당!
};
"…!"
코드를 작성하는 순간, 카토는 자신도 모르게 짧은 탄성을 내뱉었다. 그 어떤 라이브러리보다도 압도적으로 간결했다. set
함수도, produce
콜백도, 아톰을 위한 별도의 업데이트 함수 정의도 필요 없었다. 그저 평범한 자바스크립트 배열 메서드 push
와 filter
를 사용했을 뿐이었다. 마치 맨손으로 직접 물건을 만지고 조작하는 듯한 직관적인 '느낌'. 불필요한 추상화 계층이 사라지고, 개발자의 의도가 코드에 그대로 반영되는 듯한 해방감이었다.
그는 세 개의 코드를 나란히 놓고 비교했다. 각기 다른 철학과 장단점이 명확하게 드러났다.
- Valtio: 상태 '업데이트' 경험은 가히 혁명적이었다. 압도적인 직관성과 간결함. 마치 아무런 도구 없이 맨손으로 작업하는 듯한 자유로움.
- Jotai: 상태 '구조'를 원자 단위로 세밀하게 설계하고 조합하는 데 강점이 있었다. 복잡하고 상호 의존적인 상태를 명시적으로 관리하고 최적화하기에 유리했다. 마치 정밀 공구를 사용하여 복잡한 기계를 조립하는 느낌.
- Zustand: 단순함과 예측 가능성이 돋보였다. 중앙 집중식 스토어는 이해하기 쉽고, 상태 관리의 기본에 충실했다. 마치 견고하게 지어진 표준 규격의 집과 같았다.
"이제야… 확실히 알겠다."
카토는 깊은 깨달음을 얻었다. Valtio는 Zustand나 Jotai를 대체하기 위한 것이 아니었다. 그들과 경쟁하는 것이 아니라, 상태 관리라는 넓은 스펙트럼 위에서 자신만의 독특한 위치를 차지하는 존재였다. 특히 '상태 업데이트 방식'에 대한 개발자의 고충을 정면으로 겨냥하고, Proxy라는 기술을 통해 가장 직관적인 해답을 제시하는 것. 그것이 바로 Valtio의 존재 이유였다.
그는 세 라이브러리의 명확한 개성과 적합한 사용처를 더욱 깊이 이해하게 되었다. 이제 남은 것은 이 세 번째 아이, Valtio를 세상에 내보내고 그 가치를 증명하는 일이었다. 하지만 그의 마음 한편에는 새로운 종류의 부담감이 스멀스멀 피어오르고 있었다. "왜 또 상태 관리 라이브러리인가?" 라는 세상의 질문에, 그는 어떻게 답해야 할까?