내부 검증, Zustand와의 비교
제11화
발행일: 2025년 05월 21일
Jotai의 핵심 엔진은 이제 제법 그럴듯하게 작동하고 있었다. 독립적인 원자(Atom), 함수로서의 파생 상태(Derived Atom), 자동 의존성 추적, 영리한 메모리 관리, 그리고 비동기 처리의 가능성까지. 다이시 카토는 자신이 쌓아 올린 코드의 탑을 바라보며 희미한 만족감을 느꼈다. 이론은 현실이 되었고, 가능성은 코드로 증명되었다.
하지만 그의 마음 한편에는 여전히 불안감의 그림자가 드리워져 있었다. 마치 새로 개발한 강력한 무기를 손에 쥐었지만, 아직 실전에서 그 위력을 제대로 시험해보지 못한 장수가 된 느낌이었다.
‘정말… 쓸만한 물건인가?’
Jotai는 분명 Zustand와는 달랐다. 더 세분화되고, 더 분산적이며, 어쩌면 더 유연할지도 몰랐다. 하지만 ‘다르다’는 것이 항상 ‘더 좋다’는 의미는 아니었다. 특정 상황에서는 오히려 Zustand의 단순함과 중앙 집중적인 관리 방식이 더 효율적일 수도 있었다.
그는 스스로에게 냉정한 질문을 던져야 했다. Jotai는 정말로 Zustand가 해결하지 못하는 문제를 해결하는가? 아니면 그저 또 다른 복잡성을 야기하는 것은 아닌가? 세상에 내놓기 전에, 창조주인 그 자신부터 이 질문에 대한 명확한 답을 가져야 했다.
“직접 부딪혀봐야 알겠지.”
그는 결심했다. 자신의 두 창조물, Zustand와 Jotai를 정면으로 비교 분석해보기로. 두 명의 뛰어난 검투사를 같은 경기장에 세워 그들의 장단점을 직접 겨뤄보는 것이다.
테스트 베드는 신중하게 선택해야 했다. 너무 단순하면 차이가 드러나지 않을 것이고, 너무 복잡하면 비교 자체가 어려워질 터였다. 그는 적당한 복잡성을 가진 미니 프로젝트를 구상했다. 예를 들어, 실시간으로 업데이트되는 사용자 활동 피드, 각 피드 아이템에 대한 ‘좋아요’ 기능, 그리고 사용자의 필터 설정에 따라 피드 목록이 동적으로 변경되는 기능. 이 정도면 상태 간의 의존성, 비동기 처리, 파생 상태 계산 등 다양한 시나리오를 포함할 수 있었다.
먼저, 그는 익숙한 도구인 Zustand로 이 기능을 구현하기 시작했다.
// Zustand 방식 (개념적 스케치)
const useFeedStore = create(
devtools(
persist((set, get) => ({
feeds: [],
filter: 'all',
isLoading: false,
error: null,
likedFeedIds: new Set(),
fetchFeeds: async () => {
/* ... API 호출 및 feeds, isLoading, error 상태 업데이트 ... */
},
setFilter: (newFilter) => set({ filter: newFilter }),
toggleLike: (feedId) =>
set((state) => {
const newLikedIds = new Set(state.likedFeedIds);
if (newLikedIds.has(feedId)) {
newLikedIds.delete(feedId);
} else {
newLikedIds.add(feedId);
}
return { likedFeedIds: newLikedIds };
}),
// 파생 상태: 필터링된 피드 목록 (셀렉터에서 계산)
}))
)
);
// 컴포넌트에서 사용
const feeds = useFeedStore((state) => state.feeds);
const filter = useFeedStore((state) => state.filter);
const likedFeedIds = useFeedStore((state) => state.likedFeedIds);
const isLoading = useFeedStore((state) => state.isLoading);
const setFilter = useFeedStore((state) => state.setFilter);
const toggleLike = useFeedStore((state) => state.toggleLike);
// 필터링된 피드 목록 계산
const filteredFeeds = useMemo(
() =>
feeds.filter((feed) => {
if (filter === 'liked') return likedFeedIds.has(feed.id);
return true; // 'all' 또는 다른 필터 로직
}),
[feeds, filter, likedFeedIds]
);
코드를 작성하는 손길은 익숙하고 편안했다. 하나의 스토어 파일 안에서 모든 상태와 로직을 관리하는 것은 명확하고 예측 가능했다. 하지만 파생 상태인 filteredFeeds
를 계산하기 위해 여러 상태 조각을 가져와 컴포넌트 레벨에서 useMemo
같은 최적화 훅을 사용해야 하는 부분에서는 미묘한 불편함이 느껴졌다.
다음은 Jotai 차례였다. 그는 심호흡을 하고, 동일한 기능을 아토믹 모델로 구현하기 시작했다.
// Jotai 방식 (개념적 스케치)
const feedsAtom = atomWithAsync(async () => {
/* ... API 호출 로직 ... */
}); // 비동기 아톰
const filterAtom = atom('all'); // 필터 상태 아톰
const likedFeedIdsAtom = atom(new Set()); // 좋아요 상태 아톰
// 파생 상태: 필터링된 피드 목록 (함수형 파생 아톰)
const filteredFeedsAtom = atom((get) => {
const feeds = get(feedsAtom); // 비동기 아톰 값 읽기 (Suspense 연동 가정)
const filter = get(filterAtom);
const likedFeedIds = get(likedFeedIdsAtom);
if (!feeds) return []; // 로딩 중이거나 에러 시
return feeds.filter((feed) => {
if (filter === 'liked') return likedFeedIds.has(feed.id);
return true;
});
});
// 컴포넌트에서 사용
function FeedList() {
const filteredFeeds = useAtomValue(filteredFeedsAtom); // 파생 아톰 값 직접 사용!
// ... 목록 렌더링 ...
}
function FilterControls() {
const [filter, setFilter] = useAtom(filterAtom);
// ... 필터 UI ...
}
function LikeButton({ feedId }) {
const [likedFeedIds, setLikedFeedIds] = useAtom(likedFeedIdsAtom);
const isLiked = likedFeedIds.has(feedId);
const toggleLike = () => {
setLikedFeedIds((prev) => {
const next = new Set(prev);
if (next.has(feedId)) next.delete(feedId);
else next.add(feedId);
return next;
});
};
// ... 버튼 UI ...
}
코드를 작성하면서 카토는 명백한 차이를 느꼈다. 상태 조각들이 각자의 아톰으로 분리되어, 필요한 컴포넌트 근처에서 정의되고 사용될 수 있었다. 특히 filteredFeedsAtom
처럼 파생 상태가 그 자체로 하나의 독립적인 아톰(함수)으로 정의되는 방식은 놀랍도록 간결하고 선언적이었다. 컴포넌트에서는 그저 useAtomValue(filteredFeedsAtom)
으로 최종 결과만 가져다 쓰면 되었다. useMemo
같은 수동 최적화 코드가 사라졌다!
두 가지 방식으로 구현된 코드를 나란히 놓고 비교했을 때, 카토의 머릿속은 명료해졌다.
‘이런 시나리오에서는… 확실히 Jotai가 더 간결하고 유연하군.’
상태들이 서로 느슨하게 연결되어 있고, 파생 상태 로직이 중요하며, 코드 스플리팅이나 모듈화가 중요한 경우에는 Jotai의 아토믹 모델이 빛을 발했다. 각 상태 조각이 독립적으로 존재하고, 필요한 곳에서 쉽게 조합하여 사용할 수 있다는 장점이 두드러졌다.
‘하지만…’
그는 동시에 Zustand의 강점도 다시 한번 확인했다. 만약 상태 구조가 비교적 단순하고, 예측 가능성이 중요하며, 모든 상태를 한눈에 파악하고 관리하는 것이 더 중요하다고 판단된다면? Zustand의 중앙 집중적인 스토어 모델이 더 나은 선택일 수도 있었다. 마치 잘 짜인 설계도에 따라 건축하는 것과, 다양한 재료로 자유롭게 조각하는 것의 차이와 같았다.
“두 라이브러리는… 경쟁자가 아니라, 서로 다른 문제를 푸는 도구였어.”
카토는 마침내 확신을 얻었다. Jotai는 Zustand의 대체재가 아니었다. 그것은 상태 관리라는 광대한 문제 영역에서, Zustand와는 다른 종류의 해답을 제시하는, 독자적인 가치를 지닌 존재였다. 각자의 자리에서 빛날 수 있는, 서로 다른 연장통에 담긴 정교한 도구들.
내부 검증은 끝났다. 창조주는 자신의 새로운 창조물이 세상에 나아갈 자격이 있음을 스스로에게 증명했다. 이제 남은 것은 단 하나. 이 새로운 도구를 세상에 선보이고, 필연적으로 쏟아질 그 질문에 답하는 것이었다.
“왜 Zustand가 있는데, 또 다른 상태 관리자를 만들었는가?”
카토는 그 질문에 대한 답을 가슴에 품고, 조용히 Jotai의 첫 번째 공식 릴리즈를 준비하기 시작했다. 그의 두 번째 모험은 이제 커뮤니티라는 더 넓은 바다를 향해 닻을 올릴 참이었다.