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

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

Zustand를 설치한 뒤 첫 store와 화면 구독을 어떻게 연결하나요?

Zustand를 설치한 뒤에는 작은 store를 만들고 화면이 필요한 값을 구독하도록 연결합니다. 이전 프로젝트 없이 필터 선택값을 바꾸는 예제로 설치·state·변경 함수를 확인할 수 있습니다.

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

이 기술만 따로 실행하기

이 개념 직접 실행

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

예제 크게 보기 (새 탭)

새 React·TypeScript 연습 앱에서 먼저 아래 설치 명령을 실행합니다. 앱이 없다면 Zustand 학습 로드맵: store·action·selector·persist 순서의 React 준비 단계를 먼저 진행하세요. 기존 TaskFlow는 필요하지 않습니다.

npm install zustand@5

src/App.tsx를 아래 전체 코드로 교체합니다. Vite가 생성한 src/main.tsx는 유지합니다.

확인할 동작: 필터 설치 확인.

import { create } from 'zustand';
type Filter = 'all' | 'active' | 'completed';
type State = { filter: Filter; setFilter: (filter: Filter) => void };
const useFilter = create<State>()((set) => ({
  filter: 'all', setFilter: (filter) => set({ filter }),
}));
function FilterLabel() {
  const filter = useFilter((state) => state.filter);
  return <p>현재 필터: {filter}</p>;
}
export default function App() {
  const setFilter = useFilter((state) => state.setFilter);
  return (
    <main>
      <FilterLabel />
      <button onClick={() => setFilter('completed')}>완료만</button>
      <button onClick={() => setFilter('all')}>전체</button>
    </main>
  );
}
npm run dev
npm run build

기대 결과: 처음 all → 완료만 클릭 후 completed → 전체 클릭 후 all이 표시되어야 합니다. Provider를 추가하지 않아도 표시 컴포넌트와 조작 컴포넌트가 같은 store를 사용합니다.

자가 확인: 버튼 값을 active로 바꾸면 무엇이 표시될까요? active입니다. done은 이 예제의 Filter 타입에 없으므로 사용하지 않습니다.

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

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

설치

프로젝트에서 다음 명령을 실행합니다.

npm install zustand

설치 후 package.json의 dependencies에 zustand가 추가됐는지 확인합니다.

예시:

{
  "dependencies": {
    "react": "19.3.0",
    "react-dom": "19.3.0",
    "zustand": "5.0.15"
  }
}

버전은 설치 시점에 따라 달라질 수 있습니다.

첫 Store 만들기

가장 작은 store부터 시작합니다.

import { create } from 'zustand';


type UiStore = {
  filter: 'all' | 'active' | 'completed';
  setFilter: (
    filter: 'all' | 'active' | 'completed',
  ) => void;
};


