오늘은! Zustand 사용법을 알아볼 것이다.
바야흐로 n개월전... 정말 라이브러리라는 것을 거의 몰랐던 나는 실무에서 엄청난 코드를 마주하게 된다..
어느정도 규모가 좀 있는 프젝이였는데 초반에 코드를 짜던 분이 (그분도 프론트는 익숙치 않다 했음,,)
상태 관리를 useState로 다 하고있었던 .. (근데 그게 맞는 건줄 알았ㅋ)
원래 useState로 하는 거 아냐?
라고 한다면 맞긴한데,,, 정말 단순한 로직일 경우에만 쓰지
사실상 실무에서는 거의 상태관리 라이브러리를 쓴다. (상태관리가 복잡하기 때문)
useState로 하다보니 감당할 수 없는 사이드이펙트들......... 정말 울고싶었다.
아무래도 그렇게 뼈저리게 느끼다보니 아 이래서 개발자들 다 상태관리 라이브러리 쓰는 구나.. 괜히 유명한게 아니구나 싶었다. 그래서 퇴사하고 나서는 프로젝트들 클론해보면서 라이브러리들을 쓴 거 같다.
그때 잠깐 같이 일하셨던 개발자분께서 Zustand 많이들 쓴다하셔서 사용법을 익힌 것 같다.
Zustand
"Zustand"는 React 애플리케이션의 상태 관리를 간편하게 해주는 라이브러리입니다. Zustand는 상태 관리를 위해 React의 Context API와 함께 사용되며, Redux와 비슷한 기능을 제공하지만 훨씬 간단하고 직관적입니다.
Zustand는 상태를 관리하기 위해 상태 컨테이너를 제공하며, 이 상태 컨테이너는 React 컴포넌트 트리 내에서 사용될 수 있습니다. 이러한 상태 컨테이너는 useState와 useReducer를 사용하여 상태를 관리하지만, 더 간단한 API를 제공하여 상태를 업데이트하고 구독하는 방법을 단순화합니다.
주요 특징
- 간단한 API: Zustand는 간단하고 직관적인 API를 제공하여 상태를 관리합니다. create 함수를 사용하여 상태를 초기화하고, 이를 통해 상태를 업데이트하고 구독할 수 있습니다.
- Hooks 기반: Zustand는 React Hooks와 함께 사용되어 React 애플리케이션의 상태를 관리합니다. 이는 함수형 컴포넌트에서 상태를 쉽게 관리할 수 있음을 의미합니다.
- 최적화: Zustand는 내부적으로 Immer 라이브러리를 사용하여 불변성을 유지하면서 상태를 업데이트합니다. 또한 컴포넌트의 렌더링을 최적화하기 위해 쿼리 기반의 구독을 지원합니다.
- 편리한 디버깅: Zustand는 상태의 변화를 추적하고 디버깅할 수 있는 도구를 제공합니다. DevTools 확장 프로그램을 사용하여 상태의 변화를 모니터링하고 디버깅할 수 있습니다.
그럼 Redux와의 차이는 무엇일까?
Redux도 많이 쓰지 않냐!
Zustand와 Redux의 차이점
- API 및 사용법:
- Redux: Redux는 강력하고 유연한 상태 관리 도구이지만, 사용법이 비교적 복잡합니다. 상태, 액션, 리듀서와 같은 개념을 이해하고 사용해야 합니다.
- Zustand: Zustand는 Redux보다 훨씬 간단한 API를 제공합니다. 초기화, 상태 업데이트 및 구독 등의 작업이 간단하고 직관적입니다.
- 파일 크기:
- Redux: Redux는 상대적으로 큰 라이브러리입니다. 라이브러리 자체와 함께 사용하는 추가적인 미들웨어 및 도구들 때문에 번들 크기가 커질 수 있습니다.
- Zustand: Zustand는 Redux보다 훨씬 작고 경량화된 라이브러리입니다. 번들 크기가 작고 성능 면에서도 더 유리할 수 있습니다.
- 불변성 유지:
- Redux: Redux는 불변성을 유지하는 것이 중요합니다. 이는 불필요한 객체 복사를 유발할 수 있으며, 상태 업데이트가 복잡해질 수 있습니다.
- Zustand: Zustand는 내부적으로 Immer와 함께 사용되어 불변성을 유지합니다. 하지만 개발자가 명시적으로 불변성을 유지할 필요는 없습니다.
- React와의 통합:
- Redux: Redux는 React와 함께 사용할 때 별도의 라이브러리인 react-redux를 사용해야 합니다. 이는 컴포넌트와 상태를 연결하기 위해 별도의 작업이 필요합니다.
- Zustand: Zustand는 React Hooks와 함께 사용되며, 별도의 라이브러리 없이 React 컴포넌트와 상태를 직접 연결할 수 있습니다.
차이점을 봤을 때 결론적으로, Zustand는 Redux보다 더 간단하고 가벼운 상태 관리 라이브러리로, 작은 규모의 프로젝트나 단순한 상태 관리에 적합하다! 반면에 Redux는 대규모 애플리케이션에 더 적합하며, 상태 관리에 대한 엄격한 규칙과 고도의 유연성이 필요한 경우에 유용한 듯?!
아무튼 Redux는 나중에 공부해서 정리하도록 하고,,
👀 Zustand 사용법
Zustand 설치
npm install zustand
또는
yarn add zustand
상태와 상태를 업데이트하는 액션을 정의
따로 store.js 파일을 만들어서 상태와 액션을 정의해주어야 한다.
// store.js
import create from 'zustand';
// 초기 상태 정의
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
선언 먼저 해주고,
count는 0으로 설정하고 증가와 감소 액션을 넣어준다.
상태를 사용할 컴포넌트에서 useStore 훅을 가져와서 상태를 사용
// Counter.js
import React from 'react';
import useStore from './store';
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
컴포넌트에서 store를 가져와 쓸 수 있다.
위 예제를 보면 count의 상태와, 액션을 가져오는 것을 볼 수 있다.
나는 Zustand로 ToDo 기능을 구현해봤는데 같이 살펴보자!
👀 Zustand 사용해서 ToDo List 만들기
https://react-lib-study-jjul.netlify.app/zustand-todo
파일 구조
일단 ZustandTodo를 메인으로 리스트, 투두 아이템, 투두 추가부분으로 컴포넌트를 생성했다.
그리고 store파일 아네 todoStore.js라고 상태와 액션을 정의할 파일을 만들어줬다.
todoStore.js
import { create } from "zustand";
const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { text, completed: false, id: Date.now() }],
})),
removeTodo: (id) =>
set((state) => ({ todos: state.todos.filter((e) => e.id !== id) })),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((e) =>
e.id === id ? { ...e, completed: !e.completed } : e
),
})),
}));
export default useTodoStore;
- todos: 할 일 목록을 담는 배열이다. 초기값은 빈 배열.
- addTodo: 새로운 할 일을 추가하는 함수이다. text 매개변수로 받은 내용을 가지고 새로운 할 일 객체를 생성하여 todos 배열에 추가한다. 이때 completed는 기본적으로 false로 설정되며, id는 현재 시간을 이용하여 고유한 값으로 설정된다.
- removeTodo: 특정 id에 해당하는 할 일을 제거하는 함수이다. id 매개변수로 받은 값과 일치하는 할 일을 todos 배열에서 제거한다.
- toggleTodo: 특정 id에 해당하는 할 일의 완료 상태를 토글하는 함수이다. id 매개변수로 받은 값과 일치하는 할 일의 completed 속성 값을 반전시킨다.
TodoItem.jsx (투두 아이템 출력)
import React from "react";
import useTodoStore from "../store/todoStore";
import { motion } from "framer-motion";
// todo props 받아옴
const TodoItem = ({ todo }) => {
// store에서 삭제, 완료 기능 함수를 가져옴
const { removeTodo, toggleTodo } = useTodoStore();
// 모션 설정
const variants = {
initail: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 },
};
return (
<motion.li
variants={variants}
initial="initial"
animate="animate"
className="flex items-center p-2"
layout
>
// input 요소
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="form-checkbox h-5 w-5"
/>
// todo의 text 출력
<span className={`flex-1 ml-2 ${todo.completed ? "line-through" : ""}`}>
{todo.text}
</span>
// 삭제 버튼
<button
onClick={() => removeTodo(todo.id)}
className="px-6 py-2 bg-red-500 text-white rounded"
>
삭제
</button>
</motion.li>
);
};
export default TodoItem;
- const TodoItem = ({ todo }) => { ... }: todo라는 props를 받는다.
- const { removeTodo, toggleTodo } = useTodoStore();: useTodoStore 훅을 사용하여 상태 컨테이너에서 removeTodo와 toggleTodo 함수를 가져온다. (각각 삭제와 완료 기능)
- const variants = { ... }: Framer Motion을 사용하여 애니메이션 효과를 정의하는 객체 (framer-motion 라이브러리 사용)
- <input> ... </input>: 체크박스 input 요소로 할 일의 완료 상태를 나타낸다. checked 속성을 통해 완료 상태를 제어하고, onChange 이벤트 핸들러를 통해 완료 상태를 토글한다.
- <button> ... </button>: 삭제 버튼을 나타내는 요소이다. 클릭 이벤트를 통해 해당 할 일을 삭제한다. (해당 id를 삭제)
TodoList.jsx (리스트 출력)
import React from "react";
import useTodoStore from "../store/todoStore";
import { AnimatePresence } from "framer-motion";
import TodoItem from "./TodoItem";
const TodoList = ({ todo }) => {
const todos = useTodoStore((state) => state.todos);
return (
<AnimatePresence>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</AnimatePresence>
);
};
export default TodoList;
- const TodoList = ({ todo }) => { ... }: todo라는 props를 받는다.
- const todos = useTodoStore((state) => state.todos);: useTodoStore 훅을 사용하여 상태 컨테이너에서 할 일 목록을 가져온다.
- {todos.map((todo) => (<TodoItem key={todo.id} todo={todo} />))}: 할 일 목록을 순회하면서 각각의 할 일에 대해 TodoItem 컴포넌트를 렌더링한다. 각각의 TodoItem에는 고유한 key prop과 todo prop이 전달된다. (map 사용)
AddTodo.jsx (투두 추가 출력)
import React, { useState } from "react";
import useTodoStore from "../store/todoStore";
const AddTodo = () => {
const [text, setText] = useState(""); // text 상태
const addTodo = useTodoStore((state) => state.addTodo);
const handleSubmit = (e) => {
e.preventDefault();
// 공백 제거
if (!text.trim()) return;
addTodo(text);
setText("");
};
return (
<form onSubmit={handleSubmit} className="flex justify-between p-2">
<input
type="text"
className="flex-1 p-2 border rounded"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button
type="submit"
className="transition duration-200 ease-in-out transform hover:bg-primary_hover px-6 bg-primary text-white ml-2 rounded"
>
추가
</button>
</form>
);
};
export default AddTodo;
- const [text, setText] = useState("");: text라는 상태와 이를 업데이트하기 위한 setText 함수를 선언한다. useState 훅을 사용하여 초기값을 빈 문자열로 설정!
- const addTodo = useTodoStore((state) => state.addTodo);: useTodoStore 훅을 사용하여 상태 컨테이너에서 addTodo 함수를 가져오는데, 이 함수는 새로운 할 일을 추가하는 데 사용된다.
- const handleSubmit = (e) => { ... }: 폼 제출을 처리하는 핸들러 함수. e.preventDefault()를 호출하여 기본 제출 동작을 방지하고, 입력된 텍스트가 비어있지 않은 경우에만 addTodo 함수를 호출하여 새로운 할 일을 추가한다. 마지막으로 입력 필드를 초기화한다.
- <form onSubmit={handleSubmit} className="flex justify-between p-2"> ... </form>: 폼 요소를 정의한다. onSubmit 이벤트 핸들러로 handleSubmit 함수를 등록하고, 입력 필드와 추가 버튼을 포함한다.
- <input ... />: 사용자가 텍스트를 입력하면 상태인 text가 업데이트되고, onChange 이벤트 핸들러를 사용하여 입력값을 업데이트한다.
- <button ... >추가</button>: 사용자가 클릭하면 폼이 제출되고 새로운 할 일이 추가된다.
이제 컴포넌트들을 다 구현해놨으니 합치기만 하면 된다!
ZustandTodo.jsx
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
const ZustandTodo = () => {
return (
<div className="pt-[10%] container mx-auto max-w-md h-screen py-6 font-NanumSquareNeo">
<div className="flex flex-col p-4 border rounded-lg shadow-lg max-h-[70vh] overflow-y-auto">
<h1 className="text-2xl font-bold mb-4">💜Todo List</h1>
<AddTodo />
<TodoList />
</div>
</div>
);
};
export default ZustandTodo;
굳이 분리시켜놓지 않아도 되지만.. 난 사이트를 따로 제작하고 있어서 메인페이지에 컴포넌트들을 불러왔다.
완성된 페이지와 코드 보기
코드 : https://github.com/YeoDaSeul4355/react-lib-study/tree/main/src/pages/zustand-todo