6 분 소요


1. Zustand 개요

1.1 Zustand 개념

Zustand는 독일어로 “상태”를 의미하며, React 애플리케이션에서 상태를 효율적으로 관리할 수 있도록 도와주는 상태 관리 라이브러리이다.


1.1 Zustand 특징

  • Redux와 같은 flux 아키텍처로 동작한다.
    • Flux 아키텍처는 단방향 데이터 흐름을 가지는 애플리케이션 아키텍처 패턴이다.
  • 보일러플레이트 코드 감소
    • Redux나 React Context API에 비해 보일러플레이트 코드가 적다.
    • 보일러플레이트 코드란? 반복적으로 작성해야 하는 기본적인 설정 코드로, 애플리케이션의 핵심 로직과는 직접적으로 관련이 없지만 필수적인 부분이다.
  • Provider 불필요
    • Jotai는 전역 상태를 관리할 때 Provider 컴포넌트를 필요로 하지 않고, 각 컴포넌트에서 직접 원자(atom)를 사용하여 상태를 접근하고 업데이트할 수 있다.
  • React Hook 기반
    • React Hook을 사용하여 상태를 관리하여 함수형 컴포넌트에서 상태를 쉽게 사용할 수 있게 해준다.
    • useAtom 훅을 통해 원자를 접근하고 상태를 업데이트할 수 있다.



2. 사용하기

2.1 설치

yarn add zustand

2.2 스토어 생성

  • zustand를 사용하기 위해 스토어를 생성해야 한다.
  • 데이터를 저장하는 store를 사용할 때는 create 라는 메소드를 사용해서 선언한다.
  • set 함수는 상태를 업데이트하는 데 사용되는 함수이다.
// js로 사용할 때.
import {create} from 'zustand'

export const projectName = create((set) => ({
 	state: 0,
  	setState: (newState) => set({state: newState})
}))

// ts로 사용할 때.
interface State1 {
	state: number
  	setState: (newState: number) => void
}

export const projectName = create<State1>((set) => ({
	state: 0,
  	setState: (newState) => set({state: newState})
}))


2.3 스토어 예제

// src/store/counterStore.jsx
import create from "zustand";

const useCounterStore = create((set) => ({
  count: 0, // 초기값
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
}));

export default useCounterStore;


set 함수를 호출할 때 상태 업데이트를 객체 형태로 받는 이유가 뭘까?

  • 객체 형태의 상태 업데이트란
    • 상태를 업데이트할 때 변경하고자 하는 부분만 객체 형태로 전달하여 상태의 특정 속성만 변경하는 방식을 말한다.


  • 객체 형태의 상태 업데이트의 장점
    • 불변성 유지: 기존 상태 객체를 변경하지 않고 새 객체를 생성하기 때문에, 상태의 불변성을 유지할 수 있다.
    • 부분 업데이트: 특정 속성만 변경할 수 있다. 즉, 기존 상태의 다른 속성들을 유지하면서 일부만 변경할 수 있다.


2.4 스토어 사용

스토어를 사용하려면 React 컴포넌트에서 useCounterStore 훅을 호출하여 상태와 상태를 변경하는 함수를 사용한다.

// src/components/Counter.jsx
import useCounterStore from "../store/counterStore";

const Counter = () => {
  // 스토어에서 상태와 액션을 가져온다.
  const { count, increase, decrease } = useCounterStore();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>Decrease</button>
    </div>
  );
};

export default Counter;



3. 미들웨어(Middleware)

3.1 미들웨어 설치

zustand는 필요에 따라 미들웨어를 사용하여 추가 기능을 구현할 수 있다.

yarn add zustand/middleware


3.2 persist

persist 미들웨어는 상태를 로컬 스토리지 또는 세션 스토리지에 저장하고, 페이지를 새로고침해도 상태를 유지할 수 있도록 해준다.

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

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
      decrease: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: "counter-storage", // 저장할 키 이름
    }
  )
);


3.3 devtools

devtools 미들웨어를 사용하면 브라우저 개발자 도구에서 상태를 디버깅(Debugging)할 수 있다.

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

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
      decrease: () => set((state) => ({ count: state.count - 1 })),
    }),
    "MyStore"
  )
);


3.4 combine

여러 상태 조각을 하나의 스토어로 합치는 데 유용한 미들웨어이다.

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

const useStore = create(
  combine(
    {
      count: 0,
      user: null,
    },
    (set) => ({
      increase: () => set((state) => ({ count: state.count + 1 })),
      decrease: () => set((state) => ({ count: state.count - 1 })),
      setUser: (user) => set({ user }),
    })
  )
);



4. 실습: TodoList 만들기

4.1 useTodosStore.jsx

Zustand를 사용해 스토어를 생성하고, Axios를 사용해 API 요청을 처리한다.

코드 요약

// src/store/useTodosStore.jsx
import axios from "axios";
import { create } from "zustand";

// Axios 인스턴스 생성
const todosAxios = axios.create({
  baseURL: process.env.REACT_APP_SERVER_URL,
});