export const useUiStore = create<UiStore>((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

컴포넌트에서는 다음처럼 읽습니다.

const filter = useUiStore((state) => state.filter);

변경 함수도 같은 방식으로 가져옵니다.

const setFilter = useUiStore((state) => state.setFilter);

selector를 사용하는 이유

store 전체를 가져오기보다 필요한 값만 선택하는 습관을 들이는 편이 좋습니다.

const filter = useUiStore((state) => state.filter);

selector 최적화는 이후 회차에서 더 자세히 다룹니다.

Context를 바로 제거하지 않는 이유

기존 TaskFlow에는 이미 동작하는 reducer + Context가 있습니다.

새 라이브러리를 도입할 때 한 번에 모든 코드를 바꾸면 문제가 생겼을 때 원인을 찾기 어렵습니다.

이번 회차에서는 다음 방식으로 진행합니다.

기존 tasks
→ Context 유지


새 filter
→ Zustand에서 관리

자주 발생하는 오류

Cannot find module ‘zustand’

현재 프로젝트에 설치됐는지 확인합니다.

npm ls zustand

import 경로 오류

기본 create는 다음처럼 가져옵니다.

import { create } from 'zustand';

타입 오류

store 타입과 실제 반환 객체의 키가 일치하는지 확인합니다.

학습 완료 체크리스트

  • Zustand를 설치할 수 있습니다.
  • create로 store를 만들 수 있습니다.
  • TypeScript로 store 타입을 정의할 수 있습니다.
  • selector로 state를 읽을 수 있습니다.
  • Context를 한 번에 제거하지 않고 점진 이전할 수 있습니다.

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

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

Zustand를 설치하고 filter 상태 하나를 useTaskStore에 연결합니다.

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

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

TaskFlow 프로젝트 적용

TaskFlow 직접 실행

Zustand와 useTaskStore·TaskFilter를 추가합니다. filter는 Zustand, tasks와 dispatch는 Context에 남습니다. 필터 표시가 바뀌고 목록도 선택 조건을 따릅니다.

예제 크게 보기 (새 탭)

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

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

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

시작본에서 직접 바꾸기

시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.

  1. package-lock.json — 기존 파일의 같은 위치를 갱신합니다.
  2. package.json — 기존 파일의 같은 위치를 갱신합니다.
  3. src/App.tsx — 기존 파일의 같은 위치를 갱신합니다.
  4. src/components/TaskFilter.tsx — 이전 단계에 없던 파일을 만듭니다.
  5. src/components/TaskList.tsx — 기존 파일의 같은 위치를 갱신합니다.
  6. src/store/useTaskStore.ts — 이전 단계에 없던 파일을 만듭니다.

변경 이유와 결과: Zustand와 useTaskStore·TaskFilter를 추가합니다. filter는 Zustand, tasks와 dispatch는 Context에 남습니다. 필터 표시가 바뀌고 목록도 선택 조건을 따릅니다.

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

src/App.tsx 전체 파일

import { useEffect } from 'react';
import Header from './components/Header';
import NetworkStatus from './components/NetworkStatus';
import StateResetDemo from './components/StateResetDemo';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import TaskFilter from './components/TaskFilter';
import { useTaskContext } from './context/TaskContext';

export default function App() {
  const { tasks } = useTaskContext();
  const remainingCount = tasks.filter((task) => !task.completed).length;
  useEffect(() => { document.title = `TaskFlow (${remainingCount})`; }, [remainingCount]);
  return (
    <main className="app">
      <Header />
      <NetworkStatus />
      <p>남은 작업: {remainingCount}</p>
      <TaskFilter />
      <TaskForm />
      <TaskList />
      <StateResetDemo />
    </main>
  );
}

src/components/TaskFilter.tsx 전체 파일

import {
  useTaskStore,
  type TaskFilter as TaskFilterValue,
} from '../store/useTaskStore';


const options: TaskFilterValue[] = [
  'all',
  'active',
  'completed',
];


export default function TaskFilter() {
  const filter = useTaskStore((state) => state.filter);
  const setFilter = useTaskStore((state) => state.setFilter);


  return (
    <section>
      <h2>필터</h2>
      <select
        value={filter}
        onChange={(event) =>
          setFilter(event.target.value as TaskFilterValue)
        }
      >
        {options.map((option) => (
          <option key={option} value={option}>
            {option}
          </option>
        ))}
      </select>
    </section>
  );
}

src/components/TaskList.tsx 전체 파일

import { useTaskContext } from '../context/TaskContext';
import { useTaskStore } from '../store/useTaskStore';
import TaskItem from './TaskItem';

export default function TaskList() {
  const { tasks: allTasks } = useTaskContext();
  const filter = useTaskStore((state) => state.filter);
  const tasks = allTasks.filter((task) => filter === 'all' || (filter === 'completed' ? task.completed : !task.completed));

  if (tasks.length === 0) {
    return <p>등록된 작업이 없습니다.</p>;
  }

  return (
    <ul>
      {tasks.map((task) => (
        <TaskItem key={task.id} task={task} />
      ))}
    </ul>
  );
}

src/store/useTaskStore.ts 전체 파일

import { create } from 'zustand';


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


type TaskStore = {
  filter: TaskFilter;
  setFilter: (filter: TaskFilter) => void;
};


export const useTaskStore = create<TaskStore>((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

완료 확인

  1. 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤 npm ci로 잠금 파일의 의존성을 설치합니다.
  2. npm run dev 실행 후 터미널의 localhost 주소를 엽니다.
  3. Zustand와 useTaskStore·TaskFilter를 추가합니다. filter는 Zustand, tasks와 dispatch는 Context에 남습니다. 필터 표시가 바뀌고 목록도 선택 조건을 따릅니다.
  4. npm run typecheck와 npm run build가 성공하면 다음 프로젝트 단계로 갑니다.

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

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

이어서 만들기: Zustand란? React 상태 관리 선택 기준과 기본 Store의 적용 결과에서 시작합니다. 아래 체크포인트 이름은 단계 식별자이며 다운로드 파일명이나 Git 명령이 아닙니다.

src/store/useTaskStore.ts를 추가하고 TaskFilter.tsx에서 값을 구독한 뒤 App.tsx의 폼 위에 TaskFilter를 렌더링합니다. tasks/dispatch는 기존 Context에 두어 필터 도입과 작업 데이터 이전을 분리합니다. 아래 App.tsx는 기존 TaskForm·TaskList가 있는 누적 프로젝트용입니다.

이전 체크포인트

zustand-step-03-store-design

현재 상태

tasks와 작업 변경은 기존 TaskContext가 담당합니다.

이번 목표

Zustand를 설치하고 filter 상태 하나를 useTaskStore에 연결합니다.

변경 파일

package.json
src/store/useTaskStore.ts
src/components/TaskFilter.tsx
src/App.tsx
전체 파일·코드 보기

src/store/useTaskStore.ts

import { create } from 'zustand';


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


type TaskStore = {
  filter: TaskFilter;
  setFilter: (filter: TaskFilter) => void;
};


export const useTaskStore = create<TaskStore>((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

src/components/TaskFilter.tsx

import {
  useTaskStore,
  type TaskFilter as TaskFilterValue,
} from '../store/useTaskStore';


const options: TaskFilterValue[] = [
  'all',
  'active',
  'completed',
];


export default function TaskFilter() {
  const filter = useTaskStore((state) => state.filter);
  const setFilter = useTaskStore((state) => state.setFilter);


  return (
    <section>
      <h2>필터</h2>
      <select
        value={filter}
        onChange={(event) =>
          setFilter(event.target.value as TaskFilterValue)
        }
      >
        {options.map((option) => (
          <option key={option} value={option}>
            {option}
          </option>
        ))}
      </select>
    </section>
  );
}

src/App.tsx

import TaskFilter from './components/TaskFilter';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';


function App() {
  return (
    <main className="app">
      <h1>TaskFlow</h1>
      <TaskFilter />
      <TaskForm />
      <TaskList />
    </main>
  );
}


export default App;

Before

모든 공유 상태
→ reducer + Context

After

tasks / dispatch
→ 기존 Context


filter
→ Zustand

실행 확인

npm run dev
npm run build

확인할 내용:

  • 필터 select 값이 바뀌는가
  • 기존 작업 추가·완료·삭제가 그대로 동작하는가
  • build가 성공하는가

현재 확장 단계

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

완료 체크포인트

zustand-step-04-install

다음 학습

다음 글에서는 selectedTaskId와 modal 같은 실제 클라이언트 전역 상태를 store로 옮기고, state를 읽고 변경하는 흐름을 익힙니다.

다음 글: Zustand state 사용법: 값 읽기와 변경 흐름 익히기

공식 문서

글 소개·학습 준비·전체 목차
Zustand: 설치와 첫 Store 표지

Zustand · 4 / 15 · 필수 학습 · 라이브러리 최초 도입

이번 글에서는 Zustand를 실제 프로젝트에 설치하고 첫 store를 만듭니다.

먼저 이전 프로젝트가 필요 없는 필터 예제로 설치와 구독을 확인합니다. 하단에서는 기존 React TaskFlow의 Context를 유지하며 store를 한 부분에 연결합니다.

현재 기준 Zustand 5를 사용합니다.

이번 글에서 배우는 내용

  • Zustand 설치
  • create로 store 만들기
  • TypeScript store 타입 작성
  • 컴포넌트에서 state 구독
  • 기존 Context와 Zustand 공존
  • 설치 후 확인해야 할 오류

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

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

이 글을 쓴 해비

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

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기