이 글에서 정리하는 내용
Zustand를 처음 공부할 때는 문법보다 상태를 어디에 둘지부터 잡아야 합니다. 컴포넌트 내부 state, Context API, Redux Toolkit과 비교하면서 Zustand가 어떤 문제를 줄이고, 어떤 상태를 맡기면 되는지 기준을 정리합니다.
- React에서 상태가 흩어지기 시작하는 순간
- Zustand는 어떤 문제를 줄이려는 도구인가
- Zustand의 기본 구성
- useState, Context API, Redux Toolkit과의 차이
- Zustand에 넣기 좋은 상태와 아닌 상태
- 처음 배울 때 확인할 기준
- 정리
React에서 상태가 흩어지기 시작하는 순간

React에서 상태 관리는 처음에는 크게 복잡하지 않습니다. 버튼을 눌렀는지, 입력창에 어떤 값이 들어왔는지, 탭이 몇 번째 항목을 보고 있는지 정도는 컴포넌트 안에서 useState로 처리해도 충분합니다. 상태가 그 컴포넌트 안에서만 쓰이고 다른 화면과 엮이지 않는다면 별도의 상태 관리 라이브러리를 먼저 꺼낼 필요는 없습니다.
불편함은 같은 값을 여러 컴포넌트가 같이 보기 시작할 때 생깁니다. 예를 들어 헤더에서는 로그인 사용자 이름을 보여주고, 사이드바에서는 권한에 따라 메뉴를 바꾸고, 설정 페이지에서는 같은 사용자 정보를 수정해야 한다고 가정해볼 수 있습니다. 이때 사용자 정보를 각 컴포넌트마다 따로 들고 있으면 값이 어긋날 수 있고, 상위 컴포넌트에서 props로 계속 내려보내면 중간 컴포넌트가 단순 전달 통로가 됩니다.
모달 상태도 비슷합니다. 버튼은 리스트 안에 있고, 실제 모달은 페이지 하단에서 렌더링되며, 닫기 처리는 모달 내부에서 일어난다면 상태의 위치가 애매해집니다. 가까운 부모에 두면 당장은 해결되지만, 기능이 늘어날수록 상태를 어디에 둘지 다시 판단해야 합니다.
상태를 위로 올리는 방식은 React에서 자연스러운 해결책입니다. 하지만 올려야 하는 상태가 많아지면 화면 구조보다 상태 전달 구조가 더 눈에 띄게 됩니다. 컴포넌트가 실제로는 값을 쓰지 않는데 props를 넘기기 위해 매개 역할만 하는 상황이 반복되면 상태 관리 코드를 따로 분리할 시점입니다.
Zustand는 이런 상황에서 검토할 수 있는 클라이언트 상태 관리 도구입니다. 단순히 값을 저장하는 저장소가 아니라, 여러 컴포넌트가 같은 상태를 읽고 바꿀 수 있도록 컴포넌트 바깥에 store를 만드는 방식으로 접근합니다.
Zustand는 어떤 문제를 줄이려는 도구인가
Zustand를 한 문장으로 정리하면 React에서 사용할 수 있는 가벼운 전역 상태 관리 라이브러리입니다. 다만 여기서 전역이라는 말을 너무 크게 받아들이면 오해가 생깁니다. 애플리케이션의 모든 값을 하나의 큰 저장소에 몰아넣는다는 뜻이 아닙니다. 여러 컴포넌트가 함께 봐야 하는 클라이언트 상태를 컴포넌트 바깥으로 분리한다는 의미에 가깝습니다.
Zustand의 특징은 store를 hook처럼 사용할 수 있다는 점입니다. store 안에는 숫자, 문자열, 객체 같은 상태도 들어갈 수 있고, 그 상태를 바꾸는 함수도 함께 둘 수 있습니다. 컴포넌트는 store 전체를 억지로 가져오는 대신 필요한 값이나 함수만 선택해서 사용합니다.
이 구조는 상태를 전달하는 코드가 많아질 때 차이를 만듭니다. props를 여러 단계로 내려보내지 않아도 되고, 기본적인 사용에서는 Context Provider를 화면 루트에 계속 추가하지 않아도 됩니다. 컴포넌트는 필요한 위치에서 store를 호출하고, 자신이 선택한 값이 바뀔 때 다시 렌더링됩니다.
처음에는 이 방식이 너무 단순해 보여서 오히려 헷갈릴 수 있습니다. Redux처럼 action type을 만들지 않고, reducer를 따로 작성하지도 않습니다. Context처럼 Provider로 감싸는 코드도 기본 사용에서는 보이지 않습니다. 그래서 Zustand를 배울 때는 “문법이 짧다”보다 “상태를 컴포넌트 밖으로 빼고, 필요한 컴포넌트만 구독한다”는 구조를 먼저 보는 편이 이해에 맞습니다.
여기서 구독이라는 말은 store의 변화를 컴포넌트가 지켜본다는 뜻입니다. 전체 store가 아니라 선택한 값만 바라보게 만들면, 상태가 커졌을 때도 어떤 컴포넌트가 어떤 값 때문에 다시 렌더링되는지 추적하기 쉬워집니다.
Zustand의 기본 구성
Zustand에서 자주 만나는 단어는 store, state, action, selector입니다. 이 네 가지를 구분해두면 이후 설치와 코드 작성 단계로 넘어갈 때 덜 흔들립니다.
store는 상태를 모아두는 공간입니다. React 컴포넌트 안에 만드는 것이 아니라 별도 파일에 만들고, 여러 컴포넌트에서 가져다 씁니다. 보통 useCounterStore, useModalStore, useAuthStore처럼 hook 이름과 비슷한 형태로 작성합니다.
state는 실제 값입니다. 카운터의 숫자, 모달 열림 여부, 선택된 필터, 사용자 정보처럼 화면이 참고하는 데이터가 여기에 해당합니다. Zustand에서는 이 값들을 store 안에 객체 형태로 둡니다.
action은 상태를 바꾸는 함수입니다. 예를 들어 openModal, closeModal, increase, setUser 같은 함수가 action입니다. 상태 변경 로직을 컴포넌트 안에 흩어두지 않고 store 안에 모으면, 어느 화면에서 상태를 어떻게 바꾸는지 찾기 쉬워집니다.
selector는 store에서 필요한 부분만 꺼내는 함수입니다. store 안에 값이 여러 개 있어도 컴포넌트가 항상 전체 store를 읽을 필요는 없습니다. 카운터 숫자만 필요한 컴포넌트는 숫자만 선택하고, 버튼 컴포넌트는 증가 함수만 선택하면 됩니다.
import { create } from 'zustand'; type CounterStore = { count: number; increase: () => void; reset: () => void;
}; export const useCounterStore = create<CounterStore>((set) => ({ count: 0, increase: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 })})); 이 예시에서 count는 state이고, increase와 reset은 action입니다. create로 만든 useCounterStore는 컴포넌트에서 hook처럼 사용할 수 있습니다. 상태를 바꿀 때는 set 함수를 사용하고이전 상태를 기준으로 계산해야 할 때는 set((state) => ...) 형태로 작성합니다.
function CounterValue() { const count = useCounterStore((state) => state.count); return <p>현재 숫자: {count}</p>;
} function CounterButton() { const increase = useCounterStore((state) => state.increase); return <button onClick={increase}>증가</button>;
} 여기서 두 컴포넌트는 같은 store를 사용하지만 읽는 대상이 다릅니다. CounterValue는 숫자를 읽고, CounterButton은 증가 함수만 가져옵니다. 이런 식으로 컴포넌트가 필요한 부분만 선택하게 만들면 store가 커져도 각 컴포넌트의 관심사를 좁게 유지할 수 있습니다.
store 이름은 상태의 성격을 드러내는 쪽이 좋습니다. 카운터 예시는 학습용으로 단순하지만, 실제 화면에서는 useModalStore, useFilterStore, useEditorStore처럼 기능 단위로 나누는 경우가 많습니다. 단순히 파일을 많이 쪼개는 것이 아니라, 어떤 화면 흐름이 같은 상태를 공유하는지 보고 나누는 방식입니다.
, Context API, Redux Toolkit과의 차이
Zustand를 제대로 이해하려면 다른 상태 관리 방식과의 경계를 같이 봐야 합니다. 각각의 도구는 틀린 방식과 맞는 방식으로 나뉘기보다 맡는 역할이 다릅니다.
useState는 컴포넌트 내부 상태에 적합합니다. 입력값, 토글 여부, 현재 선택된 탭처럼 특정 컴포넌트 안에서만 의미가 있는 값은 useState로 충분합니다. 이 상태까지 Zustand로 빼면 오히려 코드가 멀어지고, 수정할 때 파일을 더 많이 오가게 됩니다.
Context API는 값을 하위 트리로 전달할 때 사용할 수 있습니다. 테마, 언어, 인증 정보처럼 여러 컴포넌트가 같은 값을 봐야 할 때 사용할 수 있지만, 상태 변경이 자주 일어나는 값까지 무겁게 넣기 시작하면 Provider의 value 관리와 렌더링 범위를 신경 써야 합니다. Zustand는 이 부분을 store와 selector 중심으로 다루게 해줍니다.
Redux Toolkit은 상태 변경 흐름을 더 명시적으로 관리할 수 있는 도구입니다. slice, reducer, action, middleware, devtools 같은 구조가 있고, 규모가 크거나 팀 규칙이 강하게 필요한 프로젝트에서 여전히 선택할 이유가 있습니다. 반면 작은 프로젝트나 UI 중심의 전역 상태를 빠르게 정리해야 하는 상황에서는 Zustand가 훨씬 적은 코드로 시작할 수 있습니다.
| 도구 | 잘 맞는 상태 | 처음 볼 때의 기준 |
|---|---|---|
| 컴포넌트 내부에서만 쓰는 상태 | 상태를 쓰는 범위가 좁으면 그대로 둡니다. | |
| Context API | 하위 트리에 공유할 값 | Provider로 감싸는 구조가 자연스러운지 봅니다. |
| Zustand | 여러 컴포넌트가 공유하는 클라이언트 상태 | store로 분리했을 때 전달 코드가 줄어드는지 봅니다. |
| Redux Toolkit | 규칙과 추적이 강하게 필요한 복잡한 전역 상태 | 상태 변경 흐름을 엄격하게 관리해야 하는지 봅니다. |
이 비교에서 놓치기 쉬운 부분은 Zustand가 Redux Toolkit을 무조건 대체하는 도구가 아니라는 점입니다. Zustand는 간단하게 시작하기 좋지만, 그만큼 구조를 스스로 정해야 합니다. store를 아무 기준 없이 계속 늘리면 파일만 나뉘었을 뿐, 상태 관리가 깔끔해지는 것은 아닙니다.
선택 기준은 코드 양만으로 잡지 않는 편이 낫습니다. 상태 변경 과정을 팀 전체가 엄격하게 추적해야 한다면 Redux Toolkit의 구조가 더 맞을 수 있습니다. 반대로 모달, 필터, 편집 중인 UI 상태처럼 화면 조작 중심의 값이 여러 컴포넌트에 퍼져 있다면 Zustand가 부담을 줄여줍니다.
Zustand에 넣기 좋은 상태와 아닌 상태
Zustand를 처음 쓰면 모든 상태를 store에 넣고 싶어질 수 있습니다. 하지만 전역 상태 관리에서 가장 먼저 정해야 하는 것은 “어디에 넣을까”가 아니라 “굳이 전역이어야 하는가”입니다.
Zustand에 넣기 좋은 상태는 여러 컴포넌트가 함께 쓰는 클라이언트 상태입니다. 예를 들어 전역 모달 상태, 사이드바 접힘 여부, 선택된 필터 조건, 다단계 폼의 임시 진행 상태로그인 후 클라이언트에서 참고하는 사용자 UI 정보 등이 후보가 될 수 있습니다.
반대로 특정 컴포넌트 안에서만 쓰는 입력값이나 hover 상태, 단순한 열림/닫힘 값은 store로 빼지 않는 편이 낫습니다. 상태가 사용되는 위치와 변경되는 위치가 가까우면, 컴포넌트 안에 두는 쪽이 코드를 읽기 쉽습니다.
서버 데이터도 구분해야 합니다. API로 받아온 상품 목록, 게시글 목록, 사용자 상세 데이터처럼 서버와 동기화되는 데이터는 캐싱, 재요청로딩에러, 무효화 같은 흐름이 따라옵니다. 이런 영역은 Zustand보다 TanStack Query 같은 서버 상태 관리 도구가 더 자연스럽습니다. Zustand는 서버에서 가져온 원본 데이터를 오래 들고 있는 저장소라기보다, 화면에서 공유해야 하는 클라이언트 상태를 다루는 쪽에 초점을 맞추는 것이 좋습니다.
예를 들어 쇼핑몰 화면을 기준으로 보면 상품 목록 자체는 서버 데이터입니다. 하지만 사용자가 현재 선택한 정렬 방식, 필터 패널 열림 여부, 장바구니 미리보기 모달 상태는 클라이언트 상태입니다. 이 둘을 한 store에 섞으면 나중에 새로고침, 캐시 갱신, 서버 요청 실패 상황에서 판단이 어려워집니다.
관리자 화면도 같은 기준으로 볼 수 있습니다. 목록 데이터는 서버에서 받아오고, 현재 열려 있는 상세 패널, 선택된 행, 임시 검색 조건처럼 화면 조작에 가까운 값은 Zustand 후보가 됩니다. 이 경계를 잡아두면 store가 데이터 캐시 역할까지 떠안는 상황을 줄일 수 있습니다.
처음 배울 때 확인할 기준

