Zustand

Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기

2026.05.04·수정 2026.05.12·약 13분

이번 단계에서 정리하는 내용

Zustand를 React 프로젝트에 설치하고, 컴포넌트 밖에 기본 Store를 만든 뒤 필요한 상태와 액션만 꺼내 쓰는 흐름을 정리합니다. 설치 명령어보다 더 오래 남겨야 할 기준은 안에서 상태와 상태 변경 함수를 어떤 책임으로 묶을지입니다.

Zustand 설치 후 바로 확인할 것

React 프로젝트에서 Zustand를 설치하고 Store 파일을 구성하는 기본 흐름

Zustand 설치 과정은 길지 않습니다. React 프로젝트가 이미 준비되어 있다면 패키지를 추가하고, Store 파일에서 create를 가져와 쓰면 됩니다. Redux Toolkit처럼 애플리케이션 최상단에 Provider를 먼저 감싸는 흐름으로 시작하지 않기 때문에, 처음에는 오히려 “이렇게만 해도 전역 상태가 되는 건가?”라는 느낌이 들 수 있습니다.

을 사용한다면 아래 명령어로 설치합니다.

npm install zustand

설치가 끝났다고 바로 모든 를 Store로 옮길 필요는 없습니다. Zustand는 컴포넌트 안의 지역 상태를 전부 대체하는 도구라기보다, 여러 컴포넌트가 같이 읽거나 수정해야 하는 상태를 컴포넌트 밖으로 빼는 데 잘 맞습니다.

예를 들어 버튼 하나의 열림 여부처럼 해당 컴포넌트 안에서만 끝나는 상태는 로 충분합니다. 반대로 헤더, 사이드바, 본문 컴포넌트가 같은 로그인 정보나 필터 값을 같이 봐야 한다면 Store로 분리할 이유가 생깁니다. 설치 다음에 바로 잡아야 하는 기준은 “Zustand를 쓸 수 있느냐”가 아니라 “이 상태가 컴포넌트 밖에 있어야 하느냐”입니다.

Store 파일을 컴포넌트 밖에 두는 이유

Zustand Store는 보통 컴포넌트 파일 안에 만들지 않습니다. Store가 특정 컴포넌트에 묶이면 다른 화면에서 재사용하기 어렵고, 상태의 책임도 흐려집니다. 처음에는 파일 하나를 더 만드는 일이 번거롭게 보일 수 있지만, 컴포넌트가 늘어나면 상태 파일을 따로 둔 차이가 더 빨리 드러납니다.

카운터 예제라면 아래처럼 둘 수 있습니다. 실제 프로젝트에서는 대신처럼 기능 이름이 들어가는 경우가 많습니다.

src/ stores/ useCounterStore.ts components/ CounterPanel.tsx CounterBadge.tsx

파일 이름을 로 지어도 동작에는 문제가 없습니다. 다만 Zustand Store는 React 컴포넌트에서 Hook처럼 호출하는 형태라서 처럼 이름을 맞춰두면 사용 위치에서 덜 헷갈립니다. 컴포넌트에서 를 보는 순간 “외부 Store에서 값을 가져오고 있구나”라고 읽히기 때문입니다.

여기서 Store는 단순한 전역 변수와 다릅니다. 값만 담아두는 파일이 아니라, 상태와 상태를 바꾸는 함수를 함께 가진 전용 Hook에 가깝습니다. 컴포넌트는 Store 내부 값을 직접 고치는 대신, Store가 제공하는 액션을 호출해서 상태 변경을 요청합니다.

이 차이를 놓치면 Store 파일이 금방 잡동사니가 됩니다. 화면에서 쓰는 모든 값을 한 파일에 넣는 것이 아니라, 함께 움직이는 상태끼리 묶어야 합니다. 카운터 값과 카운터 버튼 액션은 같은 Store에 두기 쉽지만로그인 사용자 정보와 상품 필터 값을 같은 Store에 넣을 이유는 거의 없습니다.

create로 기본 Store 만들기

기본 Store는 함수로 만듭니다. TypeScript를 사용한다면 Store가 가진 상태와 액션 타입을 먼저 정의해두면 좋습니다. 가 숫자인지가 인자를 받지 않는 함수인지 컴포넌트에서 바로 확인할 수 있고, 나중에 액션이 늘어났을 때도 Store의 모양이 흐려지지 않습니다.

