Zustand란? React 상태 관리 선택 기준과 기본 Store

··설명 약 8분·작성: 해비·블로그 소개

Zustand는 무엇이며 state와 action, 컴포넌트 구독을 어떻게 연결하나요?

Zustand는 state와 변경 함수를 store에 정의하고 React 컴포넌트가 필요한 값만 구독하게 하는 상태 관리 도구입니다. 작은 선택 예제로 state와 action의 관계부터 확인합니다.

적용 조건·범위: 실행 환경은 React·TypeScript 앱과 Zustand 5입니다. React 함수 컴포넌트와 이벤트를 알고 있으면 이전 TaskFlow 프로젝트 없이 실습할 수 있습니다. 환경이 없다면 Zustand 학습 로드맵: store·action·selector·persist 순서의 준비 단계 또는 Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기의 설치 항목을 확인하세요. 아래는 별도 연습 앱의 src/App.tsx 전체 내용입니다. 기존 앱에서 연결한 src/main.tsx는 유지합니다.

본문 근거: Zustand create · zustand.docs.pmnd.rs · zustand.docs.pmnd.rs/apis/create

이 기술만 따로 실행하기

이 개념 직접 실행

상태를 바꾸고 화면에 반영되는 결과를 확인합니다.

예제 크게 보기 (새 탭)

기대 결과: 처음에는 선택: 없음, 첫 작업 선택 뒤에는 선택: task-1, 해제 뒤에는 다시 없음이 표시되어야 합니다.

직접 실행: 환경 준비와 전체 파일

실행 환경은 React·TypeScript 앱과 Zustand 5입니다. React 함수 컴포넌트와 이벤트를 알고 있으면 이전 TaskFlow 프로젝트 없이 실습할 수 있습니다. 환경이 없다면 Zustand 학습 로드맵: store·action·selector·persist 순서의 준비 단계 또는 Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기의 설치 항목을 확인하세요. 아래는 별도 연습 앱의 src/App.tsx 전체 내용입니다. 기존 앱에서 연결한 src/main.tsx는 유지합니다.

확인할 동작: 선택 상태와 행동.

import { create } from 'zustand';
type Selection = { selectedId: string | null; select: (id: string | null) => void };
const useSelection = create<Selection>()((set) => ({
  selectedId: null,
  select: (selectedId) => set({ selectedId }),
}));
function SelectedLabel() {
  const id = useSelection((state) => state.selectedId);
  return <p>선택: {id ?? '없음'}</p>;
}
export default function App() {
  const select = useSelection((state) => state.select);
  return (
    <main>
      <SelectedLabel />
      <button onClick={() => select('task-1')}>첫 작업 선택</button>
      <button onClick={() => select(null)}>선택 해제</button>
    </main>
  );
}
npm run dev
npm run build

자가 확인: selectedId는 state이고 select는 action입니다. 두 컴포넌트가 같은 store를 사용하되 각각 표시값과 변경 함수만 구독합니다. 선택값에 따른 라벨을 별도 state로 저장할 필요가 있을까요? selectedId에서 계산할 수 있으므로 필요하지 않습니다.

검증: 2026-09-28, 독립 예제는 TypeScript strict 검사와 Vite 라이브러리 빌드를 통과했습니다. jsdom에서 버튼 클릭 후 표시값을 확인했습니다. 실제 브라우저 배치와 누적 프로젝트 전체 실행은 이번 검사 범위가 아닙니다.

공식 기준: Zustand create (확인일 2026-09-28).

Zustand의 핵심 구조

가장 기본적인 Zustand store는 다음 모양입니다.

import { create } from 'zustand';


type CounterStore = {
  count: number;
  increase: () => void;
};


export const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increase: () =>
    set((state) => ({
      count: state.count + 1,
    })),
}));

구조를 나누면 다음과 같습니다.

count
→ state


increase
→ action


set
→ store 상태 변경 함수

컴포넌트에서는 필요한 값만 가져옵니다.

const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);

reducer + Context와 비교

React reducer + Context에서는 보통 다음 구조가 있습니다.

TaskContext
TaskProvider
taskReducer
dispatch
useContext

Zustand로 옮기면 다음 구조로 단순화할 수 있습니다.

useTaskStore
state
actions
selectors

기존 reducer에서는 action 객체를 dispatch합니다.

dispatch({
  type: 'task/toggled',
  id,
});

Zustand에서는 메서드처럼 action을 호출할 수 있습니다.

toggleTask(id);

