
useEffectのように変化(へんか)を検知(けんち)して選択(せんたく)します。
ストア内部(ないぶ)を購読(こうどく)し、変化(へんか)があるたびにコールバックを実行(じっこう)します。
useEffect처럼 변화를 감지해서 선택합니다.
스토어 내부를 구독하고 변화가 있을 때마다 콜백을 실행합니다.
import { subscribeWithSelector } from "zustand/middleware";
export const useCountStore = create(
subscribeWithSelector(
// ... store 설정
)
);
// 사용 예시
useCountStore.subscribe(
(store) => store.count, // 구독할 값 선택
(count, prevCount) => {
// Listener: 변경될 때마다 실행
console.log("현재:", count, "이전:", prevCount);
}
);
Listener: count가 변경될 때마다 실행되는 콜백 함수
データを永続化(えいぞくか)してlocalStorageまたはsessionStorageに保存(ほぞん)します。
partializeを使(つか)って保存(ほぞん)する値(あたい)を選択(せんたく)できます。
데이터를 영구 저장해서 localStorage 또는 sessionStorage에 보관합니다.
partialize를 사용해서 저장할 값을 선택할 수 있습니다.
import { persist, createJSONStorage } from "zustand/middleware";
export const useCountStore = create(
persist(
// ... store 설정
{
name: "countStore", // storage key 이름
partialize: (store) => ({
count: store.count, // count만 저장
}),
storage: createJSONStorage(() => sessionStorage),
}
)
);
| 옵션(おぷしょん) | 説明(せつめい) |
|---|---|
name | storage에 저장될 키(き) 이름(めい) |
partialize | 저장(ちょぞう)할 값(あたい) 선택(せんたく) |
storage | localStorage 또는 sessionStorage |
주의: JavaScript 함수는 JSON으로 변환되지 않아 저장 안 됨 → partialize로 state만 저장
開発者(かいはつしゃ)ツールでデバッグが可能(かのう)になります。
Redux DevToolsをインストールして使用(しよう)します。
개발자 도구에서 디버깅이 가능해집니다.
Redux DevTools를 설치해서 사용합니다.
import { devtools } from "zustand/middleware";
export const useCountStore = create(
devtools(
// ... store 설정
{
name: "countStore", // DevTools에 표시될 이름
}
)
);
Redux DevTools: Chrome 확장 프로그램 설치 → Redux 탭에서 상태 변화 추적
ミドルウェアを適用(てきよう)する順序(じゅんじょ)が重要(じゅうよう)です。
外側(そとがわ)から内側(うちがわ)へ、devtools → persist → subscribeWithSelector → immer → combine
미들웨어를 적용하는 순서가 중요합니다.
바깥쪽에서 안쪽으로, devtools → persist → subscribeWithSelector → immer → combine
store/count.ts
import { create } from "zustand";
import {
combine,
subscribeWithSelector,
persist,
createJSONStorage,
devtools,
} from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
export const useCountStore = create(
devtools(
persist(
subscribeWithSelector(
immer(
combine({ count: 0 }, (set, get) => ({
actions: {
increaseOne: () => {
set((state) => {
state.count += 1;
});
},
decreaseOne: () => {
set((state) => {
state.count -= 1;
});
},
},
})),
),
),
{
name: "countStore",
partialize: (store) => ({
count: store.count,
}),
storage: createJSONStorage(() => sessionStorage),
},
),
{
name: "countStore",
},
),
);
// subscribe 사용 예시
useCountStore.subscribe(
(store) => store.count,
(count, prevCount) => {
console.log(count, prevCount);
// 현재 스토어 값 가져오기
const store = useCountStore.getState();
// 스토어 값 업데이트
// useCountStore.setState((store) => ({ ... }))
},
);
export const useCount = () => {
const count = useCountStore((store) => store.count);
return count;
};
export const useIncreaseCount = () => {
const increase = useCountStore((store) => store.actions.increaseOne);
return increase;
};
export const useDecreaseCount = () => {
const decrease = useCountStore((store) => store.actions.decreaseOne);
return decrease;
};
| 順序(じゅんじょ) | ミドルウェア | 役割(やくわり) |
|---|---|---|
| 1 (외부) | devtools | 디버깅 |
| 2 | persist | 영구 저장 |
| 3 | subscribeWithSelector | 선택적 구독 |
| 4 | immer | 불변성 관리 |
| 5 (내부) | combine | state/action 분리 |
중요: 순서를 지키지 않으면 오류 발생 가능
Todoリストページのレイアウトを作成(さくせい)します。
TodoEditorとTodoItemコンポーネントを配置(はいち)します。
Todo 리스트 페이지의 레이아웃을 작성합니다.
TodoEditor와 TodoItem 컴포넌트를 배치합니다.
pages/todo-list-page.tsx
import TodoEditor from "@/components/todo-list/todo-editor";
import TodoItem from "@/components/todo-list/todo-item";
const dummyTodos = [
{ id: 1, content: "Todo 1" },
{ id: 2, content: "Todo 2" },
{ id: 3, content: "Todo 3" },
];
export default function TodoListPage() {
return (
<div className="flex flex-col gap-5 p-5">
<h1 className="text-2xl font-bold">TodoList</h1>
<TodoEditor />
{dummyTodos.map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</div>
);
}
App.tsx
import { Outlet, Route, Routes } from "react-router";
import "./App.css";
import CounterPage from "./pages/counter-page";
import TodoListPage from "@/pages/todo-list-page";
function App() {
return (
<Routes>
<Route path="/" element={<IndexPage />} />
<Route path="/counter" element={<CounterPage />} />
<Route path="/todolist" element={<TodoListPage />} />
<Route element={<AuthLayout />}>
<Route path="/sign-in" element={<SignInPage />} />
<Route path="/sign-up" element={<SignUpPage />} />
</Route>
</Routes>
);
}
export default App;
접근:
localhost:3000/todolist로 페이지 확인
components/todo-list/todo-editor.tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export default function TodoEditor() {
return (
<div className="flex gap-2">
<Input placeholder="새로운 할 일을 입력하세요 ..." />
<Button>추가</Button>
</div>
);
}
components/todo-list/todo-item.tsx
import { Button } from "@/components/ui/button";
export default function TodoItem({
id,
content,
}: {
id: number;
content: string;
}) {
return (
<div className="flex items-center justify-between border p-2">
{content}
<Button variant={"destructive"}>삭제</Button>
</div>
);
}
全域的(ぜんいきてき)にTodoを管理(かんり)するストアを作成(さくせい)します。
createTodoとdeleteTodo機能(きのう)を実装(じっそう)します。
전역적으로 Todo를 관리하는 스토어를 생성합니다.
createTodo와 deleteTodo 기능을 구현합니다.
types.ts
export interface Todo {
id: number;
content: string;
}
store/todos.ts
import { create } from "zustand";
import { combine } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import type { Todo } from "@/types";
const initialState: {
todos: Todo[];
} = {
todos: [],
};
const useTodosStore = create(
immer(
combine(initialState, (set) => ({
actions: {
createTodo: (content: string) => {
set((state) => {
state.todos.push({
id: new Date().getTime(),
content: content,
});
});
},
deleteTodo: (targetId: number) => {
set((state) => {
state.todos = state.todos.filter((todo) => todo.id !== targetId);
});
},
},
})),
),
);
// 커스텀 훅
export const useTodos = () => {
const todos = useTodosStore((store) => store.todos);
return todos;
};
export const useCreateTodo = () => {
const createTodo = useTodosStore((store) => store.actions.createTodo);
return createTodo;
};
export const useDeleteTodo = () => {
const deleteTodo = useTodosStore((store) => store.actions.deleteTodo);
return deleteTodo;
};
| 機能(きのう) | 説明(せつめい) |
|---|---|
createTodo | 새(あたら)로운 Todo 추가(ついか) |
deleteTodo | targetId와 일치(いっち)하지 않는 Todo만 필터링(ふぃるたりんぐ) |
useTodos | todos 배열(はいれつ) 가져오기 |
ID 생성:
new Date().getTime()으로 고유 ID 생성
pages/todo-list-page.tsx
import TodoEditor from "@/components/todo-list/todo-editor";
import TodoItem from "@/components/todo-list/todo-item";
import { useTodos } from "@/store/todos";
export default function TodoListPage() {
const todos = useTodos();
return (
<div className="flex flex-col gap-5 p-5">
<h1 className="text-2xl font-bold">TodoList</h1>
<TodoEditor />
{todos.map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</div>
);
}
components/todo-list/todo-editor.tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useCreateTodo } from "@/store/todos";
import { useState } from "react";
export default function TodoEditor() {
const [content, setContent] = useState("");
const createTodo = useCreateTodo();
const handleCreateClick = () => {
if (content.trim() === "") return; // 공백 체크
createTodo(content);
setContent(""); // 입력 초기화
};
return (
<div className="flex gap-2">
<Input
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="새로운 할 일을 입력하세요 ..."
/>
<Button onClick={handleCreateClick}>추가</Button>
</div>
);
}
components/todo-list/todo-item.tsx
import { Button } from "@/components/ui/button";
import { useDeleteTodo } from "@/store/todos";
export default function TodoItem({
id,
content,
}: {
id: number;
content: string;
}) {
const deleteTodo = useDeleteTodo();
const handleDeleteClick = () => {
deleteTodo(id);
};
return (
<div className="flex items-center justify-between border p-2">
{content}
<Button onClick={handleDeleteClick} variant={"destructive"}>
삭제
</Button>
</div>
);
}
검증: 공백을 제거한 값이 비었는지 체크 → 추가/삭제 기능 정상 동작
従来(じゅうらい)は地域状態(ちいきじょうたい)と全域状態(ぜんいきじょうたい)だけで分類(ぶんるい)していました。
ゲシムルリストやAPI要請(ようせい)のようなデータは分類(ぶんるい)があいまいです。
기존에는 지역 상태와 전역 상태만으로 분류했습니다.
게시글 리스트나 API 요청 같은 데이터는 분류가 애매합니다.
従来の分類:
- 지역 상태 (Local State): 특정 컴포넌트 내부
- 전역 상태 (Global State): 앱 전체
문제점:
- 게시글 리스트 (API 데이터)는?
- 비동기 요청 상태는?
- 너무 많은 데이터 + 복잡한 코드
サーバー状態(じょうたい)で多様(たよう)なデータを一度(いちど)に管理(かんり)します。
TanStack Queryを使(つか)って非同期状態(ひどうきじょうたい)を便利(べんり)に管理(かんり)します。
서버 상태로 다양한 데이터를 한 번에 관리합니다.
TanStack Query를 사용해서 비동기 상태를 편리하게 관리합니다.
| 状態分類(じょうたいぶんるい) | 特徴(とくちょう) | 管理道具(かんりどうぐ) |
|---|---|---|
| 지역(ちいき) 상태(じょうたい) | 컴포넌트 내부(ないぶ) | useState, useReducer |
| 전역(ぜんいき) 상태(じょうたい) | 앱 전체(ぜんたい) | Zustand, Redux |
| 서버(さーばー) 상태(じょうたい) | API 데이터, 비동기(ひどうき) | TanStack Query |
非同期(ひどうき)状態(じょうたい)を非常(ひじょう)に便利(べんり)に管理(かんり)できます。
キャッシング、リフェッチ、ローディング状態(じょうたい)などを自動(じどう)で処理(しょり)します。
비동기 상태를 매우 편리하게 관리할 수 있습니다.
캐싱, 리페치, 로딩 상태 등을 자동으로 처리합니다.
TanStack Query 주요 기능:
✅ 자동 캐싱
✅ 백그라운드 리페칭
✅ 로딩/에러 상태 관리
✅ 데이터 동기화
✅ 무한 스크롤 지원
Zustandのミドルウェアで高度(こうど)な状態管理(じょうたいかんり)ができます。
Todoリストで実践的(じっせんてき)な実装(じっそう)経験(けいけん)を積(つ)み、TanStack Queryでサーバー状態(じょうたい)を管理(かんり)します。
Zustand 미들웨어로 고급 상태 관리를 할 수 있습니다.
Todo 리스트로 실전 경험을 쌓고, TanStack Query로 서버 상태를 관리합니다.
핵심: Zustand (클라이언트 상태) + TanStack Query (서버 상태) = 완벽한 상태 관리