Zustand persist로 새로고침 뒤 필요한 상태를 어떻게 복원하나요?
Zustand state는 기본적으로 메모리에 있어 새로고침하면 초기값으로 돌아갑니다. persist를 사용하면 필요한 일부 값을 localStorage 등에 저장하고 다음 실행 때 복원할 수 있습니다.
적용 조건·범위: persist 대상은 "사용자가 다음 방문에도 기대하는 상태인가?"를 기준으로 판단합니다.
본문 근거: zustand.docs.pmnd.rs/integrations/persisting-store-data · zustand.docs.pmnd.rs/middlewares/persist
이 기술만 따로 실행하기
이 개념 직접 실행
상태를 바꾸고 화면에 반영되는 결과를 확인합니다.
예제 크게 보기 (새 탭)실행 환경: Zustand 5가 설치된 별도 React·TypeScript 연습 앱입니다. 환경 준비는 Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기를 참고하세요. 기존 TaskFlow 데이터는 사용하지 않습니다. 아래 전체 코드를 src/App.tsx에 저장하고 기존 src/main.tsx의 App 연결을 유지합니다.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type State = {
filter: string;
isOpen: boolean;
changeFilter: () => void;
open: () => void;
};
const useDemo = create<State>()(persist((set) => ({
filter: 'all',
isOpen: false,
changeFilter: () => set({ filter: 'completed' }),
open: () => set({ isOpen: true }),
}), {
name: 'blogflow-independent-persist-demo',
partialize: (state) => ({ filter: state.filter }),
}));
export default function App() {
const filter = useDemo((state) => state.filter);
const isOpen = useDemo((state) => state.isOpen);
const changeFilter = useDemo((state) => state.changeFilter);
const open = useDemo((state) => state.open);
return (
<main>
<p>필터: {filter} / 모달: {isOpen ? '열림' : '닫힘'}</p>
<button onClick={changeFilter}>완료 필터</button>
<button onClick={open}>모달 열림 상태</button>
</main>
);
}
npm run dev
npm run build
확인 순서: 완료 필터 → 모달 열림 상태 → 브라우저 새로고침
기대 결과: 새로고침 전에는 completed/열림, 이후에는 completed/닫힘을 기대합니다. Application의 독립 예제 전용 저장 키에는 filter만 있어야 합니다. 이미 실행한 키가 있으면 초기값 all 대신 저장값에서 시작할 수 있습니다.
자가 확인: partialize에 isOpen을 추가하면 다음 방문의 초기 화면이 왜 달라질까요? 사용자가 다시 기대하는 값인지에 따라 저장 여부를 선택합니다.
검증: 2026-09-28, TypeScript strict 검사와 Vite 라이브러리 빌드, jsdom 버튼 조작 후 표시값 검사를 통과했습니다. 실제 브라우저 배치와 누적 프로젝트 전체 실행은 확인 범위 밖입니다.
기본 persist
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type UiStore = {
filter: 'all' | 'active' | 'completed';
};
export const useUiStore = create<UiStore>()(
persist(
() => ({
filter: 'all',
}),
{
name: 'taskflow-ui',
},
),
);
이제 filter는 브라우저 저장소에 기록되고 새로고침 후 복원됩니다.
왜 모든 state를 저장하면 안 되는가
다음 값은 보통 persist 대상이 아닙니다.
isTaskModalOpen
현재 hover 상태
일시적 loading 상태
서버 API 응답 원본
짧은 생명주기의 form 입력값
예를 들어 modal이 열린 상태를 저장하면 페이지를 다시 열었을 때 이유 없이 modal이 열린 채 시작할 수 있습니다.
partialize로 저장 범위 제한
store 안에 여러 state가 있어도 일부만 저장할 수 있습니다.
persist(
(set) => ({
filter: 'all',
selectedTaskId: null,
isTaskModalOpen: false,
}),
{
name: 'taskflow-ui',
partialize: (state) => ({
filter: state.filter,
selectedTaskId: state.selectedTaskId,
}),
},
)
persist 대상은 “사용자가 다음 방문에도 기대하는 상태인가?”를 기준으로 판단합니다.
tasks를 저장해도 되는가
현재 TaskFlow의 tasks가 브라우저 안에서만 존재하는 로컬 학습 데이터라면 persist로 저장하는 예제를 만들 수 있습니다.
하지만 실제 서비스에서 tasks가 서버 API의 원본이라면 Zustand persist에 복제하지 않는 편이 좋습니다.
서버 원본 tasks
→ TanStack Query
filter, selectedTaskId, UI preference
→ Zustand persist 가능
저장소 이름
name은 localStorage key가 됩니다.
{
name: 'taskflow-ui',
}
프로젝트 내에서 충돌하지 않는 이름을 사용합니다.
초기화와 clearStorage
로그아웃이나 설정 초기화처럼 저장값을 지워야 하는 상황이 있습니다.
persist API를 이용해 storage를 비울 수 있습니다.
useTaskStore.persist.clearStorage();
state 메모리 값까지 초기화해야 한다면 별도의 reset action을 함께 설계하는 편이 명확합니다.
학습 완료 체크리스트
- persist middleware를 store에 적용할 수 있습니다.
- 저장할 state와 저장하지 않을 state를 구분할 수 있습니다.
- partialize로 저장 범위를 제한할 수 있습니다.
- 서버 원본 데이터를 Zustand persist에 중복 저장하지 않아야 하는 이유를 설명할 수 있습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 여러 화면이 공유하는 상태를 관리합니다.
TaskFlow의 filter와 selectedTaskId만 저장합니다. isTaskModalOpen은 새로고침 후 복원할 이유가 없으므로 제외합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
TaskFlow 직접 실행
persist로 filter·selectedTaskId만 저장합니다. 모달·임시 입력·tasks는 저장하지 않습니다. 완료 필터 뒤 새로고침하면 필터는 유지되며 메모리 작업은 초기화됩니다.
예제 크게 보기 (새 탭)중간부터 시작하는 전체 실습 파일
기존 React Context TaskFlow의 작업 목록·추가·완료·삭제 화면을 이어서 확장합니다.
이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.
시작본에서 직접 바꾸기
시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.
src/store/useTaskStore.ts— 기존 파일의 같은 위치를 갱신합니다.
변경 이유와 결과: persist로 filter·selectedTaskId만 저장합니다. 모달·임시 입력·tasks는 저장하지 않습니다. 완료 필터 뒤 새로고침하면 필터는 유지되며 메모리 작업은 초기화됩니다.
완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.
src/store/useTaskStore.ts 전체 파일
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type Task = {
id: string;
title: string;
completed: boolean;
};
export type TaskFilter =
| 'all'
| 'active'
| 'completed';
type TaskStore = {
tasks: Task[];
filter: TaskFilter;
selectedTaskId: string | null;
isTaskModalOpen: boolean;
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;
};
export const useTaskStore = create<TaskStore>()(
persist(
(set) => ({
tasks: [],
filter: 'all',
selectedTaskId: null,
isTaskModalOpen: false,
addTask: (title) =>
set((state) => ({
tasks: [
...state.tasks,
{
id: crypto.randomUUID(),
title,
completed: false,
},
],
})),
toggleTask: (id) =>
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id
? {
...task,
completed: !task.completed,
}
: task,
),
})),
deleteTask: (id) =>
set((state) => ({
tasks: state.tasks.filter(
(task) => task.id !== id,
),
})),
selectTask: (selectedTaskId) =>
set({ selectedTaskId }),
openTaskModal: () =>
set({ isTaskModalOpen: true }),
closeTaskModal: () =>
set({ isTaskModalOpen: false }),
setFilter: (filter) =>
set({ filter }),
}),
{
name: 'taskflow-ui',
partialize: (state) => ({
filter: state.filter,
selectedTaskId: state.selectedTaskId,
}),
},
),
);
완료 확인
- 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤
npm ci로 잠금 파일의 의존성을 설치합니다. npm run dev실행 후 터미널의 localhost 주소를 엽니다.- persist로 filter·selectedTaskId만 저장합니다. 모달·임시 입력·tasks는 저장하지 않습니다. 완료 필터 뒤 새로고침하면 필터는 유지되며 메모리 작업은 초기화됩니다.
npm run typecheck와npm run build가 성공하면 다음 프로젝트 단계로 갑니다.
이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.
아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.
누적 실습은 이 절의 프로젝트 단계 링크를 따라 진행합니다. 페이지 끝 학습 순서는 기술 주제 읽기 순서입니다.
이전 프로젝트 단계: Zustand 상태 변경 후 리렌더링이 안 될 때 해결 방법의 적용 결과에서 시작합니다. 아래 체크포인트 이름은 단계 식별자이며 다운로드 파일명이나 Git 명령이 아닙니다.
src/store/useTaskStore.ts의 persist 옵션에 filter와 selectedTaskId만 선택합니다. isTaskModalOpen과 임시 입력은 제외합니다. 기존 프로젝트 키 taskflow-ui와 위 독립 실습 키를 혼용하지 않습니다.
이전 체크포인트
zustand-step-09-rerender-fix
이번 목표
TaskFlow의 filter와 selectedTaskId만 저장합니다.
isTaskModalOpen은 새로고침 후 복원할 이유가 없으므로 제외합니다.
전체 파일·코드 보기
src/store/useTaskStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type Task = {
id: string;
title: string;
completed: boolean;
};
export type TaskFilter =
| 'all'
| 'active'
| 'completed';
type TaskStore = {
tasks: Task[];
filter: TaskFilter;
selectedTaskId: string | null;
isTaskModalOpen: boolean;
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;
};
export const useTaskStore = create<TaskStore>()(
persist(
(set) => ({
tasks: [],
filter: 'all',
selectedTaskId: null,
isTaskModalOpen: false,
addTask: (title) =>
set((state) => ({
tasks: [
...state.tasks,
{
id: crypto.randomUUID(),
title,
completed: false,
},
],
})),
toggleTask: (id) =>
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id
? {
...task,
completed: !task.completed,
}
: task,
),
})),
deleteTask: (id) =>
set((state) => ({
tasks: state.tasks.filter(
(task) => task.id !== id,
),
})),
selectTask: (selectedTaskId) =>
set({ selectedTaskId }),
openTaskModal: () =>
set({ isTaskModalOpen: true }),
closeTaskModal: () =>
set({ isTaskModalOpen: false }),
setFilter: (filter) =>
set({ filter }),
}),
{
name: 'taskflow-ui',
partialize: (state) => ({
filter: state.filter,
selectedTaskId: state.selectedTaskId,
}),
},
),
);
실행 확인
- filter를 completed로 바꿉니다.
- selectedTaskId를 선택합니다.
- modal을 엽니다.
- 브라우저를 새로고침합니다.
기대 결과:
filter
→ 유지
selectedTaskId
→ 유지
isTaskModalOpen
→ false로 초기화
개발자 도구의 Application → Local Storage에서도 저장값을 확인할 수 있습니다.
npm run build
현재 확장 단계
✓ 1~9. Store·selector·리렌더링
→ 10. persist
○ 11. combine / immer
○ 12. subscribeWithSelector / devtools
○ 13. migration
○ 14. 실무 설계 점검
○ 15. 최종 통합
완료 체크포인트
zustand-step-10-persist
다음 학습
다음 글에서는 store가 커졌을 때 combine과 immer를 무조건 붙이지 않고, 타입 추론과 중첩 상태 구조를 기준으로 도입 여부를 판단합니다.
다음 프로젝트 단계: Zustand combine·immer 실습: 타입 추론과 중첩 상태 불변성
공식 문서
- Persisting store data: https://zustand.docs.pmnd.rs/integrations/persisting-store-data
- Middleware: https://zustand.docs.pmnd.rs/middlewares/persist
글 소개·학습 준비·전체 목차

Zustand · 10 / 15 · 필수 학습 · persist
Zustand state는 기본적으로 메모리에만 존재합니다. 브라우저를 새로고침하면 초기값으로 돌아갑니다.
persist middleware를 사용하면 일부 상태를 localStorage 같은 저장소에 보관하고 다음 실행에서 복원할 수 있습니다. 중요한 점은 모든 state를 저장하는 것이 아니라 “새로고침 후에도 유지할 가치가 있는 상태”만 고르는 것입니다.
이번 글에서 배우는 내용
- persist middleware 기본 사용법
- localStorage 저장 구조
- partialize로 저장 범위 제한
- 일시적 UI 상태를 저장하지 않는 기준
- 서버 데이터는 persist하지 않는 이유
- TaskFlow filter와 UI 설정 저장
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.