둘 중 하나가 항상 더 좋은 것은 아닙니다. 중요한 차이는 Zustand가 Provider 없이 store를 직접 구독하고, 필요한 값만 선택하는 패턴을 쉽게 제공한다는 점입니다.

create는 무엇을 만드는가

create는 React 컴포넌트에서 사용할 수 있는 store hook을 만듭니다.

const useStore = create((set) => ({
  count: 0,
}));

이후 컴포넌트는 useStore를 호출해 state를 구독합니다.

const count = useStore((state) => state.count);

여기서 selector 함수가 어떤 값을 반환하는지가 리렌더링 범위를 결정하는 중요한 기준이 됩니다.

selector는 7~9회차에서 자세히 다룹니다.

state와 action을 같이 두는 이유

상태와 변경 로직을 store에 함께 두면 컴포넌트가 구체적인 변경 방법을 알 필요가 없습니다.

좋지 않은 예:

const tasks = useTaskStore((state) => state.tasks);
const setTasks = useTaskStore((state) => state.setTasks);


setTasks(
  tasks.map((task) =>
    task.id === id
      ? { ...task, completed: !task.completed }
      : task,
  ),
);

더 나은 방향:

const toggleTask = useTaskStore((state) => state.toggleTask);


toggleTask(id);

컴포넌트는 “무엇을 할지”만 요청하고, “어떻게 상태를 바꿀지”는 store가 담당합니다.

Store 설계에서 먼저 정할 것

store를 만들기 전에 다음을 먼저 정합니다.

  1. 어떤 state가 필요한가
  2. 어떤 action이 필요한가
  3. 어떤 값은 로컬 state로 남길 것인가
  4. 어떤 값은 persist할 것인가
  5. 서버 상태와 겹치는 값은 없는가

TaskFlow에서는 우선 다음 구조를 목표로 합니다.

type TaskStore = {
  tasks: Task[];
  selectedTaskId: string | null;
  isTaskModalOpen: boolean;
  filter: TaskFilter;


  addTask: (title: string) => void;
  toggleTask: (id: string) => void;
  deleteTask: (id: string) => void;


  selectTask: (id: string | null) => void;
  openTaskModal: () => void;
  closeTaskModal: () => void;
  setFilter: (filter: TaskFilter) => void;
};

Store를 너무 크게 만들지 않는 기준

한 store에 모든 앱 상태를 넣는 것은 권장되지 않습니다.

다음처럼 역할을 나눌 수 있습니다.

taskStore
→ TaskFlow 작업과 선택 상태


uiStore
→ 전역 레이아웃, theme, sidebar 등


server state
→ TanStack Query

처음부터 store를 여러 개 만들 필요는 없지만, 책임이 다른 상태를 구분할 수 있어야 합니다.

자주 발생하는 설계 실수

setter만 많이 만듭니다

setTasks
setSelectedTaskId
setModal
setFilter

이 방식은 store를 단순한 전역 변수 모음으로 만들기 쉽습니다.

가능하면 도메인 행동을 action 이름으로 표현합니다.

addTask
toggleTask
selectTask
openTaskModal

파생 값을 state로 저장합니다

예를 들어 completedCount는 tasks에서 계산할 수 있습니다.

const completedCount = tasks.filter(
  (task) => task.completed,
).length;

계산 가능한 값을 별도 state로 저장하면 두 값이 어긋날 가능성이 생깁니다.

학습 완료 체크리스트

  • create가 store hook을 만든다는 것을 이해했습니다.
  • state와 action의 차이를 설명할 수 있습니다.
  • reducer + Context와 Zustand 구조를 비교할 수 있습니다.
  • setter보다 행동 중심 action을 설계할 수 있습니다.
  • 파생 값을 불필요하게 state로 저장하지 않을 수 있습니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 여러 화면이 공유하는 상태를 관리합니다.

실제 설치 전에 TaskFlow의 useTaskStore 구조를 설계합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

TaskFlow 직접 실행

src/types/task-store-plan.ts에 Store 타입 계약을 추가합니다. 이 설계 타입은 실행 상태를 만들지 않으며 기존 앱은 유지됩니다.

예제 크게 보기 (새 탭)

중간부터 시작하는 전체 실습 파일

기존 React Context TaskFlow의 작업 목록·추가·완료·삭제 화면을 이어서 확장합니다.

이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.

시작본에서 직접 바꾸기