import { create } from 'zustand'; type CounterStore = { count: number; increase: () => void; decrease: () => void; reset: () => void;
}; export const useCounterStore = create<CounterStore>()((set) => ({ count: 0, increase: () => set((state) => ({ count: state.count + 1 })), decrease: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 })}));

처음 보면 형태가 낯설 수 있습니다. 크게 나누면 는 Store Hook을 만들고는 이 Store가 어떤 상태와 액션을 갖는지 알려줍니다. 안쪽 함수에서 반환하는 객체가 실제 Store의 내용입니다.

는 화면에 표시될 상태 값입니다.은 상태를 바꾸는 액션입니다. Zustand에서는 이렇게 상태와 액션을 같은 Store 안에 둘 수 있습니다. 컴포넌트 입장에서는 “값은 Store에서 읽고, 변경도 Store에 요청한다”는 흐름이 됩니다.

은 상태를 변경할 때 사용하는 함수입니다. 현재 상태를 기준으로 다음 값을 계산해야 할 때는 형태를 씁니다. 와 는 기존 를 기준으로 새 값을 만들기 때문에 현재 상태를 받아 계산합니다. 반대로 처럼 특정 값으로 바로 돌려놓는 경우에는 처럼 작성할 수 있습니다.

상태와 액션 이름을 분리해서 읽기

Store 안에 상태와 액션이 같이 들어간다고 해서 둘의 역할이 같아지는 것은 아닙니다. 상태는 현재 화면이 참고하는 값이고, 액션은 그 값을 바꾸는 통로입니다. 이 구분이 흐리면 Store가 커졌을 때 어떤 함수가 어떤 상태를 바꾸는지 찾기 어려워집니다.

처음에는 처럼 에서 가져온 이름을 그대로 쓰고 싶을 수 있습니다. 하지만 Zustand Store에서는 처럼 상태 변화의 의도가 드러나는 이름이 더 읽기 좋습니다. 나중에 버튼 클릭, 키보드 이벤트, API 성공 후 처리처럼 호출 위치가 늘어났을 때 함수 이름만 보고도 흐름을 따라갈 수 있습니다.

컴포넌트에서 Store 사용하기

Store를 만들었다면 컴포넌트에서는 를 호출해 필요한 값과 액션을 가져옵니다. 이때 Store 전체를 한 번에 가져오기보다 selector로 필요한 항목만 선택하는 습관을 먼저 들이는 것이 낫습니다.

import { useCounterStore } from '../stores/useCounterStore'; export function CounterPanel() { const count = useCounterStore((state) => state.count); const increase = useCounterStore((state) => state.increase); const decrease = useCounterStore((state) => state.decrease); const reset = useCounterStore((state) => state.reset); return ( <section> <p>현재 값: {count}</p> <button type="button" onClick={decrease}> -1 </button> <button type="button" onClick={increase}> +1 </button> <button type="button" onClick={reset}> 초기화 </button> </section> );
}

이 컴포넌트 안에는 가 없습니다. 상태는 Store에 있고, 버튼은 Store에 정의된 액션을 호출합니다. 같은 를 다른 컴포넌트에서도 읽을 수 있다는 점이 지역 상태와 가장 크게 달라지는 부분입니다.

예를 들어 상단 배지에는 현재 카운트만 보여주고, 별도의 패널에서는 값을 변경한다고 가정할 수 있습니다. 였다면 부모 컴포넌트로 상태를 끌어올리고 props를 내려보내야 합니다. Zustand를 쓰면 두 컴포넌트가 같은 Store를 바라보면 됩니다.

import { useCounterStore } from '../stores/useCounterStore'; export function CounterBadge() { const count = useCounterStore((state) => state.count); return <span>카운트 {count}</span>;
}

이 구조가 Zustand를 처음 배울 때 확인해야 할 첫 번째 변화입니다. 상태를 어느 컴포넌트가 소유하는지 계속 고민하기보다, 기능 단위 Store가 상태를 가지고 있고 필요한 컴포넌트가 선택해서 구독하는 방식으로 바뀝니다.

처음 작성할 때 헷갈리는 부분

첫 번째로 헷갈리는 지점은 Store 전체를 한 번에 꺼내는 방식입니다. 아래처럼 작성하면 코드가 짧아 보입니다.

const { count, increase, decrease, reset } = useCounterStore();

