Zustand store의 state를 읽고 set으로 바꾸는 흐름은 어떻게 연결되나요?
Zustand store의 state를 selector로 읽고 set으로 바꾸면 그 값을 구독하는 화면에 변경이 전달됩니다. 별도 예제의 선택값과 클릭 횟수로 읽기와 변경을 먼저 확인합니다.
적용 조건·범위: 실행 환경은 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/guides/updating-state
이 기술만 따로 실행하기
이 개념 직접 실행
상태를 바꾸고 화면에 반영되는 결과를 확인합니다.
예제 크게 보기 (새 탭)기대 결과: 선택 → 횟수 증가 두 번 → 선택 해제 순서로 누르면 없음 / 클릭 2회가 표시되어야 합니다. selectedId만 교체할 때 clicks는 유지됩니다.
직접 실행: 환경 준비와 전체 파일
실행 환경은 React·TypeScript 앱과 Zustand 5입니다. React 함수 컴포넌트와 이벤트를 알고 있으면 이전 TaskFlow 프로젝트 없이 실습할 수 있습니다. 환경이 없다면 Zustand 학습 로드맵: store·action·selector·persist 순서의 준비 단계 또는 Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기의 설치 항목을 확인하세요. 아래는 별도 연습 앱의 src/App.tsx 전체 내용입니다. 기존 앱에서 연결한 src/main.tsx는 유지합니다.
확인할 동작: 선택값 교체와 이전 상태 갱신.
import { create } from 'zustand';
type State = {
selectedId: string | null; clicks: number;
select: (id: string | null) => void; countClick: () => void;
};
const useStateDemo = create<State>()((set) => ({
selectedId: null, clicks: 0,
select: (selectedId) => set({ selectedId }),
countClick: () => set((state) => ({ clicks: state.clicks + 1 })),
}));
export default function App() {
const selectedId = useStateDemo((state) => state.selectedId);
const clicks = useStateDemo((state) => state.clicks);
const select = useStateDemo((state) => state.select);
const countClick = useStateDemo((state) => state.countClick);
return (
<main>
<p>{selectedId ?? '없음'} / 클릭 {clicks}회</p>
<button onClick={() => select('task-1')}>선택</button>
<button onClick={countClick}>횟수 증가</button>
<button onClick={() => select(null)}>선택 해제</button>
</main>
);
}
npm run dev
npm run build
자가 확인: 선택 버튼을 두 번 누르면 clicks도 늘어날까요? 늘어나지 않습니다. select는 선택값만 갱신하고 이전 clicks에서 더하는 작업은 countClick이 담당합니다.
검증: 2026-09-28, 독립 예제는 TypeScript strict 검사와 Vite 라이브러리 빌드를 통과했습니다. jsdom에서 버튼 클릭 후 표시값을 확인했습니다. 실제 브라우저 배치와 누적 프로젝트 전체 실행은 이번 검사 범위가 아닙니다.
공식 기준: Zustand create (확인일 2026-09-28).
state 읽기
type TaskStore = {
selectedTaskId: string | null;
};
컴포넌트에서는 selector로 읽습니다.
const selectedTaskId = useTaskStore(
(state) => state.selectedTaskId,
);
set으로 상태 변경
현재 값을 그대로 교체할 때는 객체를 넘길 수 있습니다.
set({ selectedTaskId: id });
이전 state를 이용해 다음 값을 계산할 때는 함수형 업데이트를 사용합니다.
set((state) => ({
isTaskModalOpen: !state.isTaskModalOpen,
}));
실무에서는 toggle보다 openTaskModal, closeTaskModal처럼 의도가 드러나는 action이 더 읽기 좋습니다. 다음 회차에서 정리합니다.
객체와 배열 변경
배열이나 객체를 수정할 때는 새 참조를 만들어야 합니다.
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id
? { ...task, completed: !task.completed }
: task,
),
}));
같은 배열 참조를 그대로 반환하면 selector와 리렌더링 판단에서 문제가 생길 수 있습니다.
파생 값은 저장하지 않기
remainingCount처럼 tasks에서 계산 가능한 값은 별도 state로 저장하지 않는 편이 좋습니다.
const remainingCount = tasks.filter(
(task) => !task.completed,
).length;
원본과 파생 값을 동시에 저장하면 동기화할 값이 늘어납니다.
학습 완료 체크리스트
- selector로 state를 읽을 수 있습니다.
- set의 객체형과 함수형 업데이트를 구분할 수 있습니다.
- 배열과 객체를 새 참조로 업데이트할 수 있습니다.
- 계산 가능한 값을 불필요하게 store에 저장하지 않을 수 있습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 여러 화면이 공유하는 상태를 관리합니다.
selectedTaskId와 isTaskModalOpen을 Zustand로 옮깁니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
TaskFlow 직접 실행
선택 ID·모달 열림 값을 Store로 이동하고 TaskSelection·TaskModalControls를 연결합니다. tasks는 여전히 Context가 소유합니다.
예제 크게 보기 (새 탭)중간부터 시작하는 전체 실습 파일
기존 React Context TaskFlow의 작업 목록·추가·완료·삭제 화면을 이어서 확장합니다.
이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.
시작본에서 직접 바꾸기
시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.
src/App.tsx— 기존 파일의 같은 위치를 갱신합니다.src/components/TaskModalControls.tsx— 이전 단계에 없던 파일을 만듭니다.src/components/TaskSelection.tsx— 이전 단계에 없던 파일을 만듭니다.src/store/useTaskStore.ts— 기존 파일의 같은 위치를 갱신합니다.
변경 이유와 결과: 선택 ID·모달 열림 값을 Store로 이동하고 TaskSelection·TaskModalControls를 연결합니다. tasks는 여전히 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';
import TaskSelection from './components/TaskSelection';
import TaskModalControls from './components/TaskModalControls';
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 />
<TaskSelection />
<TaskModalControls />
<StateResetDemo />
</main>
);
}
src/components/TaskModalControls.tsx 전체 파일
import { useTaskStore } from '../store/useTaskStore';
export default function TaskModalControls() {
const isOpen = useTaskStore(
(state) => state.isTaskModalOpen,
);
const setTaskModalOpen = useTaskStore(
(state) => state.setTaskModalOpen,
);
return (
<section>
<h2>모달 상태</h2>
<p>{isOpen ? '열림' : '닫힘'}</p>
<button
type="button"
onClick={() => setTaskModalOpen(true)}
>
열기
</button>
<button
type="button"
onClick={() => setTaskModalOpen(false)}
>
닫기
</button>
</section>
);
}
src/components/TaskSelection.tsx 전체 파일
import { useTaskStore } from '../store/useTaskStore';
export default function TaskSelection() {
const selectedTaskId = useTaskStore(
(state) => state.selectedTaskId,
);
const setSelectedTaskId = useTaskStore(
(state) => state.setSelectedTaskId,
);
return (
<section>
<h2>선택 상태</h2>
<p>선택한 작업: {selectedTaskId ?? '없음'}</p>
<button
type="button"
onClick={() => setSelectedTaskId('example-task')}
>
예제 작업 선택
</button>
<button
type="button"
onClick={() => setSelectedTaskId(null)}
>
선택 해제
</button>
</section>
);
}
src/store/useTaskStore.ts 전체 파일
import { create } from 'zustand';
export type TaskFilter =
| 'all'
| 'active'
| 'completed';
type TaskStore = {
filter: TaskFilter;
selectedTaskId: string | null;
isTaskModalOpen: boolean;
setFilter: (filter: TaskFilter) => void;
setSelectedTaskId: (id: string | null) => void;
setTaskModalOpen: (isOpen: boolean) => void;
};
export const useTaskStore = create<TaskStore>((set) => ({
filter: 'all',
selectedTaskId: null,
isTaskModalOpen: false,
setFilter: (filter) => set({ filter }),
setSelectedTaskId: (selectedTaskId) =>
set({ selectedTaskId }),
setTaskModalOpen: (isTaskModalOpen) =>
set({ isTaskModalOpen }),
}));
완료 확인
- 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤
npm ci로 잠금 파일의 의존성을 설치합니다. npm run dev실행 후 터미널의 localhost 주소를 엽니다.- 선택 ID·모달 열림 값을 Store로 이동하고 TaskSelection·TaskModalControls를 연결합니다. tasks는 여전히 Context가 소유합니다.
npm run typecheck와npm run build가 성공하면 다음 프로젝트 단계로 갑니다.
이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.
아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.
이어서 만들기: Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기의 적용 결과에서 시작합니다. 아래 체크포인트 이름은 단계 식별자이며 다운로드 파일명이나 Git 명령이 아닙니다.
src/store/useTaskStore.ts에 selectedTaskId와 isTaskModalOpen 및 변경 함수를 추가합니다. 기존 filter와 setFilter는 보존합니다. 아래 TaskSelection·TaskModalControls를 App.tsx 안에 각각 렌더링하고 선택과 모달 조작 후 기존 필터가 유지되는지 확인합니다.
이전 체크포인트
zustand-step-04-install
현재 상태
filter만 Zustand에 있고 tasks는 Context에 있습니다.
이번 목표
selectedTaskId와 isTaskModalOpen을 Zustand로 옮깁니다.
전체 파일·코드 보기
src/store/useTaskStore.ts
import { create } from 'zustand';
export type TaskFilter =
| 'all'
| 'active'
| 'completed';
type TaskStore = {
filter: TaskFilter;
selectedTaskId: string | null;
isTaskModalOpen: boolean;
setFilter: (filter: TaskFilter) => void;
setSelectedTaskId: (id: string | null) => void;
setTaskModalOpen: (isOpen: boolean) => void;
};
export const useTaskStore = create<TaskStore>((set) => ({
filter: 'all',
selectedTaskId: null,
isTaskModalOpen: false,
setFilter: (filter) => set({ filter }),
setSelectedTaskId: (selectedTaskId) =>
set({ selectedTaskId }),
setTaskModalOpen: (isTaskModalOpen) =>
set({ isTaskModalOpen }),
}));
src/components/TaskSelection.tsx
import { useTaskStore } from '../store/useTaskStore';
export default function TaskSelection() {
const selectedTaskId = useTaskStore(
(state) => state.selectedTaskId,
);
const setSelectedTaskId = useTaskStore(
(state) => state.setSelectedTaskId,
);
return (
<section>
<h2>선택 상태</h2>
<p>선택한 작업: {selectedTaskId ?? '없음'}</p>
<button
type="button"
onClick={() => setSelectedTaskId('example-task')}
>
예제 작업 선택
</button>
<button
type="button"
onClick={() => setSelectedTaskId(null)}
>
선택 해제
</button>
</section>
);
}
src/components/TaskModalControls.tsx
import { useTaskStore } from '../store/useTaskStore';
export default function TaskModalControls() {
const isOpen = useTaskStore(
(state) => state.isTaskModalOpen,
);
const setTaskModalOpen = useTaskStore(
(state) => state.setTaskModalOpen,
);
return (
<section>
<h2>모달 상태</h2>
<p>{isOpen ? '열림' : '닫힘'}</p>
<button
type="button"
onClick={() => setTaskModalOpen(true)}
>
열기
</button>
<button
type="button"
onClick={() => setTaskModalOpen(false)}
>
닫기
</button>
</section>
);
}
Before / After
Before
filter만 Zustand
After
filter
selectedTaskId
isTaskModalOpen
→ Zustand
tasks
→ 아직 Context
실행 확인
npm run dev
npm run build
- 작업 선택 버튼으로 selectedTaskId가 바뀌는지 확인합니다.
- 선택 해제가 동작하는지 확인합니다.
- modal 상태가 열림/닫힘으로 바뀌는지 확인합니다.
현재 확장 단계
✓ 1. 전체 로드맵
✓ 2. 상태 경계 결정
✓ 3. Store 구조 설계
✓ 4. Zustand 설치
→ 5. state 이전
○ 6. action 이전
○ 7. selector
○ 8~15. 최적화·persist·실무 구조
완료 체크포인트
zustand-step-05-state
다음 학습
다음 글에서는 단순 setter를 그대로 노출하지 않고 selectTask, openTaskModal, closeTaskModal처럼 상태 변경 의도를 action으로 표현합니다.
다음 글: Zustand action 사용법: 상태 변경 로직을 store로 분리하기
공식 문서
- Zustand 공식 문서: https://zustand.docs.pmnd.rs/
- Updating state: https://zustand.docs.pmnd.rs/guides/updating-state
글 소개·학습 준비·전체 목차

Zustand · 5 / 15 · 필수 학습 · state
Zustand를 설치했다면 다음 단계는 store의 state를 읽고 변경하는 흐름을 이해하는 것입니다.
상단에서는 독립적인 선택값과 클릭 횟수로 state를 읽고 변경합니다. 하단에서 이어 만드는 독자는 TaskFlow의 selectedTaskId와 modal 상태를 Zustand로 옮깁니다. action 설계는 다음 회차에서 더 정리하고, 이번에는 state와 set의 기본 동작에 집중합니다.
이번 글에서 배우는 내용
- Zustand state 읽기
- selector로 필요한 값 선택
- set으로 상태 변경
- 이전 state를 이용한 업데이트
- 객체·배열 참조를 바꿔야 하는 이유
- TaskFlow 선택 상태와 modal 이전
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.