export const useTodosStore = create((set) => ({
  // todos 초기 값
  todos: [],
  loading: false,

  // todos 조회
  fetchTodos: async () => {
    set({ loading: true });
    try {
      const response = await todosAxios.get("/todos");
      set({ todos: response.data });
    } catch (error) {
      console.error(error);
    } finally {
      set({ loading: false });
    }
  },

  // todos 추가
  addTodos: async (data) => {
    try {
      const response = await todosAxios.post("/todos", data);
      set((state) => ({
        todos: [...state.todos, response.data], // 새로운 todo 추가
      }));
    } catch (error) {
      console.error(error);
    }
  },

  // todos 삭제
  deleteTodos: async (id) => {
    try {
      await todosAxios.delete(`/todos/${id}`);
      set((state) => ({
        todos: state.todos.filter((todo) => todo.id !== id), // 삭제된 todo 필터링
      }));
    } catch (error) {
      console.error(error);
    }
  },

  // todos 수정
  editTodos: async (id, updatedData) => {
    try {
      const response = await todosAxios.patch(`/todos/${id}`, updatedData);
      set((state) => ({
        todos: state.todos.map(
          (todo) => (todo.id === id ? response.data : todo) // 수정된 todo 반영
        ),
      }));
    } catch (error) {
      console.error(error);
    }
  },

  // todos isDone 상태 수정
  toggleTodos: async (id) => {
    try {
      // 1. 현재 상태에서 id와 일치하는 todo 찾기
      //_토글은 isDone 값을 반전시키기 때문에, 현재 상태를 먼저 알아야 함
      const todoToggle = useTodosStore
        .getState()
        .todos.find((todo) => todo.id === id);

      // 2. 찾은 todo의 isDone 값을 반전시켜서 서버에 PATCH 요청을 보내기
      const response = await todosAxios.patch(`/todos/${id}`, {
        isDone: !todoToggle.isDone,
      });

      // 3. 서버 응답으로 상태 업데이트
      set((state) => ({
        todos: state.todos.map((todo) =>
          todo.id === id ? { ...todo, isDone: response.data.isDone } : todo
        ),
      }));
    } catch (error) {
      console.error(error);
    }
  },
}));


4.2 Todos.jsx

할 일 추가 폼과 완료된/미완료된 할 일 리스트를 보여준다.

// src/components/todos/Todos.jsx
import styled from "styled-components";
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";

const Todos = () => {
  return (
    <>
      <TodoForm />
      <StH1>할 일 미완료</StH1>
      <TodoList showCompleted={true} />
      <StH1>할 일 완료</StH1>
      <TodoList showCompleted={false} />
    </>
  );
};

export default Todos;

const StH1 = styled.h1`
  font-size: 24px;
`;


4.2 TodoForm.jsx

새로운 할 일을 추가할 수 있는 폼이다.

// src/components/todos/TodoForm.jsx
import { useState } from "react";
import { useTodosStore } from "store/useTodosStore";

const TodoForm = () => {
  const { addTodos } = useTodosStore();
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!title || !content) {
      alert("제목과 내용을 모두 입력하세요.");
      return;
    }

    await addTodos({ title, content, isDone: false });
    alert("추가 완료");

    setTitle("");
    setContent("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="title">제목: </label>
        <input
          id="title"
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="제목을 입력하세요."
          required
        />
      </div>
      <div>
        <label htmlFor="content">내용: </label>
        <input
          id="content"
          type="text"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="내용을 입력하세요."
          required
        />
      </div>
      <button type="submit">추가하기</button>
    </form>
  );
};

export default TodoForm;


할 일 목록을 표시하고, 수정, 삭제, 완료 상태 토글 기능을 제공한다.

4.2 TodoList.jsx

// src/components/todos/TodoList.jsx
import { useEffect, useState } from "react";
import { useTodosStore } from "store/useTodosStore";

const TodoList = ({ showCompleted }) => {
  const { todos, loading, fetchTodos, deleteTodos, editTodos, toggleTodos } =
    useTodosStore();
  const [edit, setEdit] = useState(null);

  useEffect(() => {
    fetchTodos();
  }, [fetchTodos]);

  if (loading) {
    return <p>...로딩중</p>;
  }

  const handleDeleteTodo = (todo) => {
    const deleteConfirm = window.confirm("정말 삭제하시겠습니까?");
    if (deleteConfirm) {
      deleteTodos(todo);
      alert("삭제가 완료되었습니다.");
    }
  };

  const handleEditMode = (todo) => {
    setEdit(todo);
  };

  const handleDoneEdit = (todoId) => {
    editTodos(todoId, { title: edit.title, content: edit.content });
    alert("수정이 완료되었습니다.");
    setEdit(null);
  };

  const handleToggleTodo = (todo) => {
    toggleTodos(todo.id, !todo.isDone);
  };

  const filteredTodos = todos.filter((todo) => todo.isDone === showCompleted);

  return (
    <ul>
      {filteredTodos.map((todo) => (
        <li key={todo.id}>
          {edit && todo.id === edit.id ? (
            <>
              <input
                value={edit.title}
                onChange={(e) =>
                  setEdit((prev) => ({ ...prev, title: e.target.value }))
                }
              />
              <input
                value={edit.content}
                onChange={(e) =>
                  setEdit((prev) => ({ ...prev, content: e.target.value }))
                }
              />
              <button onClick={() => setEdit(null)}>취소</button>
              <button onClick={() => handleDoneEdit(todo.id)}>완료</button>
            </>
          ) : (
            <>
              <p>제목: {todo.title}</p>
              <p>내용: {todo.content}</p>
              <p>완료 상태: {todo.isDone ? "완료됨" : "미완료"}</p>
              <button onClick={() => handleToggleTodo(todo)}>
                {todo.isDone ? "취소" : "완료"}
              </button>
              <button onClick={() => handleDeleteTodo(todo.id)}>삭제</button>
              <button onClick={() => handleEditMode(todo)}>수정</button>
            </>
          )}
        </li>
      ))}
    </ul>
  );
};

export default TodoList;


카테고리:

업데이트:

댓글남기기