작은 예제에서는 큰 문제가 없어 보입니다. 하지만 Store에 값이 많아지면 컴포넌트가 실제로 필요하지 않은 상태 변화에도 영향을 받을 수 있습니다. 처음부터 처럼 필요한 값을 선택해서 가져오는 형태에 익숙해지는 편이 이후 수정에 유리합니다.

두 번째는 Store를 너무 크게 만드는 문제입니다. 카운터, 모달, 사용자 정보, 검색 필터를 하나의 Store에 모두 넣으면 처음에는 파일이 하나라 편해 보입니다. 시간이 지나면 액션 이름이 충돌하고, 어떤 컴포넌트가 어떤 상태에 의존하는지 파악하기 어려워집니다.

처음에는 기능 단위로 작게 나누는 기준이면 충분합니다. 예를 들어처럼 화면이나 기능의 책임이 드러나게 분리합니다. Store를 나누는 기준은 폴더 구조보다 상태가 함께 변경되는지에 더 가깝습니다.

세 번째는 액션 안에서 상태를 직접 바꾸는 듯한 코드를 작성하는 문제입니다. Zustand의 기본 흐름에서는 새 상태 조각을 반환하는 방식으로 작성합니다. 객체나 배열을 다룰 때는 기존 값을 직접 변경하기보다 새 값으로 만들어 넘기는 습관을 유지해야 합니다.

이 단계에서는 middleware, persist 같은 기능을 한 번에 붙이지 않는 편이 낫습니다. 기본 Store 모양이 손에 익기 전에 기능을 많이 붙이면 어떤 코드가 Zustand의 기본 구조이고 어떤 코드가 확장 기능인지 구분하기 어려워집니다.

다음 단계로 넘어가기 전 확인할 기준

Zustand Store에서 상태와 액션을 분리하고 컴포넌트가 필요한 값만 선택하는 구조

기본 Store를 만들었다면 다음 단계로 넘어가기 전에 몇 가지를 확인해야 합니다. 설치가 되었고 화면이 움직인다는 사실만으로는 부족합니다. Store 구조가 나중에 확장될 수 있는 모양인지 먼저 봐야 합니다.

먼저 Store 이름이 기능을 설명하는지 확인합니다. 하나로 시작하면 예제는 짧아지지만, 실제 프로젝트에서는 금방 의미가 흐려집니다.처럼 이름만 봐도 역할이 드러나야 합니다.

다음으로 상태와 액션 이름을 분리해서 읽을 수 있는지 봅니다. 는 값이고는 행동입니다. 이 규칙이 지켜지면 Store 파일을 처음 보는 사람도 구조를 따라가기 쉽습니다. 반대로같은 이름만 쌓이면 Store가 커졌을 때 수정 지점을 찾기 어려워집니다.

마지막으로 컴포넌트가 Store에 과하게 의존하고 있지 않은지 확인합니다. 컴포넌트는 필요한 값과 액션만 가져오면 됩니다. Store 내부의 모든 값을 한 번에 꺼내 쓰기 시작하면 전역 상태를 쓴다는 장점보다 결합도가 커지는 문제가 먼저 나타납니다.

다음 단계에서 상태를 읽고 변경하는 방식을 더 자세히 다룰 예정이라면, 지금은 Store 하나가 어떻게 만들어지고 컴포넌트가 어떤 방식으로 연결되는지만 확실히 잡아두면 됩니다.

정리

Zustand의 첫 단계는 설치보다 기본 Store 구조를 잡는 데 있습니다. 로 Store Hook을 만들고, 그 안에 상태와 액션을 함께 정의합니다. 컴포넌트에서는 Store 전체를 가져오기보다 필요한 값과 액션만 선택해서 사용합니다.

카운터 예제는 단순하지만이 예제에서 봐야 하는 것은 숫자가 증가하는 장면이 아닙니다. 상태를 컴포넌트 밖으로 분리했을 때 코드의 책임이 어떻게 바뀌는지 확인하는 것이 더 중요합니다. 이 기준이 잡히면 다음 단계에서 상태 읽기, 상태 변경, 액션 분리, Store 분할을 더 자연스럽게 이어갈 수 있습니다.

참고 자료

Zustand 공식 문서의 설치 방식, 기본 사용법, TypeScript Store 작성 흐름을 기준으로 내용을 확인했습니다.

같이 읽으면 좋은 글

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기