Zustand를 처음 공부할 때는 store를 만드는 문법보다 상태 분리 기준을 먼저 확인하는 편이 안정적입니다. 문법은 짧기 때문에 금방 따라 칠 수 있지만, 어떤 상태를 store에 넣을지 기준이 없으면 프로젝트가 커질수록 store가 임시 보관함처럼 변합니다.
첫 번째 기준은 상태의 사용 범위입니다. 하나의 컴포넌트 안에서만 쓰면 useState로 두고, 서로 떨어진 여러 컴포넌트가 같은 값을 읽거나 바꾼다면 Zustand 후보로 볼 수 있습니다.
두 번째 기준은 상태의 성격입니다. 화면 조작을 위한 클라이언트 상태인지, 서버에서 받아오는 데이터인지 나눠야 합니다. 서버 데이터라면 캐싱과 재요청 기준이 필요하기 때문에 별도의 서버 상태 관리 도구를 먼저 검토하는 것이 맞습니다.
세 번째 기준은 action의 위치입니다. 상태를 바꾸는 코드가 여러 컴포넌트에 흩어져 있다면 store 안에 action으로 모으는 것이 좋습니다. 반대로 단 한 곳에서만 바뀌는 값이라면 store까지 만들 필요가 없을 수 있습니다.
네 번째 기준은 selector 사용입니다. 컴포넌트가 store 전체를 통째로 가져오는 습관은 피해야 합니다. 지금 필요한 값 하나, 필요한 함수 하나를 선택해서 가져오면 상태가 커졌을 때 렌더링 흐름을 더 좁게 관리할 수 있습니다.
마지막으로 store를 만든 뒤에는 이름만 보고 역할이 보이는지 확인해야 합니다. useStore처럼 범위가 넓은 이름은 처음에는 편해 보여도, 시간이 지나면 어떤 상태를 넣어야 하는지 경계가 흐려집니다. 작은 store라도 역할이 분명한 이름을 붙이는 쪽이 유지보수에 유리합니다.
정리
Zustand는 React에서 전역 상태를 더 가볍게 다루기 위한 도구입니다. 핵심은 store를 컴포넌트 밖에 만들고, 컴포넌트는 필요한 상태와 action만 선택해서 사용하는 구조입니다.
처음에는 Redux Toolkit보다 코드가 짧다는 점이 먼저 보일 수 있습니다. 하지만 Zustand를 오래 쓰려면 짧은 문법보다 상태의 경계를 정하는 기준이 더 중요합니다. 컴포넌트 안에서 끝나는 값은 useState, 여러 컴포넌트가 공유하는 클라이언트 상태는 Zustand, 서버와 동기화되는 데이터는 TanStack Query처럼 역할을 나눠야 합니다.
다음 단계에서는 Zustand를 실제 프로젝트에 설치하고가장 작은 store를 직접 만들어보면 됩니다. 이때 목표는 많은 기능을 한 번에 넣는 것이 아니라 state, action, selector가 코드에서 어떻게 나뉘는지 확인하는 것입니다.
같이 읽으면 좋은 글
- React 처음 배우는 순서: 컴포넌트부터 state, Zustand까지
- Zustand 학습 순서: store, action, selector, persist까지
- React state vs Zustand: 언제 전역 상태가 필요할까
“Zustand 개념 정리: React 상태 관리를 가볍게 나누는 기준”에 대한 2개의 생각