Zustand 高度(こうど)な活用(かつよう)とTodoリスト実装(じっそう)

🔧 Zustand ミドルウェア詳細(しょうさい)

subscribeWithSelector

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가 변경될 때마다 실행되는 콜백 함수

persist ミドルウェア

データを永続化(えいぞくか)して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),
    }
  )
);
옵션(おぷしょん)説明(せつめい)
namestorage에 저장될 키(き) 이름(めい)
partialize저장(ちょぞう)할 값(あたい) 선택(せんたく)
storagelocalStorage 또는 sessionStorage

주의: JavaScript 함수는 JSON으로 변환되지 않아 저장 안 됨 → partialize로 state만 저장

devtools ミドルウェア

開発者(かいはつしゃ)ツールでデバッグが可能(かのう)になります。
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디버깅
2persist영구 저장
3subscribeWithSelector선택적 구독
4immer불변성 관리
5 (내부)combinestate/action 분리

중요: 순서를 지키지 않으면 오류 발생 가능


📝 Todoリスト - UI実装(じっそう)

ページ構成(こうせい)

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로 페이지 확인

TodoEditor コンポーネント

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>
  );
}

TodoItem コンポーネント

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リスト - 機能実装(きのうじっそう)

ストア作成(さくせい)

全域的(ぜんいきてき)に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 추가(ついか)
deleteTodotargetId와 일치(いっち)하지 않는 Todo만 필터링(ふぃるたりんぐ)
useTodostodos 배열(はいれつ) 가져오기

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>
  );
}

TodoEditor 機能追加(きのうついか)

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>
  );
}

TodoItem 削除機能(さくじょきのう)

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>
  );
}

검증: 공백을 제거한 값이 비었는지 체크 → 추가/삭제 기능 정상 동작


🌐 サーバー状態管理(じょうたいかんり)とTanStack Query

状態(じょうたい)の分類(ぶんるい)問題(もんだい)

従来(じゅうらい)は地域状態(ちいきじょうたい)と全域状態(ぜんいきじょうたい)だけで分類(ぶんるい)していました。
ゲシムルリストやAPI要請(ようせい)のようなデータは分類(ぶんるい)があいまいです。

기존에는 지역 상태와 전역 상태만으로 분류했습니다.
게시글 리스트나 API 요청 같은 데이터는 분류가 애매합니다.

従来の分類:
- 지역 상태 (Local State): 특정 컴포넌트 내부
- 전역 상태 (Global State): 앱 전체

문제점:
- 게시글 리스트 (API 데이터)는?
- 비동기 요청 상태는?
- 너무 많은 데이터 + 복잡한 코드

サーバー状態(じょうたい)の導入(どうにゅう)

サーバー状態(じょうたい)で多様(たよう)なデータを一度(いちど)に管理(かんり)します。
TanStack Queryを使(つか)って非同期状態(ひどうきじょうたい)を便利(べんり)に管理(かんり)します。

서버 상태로 다양한 데이터를 한 번에 관리합니다.
TanStack Query를 사용해서 비동기 상태를 편리하게 관리합니다.

状態分類(じょうたいぶんるい)特徴(とくちょう)管理道具(かんりどうぐ)
지역(ちいき) 상태(じょうたい)컴포넌트 내부(ないぶ)useState, useReducer
전역(ぜんいき) 상태(じょうたい)앱 전체(ぜんたい)Zustand, Redux
서버(さーばー) 상태(じょうたい)API 데이터, 비동기(ひどうき)TanStack Query

TanStack Queryの長所(ちょうしょ)

非同期(ひどうき)状態(じょうたい)を非常(ひじょう)に便利(べんり)に管理(かんり)できます。
キャッシング、リフェッチ、ローディング状態(じょうたい)などを自動(じどう)で処理(しょり)します。

비동기 상태를 매우 편리하게 관리할 수 있습니다.
캐싱, 리페치, 로딩 상태 등을 자동으로 처리합니다.

TanStack Query 주요 기능:
✅ 자동 캐싱
✅ 백그라운드 리페칭
✅ 로딩/에러 상태 관리
✅ 데이터 동기화
✅ 무한 스크롤 지원

📋 まとめ

Zustandのミドルウェアで高度(こうど)な状態管理(じょうたいかんり)ができます。
Todoリストで実践的(じっせんてき)な実装(じっそう)経験(けいけん)を積(つ)み、TanStack Queryでサーバー状態(じょうたい)を管理(かんり)します。

Zustand 미들웨어로 고급 상태 관리를 할 수 있습니다.
Todo 리스트로 실전 경험을 쌓고, TanStack Query로 서버 상태를 관리합니다.

핵심: Zustand (클라이언트 상태) + TanStack Query (서버 상태) = 완벽한 상태 관리

profile
日本での就職を目指している26歳の韓国人開発者です。 アプリとweb開発、両方準備中で、日本語で技術概念を整理しながら日本語も一緒に勉強する予定です。 コツコツ続けるのが好きな開発者の成長記録を、一緒に見守っていただけると嬉しいです! 일본에서의 취업을 목표로 하고 있는 26살의 한국인 개발자입니다. 앱과 웹 개발, 둘 다 준비 중이며, 일본어로 기술 개념을 정리하면서 일본어도 함께 공부할 예정입니다. 꾸준히 계속하는 것을 좋아하는 개발자의 성장 기록을, 함께 지켜봐

0개의 댓글