Zustand 사용기 - 투두리스트 상태 관리와 다크모드

GunYong·2024년 8월 31일

React

목록 보기
10/17
post-thumbnail

서론

프로젝트를 진행하다 보면 하나의 컴포넌트에서 다른 컴포넌트로 상태를 전달하는 상황이 빈번하게 일어난다.


https://dev.to/stuxnat/final-react-project-2poi

기본적으로 React의 데이터 흐름은 State & Props를 사용해 단방향으로 이루어져야 한다는 원칙이 있으며 이러한 데이터 흐름은 단순하고 예측 가능하며, 컴포넌트 간의 관계를 명확히 정의하기 때문에 유지 보수성을 향상시킨다.

하지만 하나의 프로젝트 내에는 다수의 컴포넌트가 존재하고 하나의 State를 전달하는데 수 많은 컴포넌트를 다 거쳐서 이동하는 과정을 "Prop drilling"이라고 한다.

경험에 비춰볼때 리펙토링을 하거나 협업을 할때 해당 State는 어디서부터 내려온 것인지 찾아가는 과정도 매우 불편했고 성능저하까지 이어졌다.

따라서 우리는 React의 상태 관리 라이브러리를 사용한다.

사용기

기존에는 커스텀 훅과 recoil을 사용해서 관리하고 적용해보며 글을 정리했었다.

하지만 Recoil은 버전 업데이트가 이루어지지 않아서 성능과 안정성이 뒤처지고 있다는 소식을 접했다.

zustand 장점

zustand는 우선 굉장히 쉽고 가볍다. 바닐라 자바스크립트 기준 핵심 로직이 42줄밖에 안된다고 한다. 또한 redux와 유사한것 처럼 redux devtool또한 사용이 가능하며 상태 변경 시 불필요한 리렌더링을 일으키지 않도록 제어하기 쉽다고 한다. zustand는 redux와 같은 flex 패턴을 사용한다. ( 단방향 데이터 흐름을 지닌다.)

적용하기

그래서 어디에 어떻게 사용할 것인가? 우선 공식 문서를 읽으며 코드에 적용하기에 앞서 간단하게 진행하고 있는 투두리스트 풀스택 프로젝트에서 다크모드와 할일 상태(all / completed / active)를 간단하게 관리해보려고 한다.
Zustand를 사용해 states 상태를 관리하는 스토어를 설정해준다. store.ts라는 파일을 만들어 스토어를 정의한다.

// store.ts
import { create } from "zustand";

interface StateStore {
  currentState: string;
  setCurrentState: (state: string) => void;
  isDarkMode: boolean;
  toggleDarkMode: () => void;
}

const useStateStore = create<StateStore>((set) => ({
  currentState: "all",
  setCurrentState: (state) => set({ currentState: state }),

  // 다크 모드 상태 추가
  isDarkMode: false,
  toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
}));

export default useStateStore;

그리고 헤더 컴포넌트에서 불러와서 적용해준다.

// Header.tsx
import { HiMoon, HiSun } from "react-icons/hi";
import useStateStore from "../store/store";

const Header = () => {
  const { currentState, setCurrentState, isDarkMode, toggleDarkMode } =
    useStateStore();

  const states = ["all", "completed", "active"];

  return (
    <header
      className={`flex justify-between items-center relative rounded-t-2xl h-12 w-full pr-4 pl-4 box-border ${
        isDarkMode ? "bg-dark-mode-header" : "bg-green-400"
      }`}
    >
      <button onClick={toggleDarkMode}>
        {isDarkMode ? <HiSun className="text-white" /> : <HiMoon />}
      </button>
      <div className="flex justify-between w-7/12">
        {states.map((state) => (
          <button
            key={state}
            onClick={() => setCurrentState(state)}
            className={`
                ${
                  currentState === state
                    ? "text-bubble-gum"
                    : isDarkMode
                    ? "text-white"
                    : "text-black"
                }
              `}
          >
            {state}
          </button>
        ))}
      </div>
    </header>
  );
};

export default Header;

이와 같이 설정해준 이유는 입력란에 원하는 할일을 작성하고 추가할 때 지금 선택되어 있는 상태와 함께 저장해서 후에 상태 토글 버튼을 누르면 해당 상태에 맞는 할일만 보여지는 기능을 구현하기 위해서이다.

그럼 선택된 상태가 잘 저장되는지 다른 컴포넌트에서 확인해본다.

import React, { useState, useEffect } from "react";
import useStateStore from "../store/store";

const Input = () => {
  const [todo, setTodo] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState<string>("");
  const { currentState, isDarkMode } = useStateStore();

  const handleAddTodo = () => {
    if (inputValue.trim() === "") return;

    setTodo([...todo, inputValue]);
    setInputValue("");
  };

  const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Enter") {
      handleAddTodo();
    }
  };

  useEffect(() => {
    console.log(todo, currentState);
  }, [todo]);

  return (
    <div
      className={`flex justify-end items-center relative rounded-b-2xl h-14 w-full pr-4 pl-4 box-border ${
        isDarkMode ? "bg-dark-mode-header" : "bg-green-400"
      }`}
    >
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyPress={handleKeyPress}
        className="h-2/3 w-full rounded-l-xl pr-2 pl-2 box-border"
        placeholder="Add Todo"
      />
      <button
        onClick={handleAddTodo}
        className="w-1/6 h-2/3 rounded-r-xl bg-bubble-gum"
      >
        Add
      </button>
    </div>
  );
};

export default Input;

후에 key-value로 구조화해서 저장해줄 예정이고 지금은 할일 추가 동작을 했을때 발생하는 함수 handleAddTodo가 호출 될 때 선택된 헤더 상태가 제대로 출력되는지 확인해준다. 콘솔창을 확인해보면 제대로 적용된다는 것을 알 수 있다.

이렇게 되면 저장한 todo 데이터를 선택한 상태와 일치하는 목록만 화면에 뿌려줄 수 있다.

후기

다크모드 적용할때 다시 사용해보겠지만 정말 간단하다는 것이 장점같았다.
프로젝트 단위가 작아서 그럴수도 있지만 개인 프로젝트다 소규모에서는 진입 장벽도 낮고 간편한 zustand를 애용할 것 같다.

추가 + 다크모드 설정

위의 코드 박스를 최신으로 업데이트 하였다. 간단히 적용한 방법에 대해서 적어보면 다음과 같다.

store.ts에 다크 모드 상태를 추가해주고 헤더와 입력창 컴포넌트에서 isDarkMode를 불러와서 상태에 따른 색을 삼항연산자를 이용하여 나타내주면 다크모드일때 다음과 같은 화면이 나타난다.

추가로 Tailwind에 원하는 색이 없을 때 아래와 같이 커스텀 컬러를 추가해준 후 불러와서 사용이 가능하다는 점을 알고 추가해주었다.

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        "bubble-gum": "#ff77e9",
        "dark-mode-main": "#ECE6E6",
        "dark-mode-header": "#1C1D3D",
      },
    },
  },
  plugins: [],
};

0개의 댓글