Zustand를 언제 도입해야 판교 실무에서 맞아떨어질까?
결론부터 적는다. Zustand 도입 여부는 "이 상태가 서버에서 왔는가, 아니면 클라이언트에서만 존재하는가"로 가른다. 서버 응답·캐시·재검증이 필요한 데이터는 TanStack Query 같은 서버 상태 도구에 맡기고, 모달·필터·선택된 탭처럼 서버에 원본이 없는 공유 상태만 Zustand로 옮기는 편이 사고가 덜 난다. 2026년 기준으로 두 도구를 역할로 나누는 조합이 가장 방어하기 쉬운 선택지였다.
- 서버 상태(API 응답·캐시·무효화)는 TanStack Query, 클라이언트 전역 UI 상태는 Zustand로 분리하는 것이 공식 문서 취지와 맞는다 출처
- URL로 공유·새로고침되어야 하는 값(검색어·정렬·페이지)은 Zustand에 복사하지 말고 URL을 단일 출처로 둔다
- 제출 전 폼 입력은 기본적으로 로컬 상태 대상이며, 여러 화면에서 초안을 공유할 때만 전역 스토어 후보가 된다
- 번들 크기나 렌더 시간 수치는 측정 조건(버전·빌드모드·선택자 구현)이 다르면 그대로 인용하면 안 된다
- 도입 후 실패는 기능보다 운영에서 나온다 — 서버 상태 복제와 스토어 무한 확장이 대표적이다
판교에서 몇 번 겪어보니 이 선택은 결국 "이 값의 원본이 어디 있는가"라는 질문 하나로 갈렸다. 원본이 서버에 있으면 아무리 편해도 Zustand에 다시 담지 않는 게 맞았고, 원본이 애초에 클라이언트에만 있으면 Context나 prop drilling보다 Zustand 쪽이 코드가 짧아졌다. 아래는 그 축을 하나씩 쪼갠 기준이다.
서버 상태와 클라이언트 상태, 어디까지 Zustand에 넣어도 될까?
넣어도 되는 쪽은 "서버가 모르는 상태"까지다. TanStack Query 공식 문서는 캐시·수명주기·재요청·중복 제거·낙관적 업데이트까지 서버 상태 관리를 전담하도록 설계되어 있다고 설명하며, Zustand·Redux·MobX 같은 클라이언트 상태 라이브러리도 비동기 서버 데이터를 저장할 수는 있지만 서버 상태 전용 도구보다 비효율적일 수 있다고 밝힌다 출처. 실무에서 이 경계를 넘는 전형적 실수는 "Query로 받아온 데이터를 useEffect로 Zustand 스토어에 다시 저장"하는 패턴이다. 화면이 두 출처(Query 캐시, Zustand 스토어)를 동시에 보게 되면서 갱신 시점이 어긋나는 걸 직접 겪은 적이 있다.
반대로 전역 모달 큐, 레이아웃 설정(사이드바 접힘 여부), 키보드 단축키 상태, 사용자 로컬 설정처럼 서버에 원본이 없는 값은 Zustand에 둘 근거가 선명하다. Provider를 여러 단계로 내려보내지 않고 필요한 컴포넌트만 선택적으로 구독할 수 있다는 점이 Context 대비 체감되는 차이였다.
URL이나 폼에 있는 값도 전역 스토어로 옮겨야 할까?
대부분은 옮기지 않는 편이 맞다. 검색어·정렬·페이지·필터처럼 새로고침과 공유, 뒤로가기에서 값이 보존돼야 한다면 URL 파라미터를 단일 출처로 삼는 게 Zustand에 복사하는 것보다 구조가 단순했다. URL에 있는 값을 다시 스토어에 동기화하면 "URL이 바뀌었는데 스토어가 안 바뀌는" 혹은 그 반대 케이스가 생긴다. 다만 2025~2026년 기준으로 이 패턴에 대한 공식 벤치마크 수치는 검색 자료에서 확인되지 않았으므로, 이 부분은 수치보다 구조적 판단으로 남겨둔다.
폼도 비슷하다. 제출 전 입력값을 전역 Zustand에 넣으면 컴포넌트 간 결합이 늘고, 폼을 닫았다가 다시 열 때 초기화 경로를 별도로 관리해야 했다. 여러 화면에 걸친 단계형 폼이나 임시 초안을 공유해야 하는 경우에만 전역 스토어 후보로 올렸고, 단일 화면 폼은 로컬 상태나 폼 전용 라이브러리로 남겨뒀다. 이 영역 역시 재현 가능한 공개 수치는 확인되지 않아 벤치마크 대신 설계 원칙으로만 적는다.
성능 수치만 보고 Zustand를 선택해도 괜찮을까?
참고는 되지만 그대로 믿고 설계 근거로 쓰기엔 조건이 부족하다. 한 2025년 개인 기술 글은 10개 구독 컴포넌트가 1,000회 업데이트되는 조건에서 Zustand 67ms, 최적화된 Context 89ms, Redux Toolkit 84ms라는 수치를 제시했고, 같은 조건에서 메모리 사용량은 Zustand 2.3MB, 최적화된 Context 2.0MB, Redux Toolkit 3.1MB로 보고했다 출처. React 버전, 브라우저, CPU, 빌드 모드, 선택자 구현이 명시되어 있지 않아서, "Zustand가 Context보다 빠르다"는 일반화로 쓰기보다는 "둘 다 최적화하면 격차가 크지 않을 수 있다"는 근거로만 쓰는 게 맞다.
오히려 실무에 더 영향을 준 건 선택자 설계였다. 다른 2025년 벤치마크는 전역 단일 스토어에서 평균 렌더 75ms, 도메인별로 스토어를 쪼갰을 때 32ms, 세밀한 선택자를 썼을 때 18ms를 보고했다 출처. 이 수치도 테스트 환경이 표준화되어 있지 않아 "4배 빠르다"로 못 박을 수는 없지만, 방향성 자체는 직접 짠 스토어에서도 비슷하게 체감했다. 하나의 거대 스토어를 구독할 때보다 useStore(state => state.filter)처럼 좁게 선택할 때 리렌더가 눈에 띄게 줄었다.
번들 크기도 같은 이유로 조심해서 써야 했다. 검색 결과에는 Zustand 약 1~4KB, Redux Toolkit 약 13~20KB 혹은 43KB까지 서로 다른 수치가 섞여 있었는데, 이는 minified·gzip 여부와 의존성 포함 여부가 통일되지 않았기 때문이다. 실측하려면 같은 lockfile, 같은 번들러로 raw/minified/gzip/brotli를 구분해서 직접 재봐야 하고, "Zustand가 작다" 정도로만 말하는 게 과장을 피하는 선이었다.
Zustand 도입에서 가장 흔하게 걸려 넘어지는 함정은 뭘까?
가장 먼저 걸리는 함정은 서버 상태 복제다. Query로 받아온 응답을 useEffect로 다시 Zustand에 저장하면 캐시 갱신 시점과 스토어 갱신 시점이 어긋나기 쉽고, 어느 쪽이 최신인지 디버깅하는 데 시간을 썼던 경험이 있다 출처. 화면은 Query 데이터를 직접 읽고, 클라이언트에서만 필요한 파생값이나 편집 초안만 Zustand에 남기는 구조가 안전했다.
두 번째 함정은 스토어의 무제한 확장이다. 처음엔 작은 UI 상태 하나로 시작했다가, 어느새 전역 스토어 하나에 액션 수십 개가 쌓이면 초기화·영속화·테스트 경계가 같이 흐려졌다. 기능·도메인 단위로 스토어를 쪼개고, 영속화 대상은 allowlist로 명시하고, 선택자를 강제하는 규칙을 팀 내에 세워두는 게 운영 단계에서 필요한 조건이었다. 마이그레이션도 Redux 전체를 한 번에 옮기기보다 저위험 UI 상태부터 옮겨 검증하는 쪽이 리스크가 작았는데, 이 접근에 대한 공식 가이드나 수치화된 기업 사례는 검색 자료에서 확인되지 않아 수치 없이 원칙으로만 남긴다.
결국 어떤 상황에 무엇을 고르면 될까?
축을 정리하면 다음과 같다. 서버 응답·캐시·재검증·낙관적 업데이트는 TanStack Query, 새로고침·공유·뒤로가기에 보존되어야 하는 값은 URL, 제출 전 단일 화면 입력은 로컬 상태나 폼 라이브러리, 여러 화면이 공유하지만 서버 원본이 없는 UI 상태만 Zustand로 보냈다. 상태가 한 컴포넌트나 가까운 부모·자식 사이에만 쓰이고 이미 URL·서버·폼이 출처 역할을 하고 있다면 useState나 useReducer로 충분했고, 전역 스토어를 먼저 만들면 오히려 데이터 소유권이 흐려졌다. 2026년 기준으로 이 네 갈래 구분이 가장 적은 함정으로 굴러갔다.
핵심 정리
- Zustand 도입 여부는 "상태의 원본이 서버인가 클라이언트인가"로 먼저 가른다
- 서버 응답·캐시·무효화는 TanStack Query, 모달·필터·레이아웃 같은 전역 UI 상태는 Zustand에 둔다
- URL로 보존돼야 하는 값과 제출 전 폼 입력은 기본적으로 Zustand 대상이 아니다
- 성능·메모리·번들 크기 수치는 측정 조건(버전·빌드모드·선택자)이 같을 때만 비교 근거가 된다
- 가장 흔한 함정은 서버 상태 복제와 스토어 무제한 확장이며, 둘 다 선택자와 도메인별 분리로 완화한다