React

[React/리액트] 상태 관리 추천 / Zustand 사용법

solfa 2024. 8. 3. 00:19

현재 개발중인 프로젝트에서 상태관리로 zustand를 사용하기로 했다.

recoil충이었던 내가 zustand를?!!

익숙해지기 위해 이론과 간단한 todo list를 만들어서 직접 사용해보자!

왜 이렇게까지 하냐면,,, 코드 잠깐 봤는데 한 번에 이해가 안돼서 직접 써봐야겠어서,,, 핳

머리가 나쁘면 몸이 고생한다는 걸 하 머래 난 말이 너무 많아

그냥 빨리 해보자


Zustand 관련 내용 정리

Zustand란?

- React의 상태 관리 라이브러리이다. Zustand는 독일어로 "상태"를 의미해서 이런 이름이 붙었다고 한다

 

주요 특징

1. API 설정이 간단하다.

2. 경량 라이브러리이다.

3.  Redux 미들웨어와 유사하게 미들웨어를 사용할 수 있다.

 

이왕 정리하는 김에 정리해본...

주요 상태관리 도구들 특징 비교

zustand가 가장 가볍고 배우기 쉬운 느낌이라 팀원분도 이 걸 쓰자고 하셨다!

과연 그럴까... 직접 써보는 시간을 가져보겠음


Zustand 기본 사용 방법

0. 자주 사용하는 함수 정리

 

- create

Zustand 스토어를 생성하는 데 사용되는 함수!

상태와 상태 업데이트 함수(콜백 함수)를 인자로 받는다.

 

사용 예시

import create from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));



- set

상태를 업데이트하는 함수!

객체를 반환하는 콜백 함수를 인자로 받아 상태를 업데이트한다.

사용 예시

const useStore = create((set) => ({
  todos: [],
  addTodo: (title) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), title, completed: false }]
  }))
}));



- get

현재 상태를 가져오는 함수!

현재 상태 객체를 반환한다.

 

사용 예시

const useStore = create((set, get) => ({
  count: 0,
  increment: () => set({ count: get().count + 1 }),
}));


- subscribe

상태 변화에 대한 구독을 설정하는 함수!

특정 상태가 변경될 때 콜백 함수가 호출된다. 이게 좀 신기함

실시간 알림 같은 거 구현할 때 좋을 듯 하다

 

사용 예시

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

const unsubscribe = useStore.subscribe(
  (count) => console.log('Count changed to', count),
  (state) => state.count
);


- destroy

스토어를 해제하고 모든 구독을 취소하는 함수!

컴포넌트가 언마운트될 때 리소스를 해제하기 위해 사용된다.

사용 예시

const useStore = create((set) => ({
  count: 0,
}));

// 스토어를 해제할 때 호출
useStore.destroy();


- persist
상태를 로컬 스토리지에 저장하고 프로그램이 다시 로드될 때 해당 상태를 복원하는 미들웨어!

사용 예시

import create from 'zustand';
import { persist } from 'zustand/middleware';

const useStore = create(persist(
  (set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }),
  { name: 'count-storage' }
));



- combine

여러 상태와 액션을 하나의 스토어로 결합하는 유틸리티 함수!

여러 상태 조각을 하나의 스토어로 결합할 때 쓴다. atomfamily같은 느낌

사용 예시

import create from 'zustand';
import { combine } from 'zustand/middleware';

const useStore = create(combine(
  { count: 0 },
  (set) => ({
    increment: () => set((state) => ({ count: state.count + 1 })),
  })
));



- devtools

Redux DevTools와 통합하여 상태를 디버깅할 수 있게 해주는 미들웨어이다.

사용 예시

import create from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(devtools(
  (set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  })
));



- createStore

스토어를 외부에서 생성하고 `Provider`를 통해 React 트리의 하위 컴포넌트에 전달할 수 있게 해주는 함수

 

사용 예시

import { createStore } from 'zustand';

