Zustand는 상태관리 본연의 기능에 집중한 라이브러리.
복잡성을 줄이고, 간단하고 직관적인 상태관리 기능 제공. 단순화된 Flux 패턴 기반의 small, fast, scalable 상태관리 솔루션이며 Hooks 기반의 간편한 API 제공.
상태관리의 중요성
기존 상태관리 라이브러리인 Redux는 강력한 기능과 다양한 미들웨어 지원하지만, 설정과 사용 방식이 복잡한 편.
상태관리는 모든 React 애플리케이션에서 핵심 요소.
작은 규모에서는 상태관리가 간단하지만, 애플리케이션이 커질수록 상태관리는 더 복잡해짐.
→ Zustand는 복잡성을 줄이고, 간단하고 직관적인 상태관리 기능 제공.
구독(subscribe)이란?
구독은 상태 변경을 감지하고, 해당 변경에 반응하는 컴포넌트만 업데이트하는 메커니즘 의미.
상태 일부가 변경되더라도, 그 값을 사용하지 않는 컴포넌트는 리렌더링되지 않음. 애플리케이션 전체 리렌더링 방지. 이를 통해 성능을 최적화하고 리렌더링으로 인한 성능 저하를 방지할 수 있음.
설치
npm install zustand
기본 사용법
// src/zustand/bearsStore.js
import { create } from "zustand";
const useBearsStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
export default useBearsStore;
// src/App.jsx
import "./App.css";
import useBearsStore from "./zustand/bearsStore";
function App() {
**const bears = useBearsStore((state) => state.bears);
const increasePopulation = useBearsStore((state) => state.increasePopulation);**
return (
<div>
<h1>{bears} around here ...</h1>
<button onClick={increasePopulation}>one up</button>
</div>
);
}
export default App;
보일러플레이트 최소화. 상태 정의와 사용 과정 직관적.
import { create } from "zustand";
const useStore = create(set => ({
bears: 0,
increasePopulation: () => set(state => ({ bears: state.bears + 1 }))
}))
보일러플레이트 많음. 비교적 많은 설정 코드 요구.
import { configureStore, createSlice } from '@reduxjs/toolkit'
const slice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1 }
}
})
const store = configureStore({ reducer: slice.reducer })
상태 정의
// src/zustand/todosStore.js
import { create } from "zustand";
const useTodosStore = create(set => ({
todos: [],
addTodo: (todo) => set(state => ({ todos: [...state.todos, todo] })),
removeTodo: (index) => set(state => ({
todos: state.todos.filter((_, i) => i !== index)
}))
}))
export default useTodosStore;
상태 사용
// src/App.jsx
import React, { useState } from "react";
import useTodosStore from "./zustand/todosStore";
function App() {
const todos = useTodosStore((state) => state.todos);
const addTodo = useTodosStore((state) => state.addTodo);
const removeTodo = useTodosStore((state) => state.removeTodo);
const [input, setInput] = useState("");
return (
<div>
<h1>Todo List</h1>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button
onClick={() => {
addTodo(input);
setInput("");
}}
>
Add Todo
</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => removeTodo(index)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
export default App;
immer란?
Immer는 JavaScript에서 상태를 쉽게 변경할 수 있게 해주는 라이브러리. 원본 데이터를 변경하지 않고 마치 직접 수정하는 것처럼 코드를 작성할 수 있으며, Immer가 자동으로 불변성을 유지한 새 상태를 만들어줌.
직접 중첩된 상태를 업데이트 했을 때의 문제점
Zustand의 간편한 상태 업데이트 방식은 깊은 중첩 구조를 가지는 상태를 업데이트할 때 문제가 발생할 수 있음.
배열이나 객체의 중첩된 상태를 업데이트할 때 불변성을 유지하지 않으면 상태 반영이 제대로 이루어지지 않아, 예상치 못한 오류가 발생할 수 있음.
import create from "zustand";
// Zustand 스토어 생성
const useTodosStore = create((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
// 불변성을 어기는 예시: 직접 배열을 수정
state.todos.push({ id: Date.now(), text, completed: false });
return state;
}),
toggleTodo: (id) =>
set((state) => {
// 불변성을 어기는 예시: 직접 객체를 수정
const todo = state.todos.find((todo) => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
return state;
}),
}));
export default useTodosStore;
// src/App.jsx
import useTodosStore from "./zustand/todosStore";
function App() {
const { todos, addTodo, toggleTodo } = useTodosStore();
return (
<div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
</li>
))}
</ul>
<button
onClick={() => addTodo(prompt("새로운 todolist를 입력해주세요."))}
>
Add Todo
</button>
</div>
);
}
export default App;
리스트가 추가/수정 되더라도 다른 요소에 의해 리렌더링이 일어나지 않는 한 UI에 반영이 안됨.
immer 설치
npm install immer
todosStore.js 수정
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
const useTodosStore = create(
immer((set) => ({
todos: [
{
id: 1,
title: "Learn Zustand",
tasks: [{ id: 1, task: "Read documentation", done: false }],
},
],
addTask: (todoId, newTask) =>
set((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
todo.tasks.push(newTask); // 불변성 유지: immer가 자동으로 처리
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
}),
toggleTask: (todoId, taskId) =>
set((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
const task = todo.tasks.find((task) => task.id === taskId);
if (task) {
task.done = !task.done; // 불변성 유지: immer가 자동으로 처리
}
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
}),
}))
);
export default useTodosStore;
선택적 상태 구독
상태의 특정 부분만 구독하여 성능 최적화 할 수 있음. 리렌더링 범위 최소화 가능.
// src/App.jsx
const todos = useTodosStore((state) => state.todos);
const addTask = useTodosStore((state) => state.addTask);
const toggleTask = useTodosStore((state) => state.toggleTask);
미들웨어 사용
import { produce } from "immer";
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useTodosStore = create(
persist(
(set) => ({
todos: [
{
id: 1,
title: "Learn Zustand",
tasks: [{ id: 1, task: "Read documentation", done: false }],
},
],
addTask: (todoId, newTask) =>
set(
produce((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
todo.tasks.push(newTask); // 불변성 깨짐: 직접 수정
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
})
),
toggleTask: (todoId, taskId) =>
set(
produce((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
const task = todo.tasks.find((task) => task.id === taskId);
if (task) {
task.done = !task.done; // 불변성 깨짐: 직접 수정
}
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
})
),
}),
{
name: "todos-storage", // 저장소 이름 설정.
// getStorage: () => sessionStorage, // localStorage가 아닌 곳에 저정하고 싶다면.
}
)
);
export default useTodosStore;
persist를 이용해서 새로고침을 하더라도 데이터 유지. persist는 zustand 내장이므로 별도 설치할 필요가 없음.
위와 같이 하면 새로고침 이후에도 localStorage에서 데이터를 관리하기 때문에 지속성을 유지할 수 있음.