개념·경계 설계 단계라 실행 앱을 바꾸지 않고 ZIP의 문서를 직접 작성·비교합니다.

  1. src/types/task-store-plan.ts — 이전 단계에 없던 파일을 만듭니다.

변경 이유와 결과: src/types/task-store-plan.ts에 Store 타입 계약을 추가합니다. 이 설계 타입은 실행 상태를 만들지 않으며 기존 앱은 유지됩니다.

완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.

src/types/task-store-plan.ts 전체 파일

import type { Task } from './task';


export type TaskFilter =
  | 'all'
  | 'active'
  | 'completed';


export type TaskStore = {
  tasks: Task[];
  selectedTaskId: string | null;
  isTaskModalOpen: boolean;
  filter: TaskFilter;


  addTask: (title: string) => void;
  toggleTask: (id: string) => void;
  deleteTask: (id: string) => void;
  selectTask: (id: string | null) => void;
  openTaskModal: () => void;
  closeTaskModal: () => void;
  setFilter: (filter: TaskFilter) => void;
};

완료 확인

  1. 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤 npm ci로 잠금 파일의 의존성을 설치합니다.
  2. npm run dev 실행 후 터미널의 localhost 주소를 엽니다.
  3. src/types/task-store-plan.ts에 Store 타입 계약을 추가합니다. 이 설계 타입은 실행 상태를 만들지 않으며 기존 앱은 유지됩니다.
  4. npm run typecheck와 npm run build가 성공하면 다음 프로젝트 단계로 갑니다.

이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.

아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.

이어서 만들기: React state vs Zustand: 전역 상태가 필요한 기준의 적용 결과에서 시작합니다. 아래 체크포인트 이름은 단계 식별자이며 다운로드 파일명이나 Git 명령이 아닙니다.

하단은 TaskFlow의 상태 설계 과제입니다. src/store/useTaskStore.ts에 들어갈 타입 초안을 작성하되 기존 Context 동작은 유지합니다. Task와 TaskFilter는 아래 설계 초안의 프로젝트 타입을 사용하며, 상단 Selection 타입과 섞지 않습니다.

이전 체크포인트

zustand-step-02-boundary

이번 목표

실제 설치 전에 TaskFlow의 useTaskStore 구조를 설계합니다.

Store 설계 초안

import type { Task } from './types/task';


export type TaskFilter =
  | 'all'
  | 'active'
  | 'completed';


export type TaskStore = {
  tasks: Task[];
  selectedTaskId: string | null;
  isTaskModalOpen: boolean;
  filter: TaskFilter;


  addTask: (title: string) => void;
  toggleTask: (id: string) => void;
  deleteTask: (id: string) => void;
  selectTask: (id: string | null) => void;
  openTaskModal: () => void;
  closeTaskModal: () => void;
  setFilter: (filter: TaskFilter) => void;
};

아직 구현하지 않고 타입과 책임만 정합니다.

Before

TaskContext
TaskProvider
taskReducer
dispatch

After 설계

useTaskStore
├─ tasks
├─ selectedTaskId
├─ isTaskModalOpen
├─ filter
└─ actions

현재 확장 단계

✓ 1. 전체 로드맵
✓ 2. 상태 경계 결정
→ 3. Store 구조 설계
○ 4. Zustand 설치
○ 5. state 이전
○ 6. action 이전
○ 7. selector
○ 8~15. 최적화·persist·실무 구조

완료 체크포인트

zustand-step-03-store-design

다음 학습

다음 글에서는 Zustand를 실제 설치하고 useTaskStore를 만든 뒤, 기존 Context와 공존시키면서 최소 상태 하나를 연결합니다.

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

공식 문서

글 소개·학습 준비·전체 목차
Zustand: 상태 관리 첫걸음 표지

Zustand · 3 / 15 · 필수 학습 · Store 설계

Zustand는 React에서 사용할 수 있는 상태 관리 라이브러리입니다. 핵심은 하나의 store 안에 state와 변경 함수를 정의하고, 컴포넌트가 필요한 값만 선택해서 구독하는 구조입니다.

위쪽은 작은 선택 예제로 state와 action의 관계를 익힙니다. 아래쪽은 기존 TaskFlow의 reducer + Context와 Zustand 구조를 비교하는 설계 과제입니다.

이번 글에서 배우는 내용

  • Zustand store의 기본 구조
  • state와 action의 역할
  • create 함수의 개념
  • reducer + Context와 Zustand 비교
  • store가 커지지 않게 설계하는 기준
  • TaskFlow useTaskStore 설계

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 15개 · 필수 14개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기