const store = createStore((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Provider를 사용하여 하위 컴포넌트에 스토어 전달
import { Provider } from 'zustand/react';
<Provider store={store}>
  <App />
</Provider>

 


1. 라이브러리 설치하기

npm install zustand


2. 스토어 생성
상태를 정의하고 관리하기 위한 스토어를 생성한다. create 함수를 사용하여 스토어를 생성할 수 있다.

import create from 'zustand';

   const useStore = create(set => ({
     bears: 0,
     increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
     removeAllBears: () => set({ bears: 0 })
   }));



3. 상태 사용
useStore 훅을 사용하여 상태와 상태 업데이트 함수를 사용할 수 있다.

import React from 'react';
import { useStore } from './store';

   function BearCounter() {
     const bears = useStore(state => state.bears);
     const increasePopulation = useStore(state => state.increasePopulation);

     return (
       <div>
         <h1>{bears} bears around here ...</h1>
         <button onClick={increasePopulation}>One up</button>
       </div>
     );
   }

   export default BearCounter;



4. 미들웨어 사용
미들웨어를 사용하여 로깅이나 기타 기능을 추가할 수 있다.

import create from 'zustand';
import { devtools, persist } from 'zustand/middleware';

   const useStore = create(devtools(persist(set => ({
     bears: 0,
     increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
     removeAllBears: () => set({ bears: 0 })
   }), { name: 'bear-storage' })));

 


Zustand를 사용한 간단한 todo 리스트 예제 프로그램

1. Vite 프로젝트 생성 및 Zustand 설치

npm create vite zustand-todo
cd zustand-todo
npm install zustand
npm install

 

vite로 프로젝트를 생성하고 zustand 라이브러리를 설치해준다.

 

폴더 구조는 다음과 같이 설정했다.

zustand-todo
├── node_modules
├── public
│   └── vite.svg
├── src
│   ├── components
│   │   └── TodoItem.jsx
│   ├── store
│   │   └── index.js
│   ├── App.css
│   ├── App.jsx
│   ├── index.css
│   └── main.jsx
├── .gitignore
├── index.html
├── package-lock.json
├── package.json
├── README.md
└── vite.config.js

 

2. Zustand 스토어 생성

src/store/index.ts

import { create } from "zustand";

const useTodoStore = create((set) => ({
  todos: [],
  addTodo: (title) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), title, completed: false }],
    })),
  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),
}));

export default useTodoStore;

 

redux를 써 본 사람들은 익숙할 것 같다!

store을 생성하고 이 안에 상태를 관리할 함수들을 정해준다. add, remove, 이런 함수들!

 

 todos: [],

 

초기 상태는 store에서 정해준다. 그래야 코드 흐름을 파악하기 쉬울 듯

 

addTodo: (title) =>
  set((state) => ({
    todos: [...state.todos, { id: Date.now(), title, completed: false }],
  })),

 

상태 변경 함수를 하나만 예시로 설명을 해보자면, addTodo함수는 새로운 todo 항목을 추가하는 역할을 한다.

title인자를 받아 새로운 항목을 생성하고 todos 배열에 추가해주는 역할을 한다. set함수는 현재 상태를 가져와서 새로운 상태 객체를 반환해준다. 새로운 todo 항목은 id, title, completed 상태를 가지게 된다.

 

이렇게 생성한 store는 컴포넌트에서 useTodoStore 훅을 사용하여 상태를 읽고 업데이트할 수 있다!

 

src/components/TodoItem.tsx

import useTodoStore from "../store/index";

function TodoItem({ todo }) {
  const removeTodo = useTodoStore((state) => state.removeTodo);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);

  return (
    <li className="todo-item">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => toggleTodo(todo.id)}
      />
      <span className={`todo-title ${todo.completed ? "completed" : ""}`}>
        {todo.title}
      </span>
      <button className="delete-button" onClick={() => removeTodo(todo.id)}>
        Delete
      </button>
    </li>
  );
}

export default TodoItem;

 

 

이렇게 useTodoStore 훅을 가져와서 사용할 수 있다. 음 뭐야 별 거 없는 듯???

 

src/App.tsx

import { useState } from "react";
import useTodoStore from "./store/index";
import TodoItem from "./Components/TodoItem";
import "./App.css";

function App() {
  const [newTodo, setNewTodo] = useState("");
  const todos = useTodoStore((state) => state.todos);
  const addTodo = useTodoStore((state) => state.addTodo);

  const handleAddTodo = () => {
    if (newTodo.trim()) {
      addTodo(newTodo);
      setNewTodo("");
    }
  };

  return (
    <div className="container">
      <h1>Todo List</h1>
      <div className="input-container">
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
        />
        <button onClick={handleAddTodo}>Add Todo</button>
      </div>
      <ul className="todo-list">
        {todos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  );
}

export default App;

 

나머지 함수들도 이렇게 사용해줄 수 있다.

 

recoil과 다르게 provider로 감싸줄 필요도 없고 이게 끝이다! 간단한데??????


완성된 화면

728x90