GitHub

[react] 상!태!관!리! Zustand 를 아십니까?

hojun lee · 09/11/2023
커버이미지

zustand : 독일어로 상태

react state-management " zustand

상태관리가 왜 필요한가

사용자와의 활발한 인터랙션을 모두 처리할 수 있으며, 실시간 통신을 통해 데이터를 받아오고 이를 기반으로 렌더링 하는 등 다양한 기능을 포함하고 있다. 그렇지만 기반이 되는 핵심은 애플리케이션 수준에서는 자체적으로 상태를 관리해야하는 역할이 필수적으로 요구된다는 점이다.

React에서 상태란 오늘날 웹 프론트엔드 개발에 있어서 상태라는 것은 크게 보면 웹 애플리케이션을 렌더(render)하는데 있어 영향을 미칠 수 있는 값이라고 한다.

Plain Javascript Object hold information influences the output of render ___ 공식문서

리액트는 독립적인 컴포넌트 단위로 구성되어있다. 하나의 단위에서만 생활하던 때는 상태라는 것이 쉽게 공유되었다. 부족단위, 마을단위, 학교단위 등 단위 내 상태는 마이크하나로 전파 될 수 있었다. React 에서는 hook useState를 사용해 하나의 컴포넌트에서 충분히 상태를 관리했고 인근 옆 단위까지도 훌륭히 상태 전파의 기능을 수행했다.

여기서 주목해야할 점은 _독립_이다.

하나의 독립적인 컴포넌트 단위에서의 공유는 쉬웠는데, 이젠 그렇지 않은 시대가 되었다. 글로벌시대에 글로벌하게 상태를 관리 할 수는 없을까라는 의문이 생기기 시작했다.

React에서는 자체적으로 상태를 관리할 수 있도록 여러 기능을 제공하고 있다. 하지만 사용자의 입장에서는 효율적으로 전역 상태를 관리해야하는 필요가 생겨났다. 그래서 등장한 것이 상태관리 라이브러리다.

redux / Context API / Mobx / Recoil / zustand / etc

많은 상태관리 라이브러리가 있지만, 이번 프로젝트에는 zustand를 사용해보고자 한다.

다시 zustand🥗

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated. 간단한 Flux 원칙을 사용하는 작고 빠르고 확장 가능한 상태 관리 솔루션입니다. Hook 기반으로 하는 편리한 API가 있습니다.

zustand를 사용하고자 마음먹은 건 한 블로그의 제목을 보고서였다.

개쉽다! Zustand 사용법

개쉽다는데 안 써볼 이유가 없었다. 내가 사용해본 상태관리 라이브러리는 Recoil뿐인데 사용한 이유도 리액트 Hook 과 비슷해 쉽다고 들어서였다. 짧은 프로젝트 기간에서 효율을 내기위해 낮은 러닝커브의 라이브러리를 선택했다. 이번엔 그냥 쉬워서라기보다 장단점을 고려해 zustand를 사용해보고자 한다.

Zustand는 Context API 사용을 배제하고 클로저를 활용하여 스토어 내부 상태를 관리한다. 따라서 createStoreHook 을 호출하여 리턴된 useStore 를 어느 컴포넌트에서나 import하여 원하는대로 사용하더라도 같은 스토어를 바라보게 된다.

flux 기반

기존 MVC 패턴 mvc

Model : 저장소 Controller : Model의 데이터 관리 (CRUD) View : 받아온 Model의 데이터를 사용자에게 보여줌

페이스북은 “MVC는 정말 눈 깜짝할 사이에 복잡해진다”고 말하며 이 문제의 해결 방안으로 단방향 데이터 흐름을 가지는 Flux 패턴을 고안했다고 한다.


장점강화 Flux 패턴

flux

Flux는 사용자 입력을 기반으로 Action을 만들고 Action을 Dispatcher에 전달하여 Store(Model)의 데이터를 변경한 뒤 View에 반영하는 단방향의 흐름으로 애플리케이션을 만드는 아키텍처

각 요소들은 단방향 흐름에 따라 순서대로 역할을 수행하고, View로부터 새로운 데이터 변경이 생기면 처음부터 다시 이 순서대로 실행합니다. 이렇게 함으로써 예외 없이 데이터를 처리할 수 있게 된다고 한다. 아무튼 그렇다고 한다.

간단한 장점

  1. 굉장히 쉽다. 동작을 이해하기 위해 알아야 하는 코드 양이 아주 적다. 핵심 로직의 코드 줄 수가 약 42줄밖에 되지 않는다. (VanillaJS 기준)
  2. 보일러플레이트가 거의 없다. (Context API랑 비교)
  3. redux Devtools를 사용할 수 있어 debugging에 용이하다.
  4. 상태 변경 시 불필요한 리랜더링을 일으키지 않도록 제어하기 쉽다.

zustand 사용법👩🏽‍💻

설치

npm install zustand
yarn add zustand

상태를 관리하는 store 만들기


// store.js

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

// set 함수를 통해서만 상태를 변경할 수 있다
const useStore = create(
    devtools((set) => ({
        isLogin: false,
        toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),

        count: 1, //state

      
          // set 함수 사용 #1 현재 상태를 기반으로 새로운 상태를 리턴하는 함수
        increaseCount: () => {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) => ({ count: state.count + 1 }));
        },

        // set 함수 사용 #2 아예 변경하려는 상태 값
        setCnt: (input) => {
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

      

        clearCnt: () => {
            // count 초기화
            set((state) => ({ count: 0 }));
        },
    }))
);

// redux devtools 사용하기
// const useStore = create(devtools(myStore));

export default useStore;
  1. store.js javascript 파일 만들기 나는 src / status / store.js 파일을 만들었다.
  2. 제일 상단에 zustand의 create를 불러와준다. 그래야 사용할 수 있다. import create from "zustand";
  3. store를 생성한다.
  • 스토어는 Hook 이다.
  • 상태를 통합적으로 관리한다. 신병훈련소 느낌
  • 어떤 type이든 넣을 수 있다. (원시, 객체, 함수)
  • set함수는 상태를 병합한다.
// store hook 생성
const useStore = create(
    (set) => ({
        
      isLogin: false, //state
      toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),
      
    })
);

export default useStore;

먼저 상태가 진짜 관리 되는지 확인 차 boolean type의 객체로 버튼 클릭 시 토글되는지 실험을 해보았다.

isLogin state에 false를 담아놨다. toggleIsLogin state에는 함수를 넣고 set함수를 넣어 isLoginstate 상태를 호출될 때마다 반전시키도록 했다.

ex) toggleIsLogin -> 호출 되면 -> isLogin : true toggleIsLogin -> 호출 되면 -> isLogin : false

컴포넌트에 바인딩한다. 살포시 적용

store를 컴포넌트에 불러오는 방법은 2가지가 있다.

    // #1 select 함수를 사용해 import 하여 사용하기
     const isLogin = useStore((state) => state.isLogin);

    // #2 구조분해 할당을 통해 가져오기 
     const { isLogin } = useStore();

직관적인 방법은 #2 구조분해 할당을 통해 가져오는 방법인 것 같다.

const App = () => {

    const { isLogin, toggleIsLogin } = useStore();

    return (
        <div>
            <p>{"" + isLogin}</p>
              <button onClick={toggleIsLogin}>
                <b>버튼 클릭 시 백만원</b>
            </button>

        </div>
    );
};

설명서

아주 쉽게 위 블로거가 말한대로 개쉽게 전역상태관리를 적용하였다.

조금 더 적용해보기

김관장 블로거님의 zustand 글 참고(댓글) 해서 추가로 구현해보고자 했다. 근데 그대로 복붙해서도 안되는 것이었다. 그래서 공식문서를 참고했다. set함수를 사용할 때 괄호를 잘 써야 오류가 안난다. 중괄호 대괄호가 많기 때문에 주의하기!

//store.js

const useStore = create(
    devtools((set) => ({
        isLogin: false,
        toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),

        count: 1, //state

          // 함수를 전달하여 상태를 갱신하는 경우
        increase: () => {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) => ({ count: state.count + 1 }));
        },

      
          // 객체를 직접 전달하여 상태를 갱신하는 경우
        setCnt: (input) => { //매개변수를 입력받아,
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

        clearCnt: () => {
            // count 초기화
            set((state) => ({ count: 0 }));
        },
    }))
);
//app.js


const App = (props) => {
    const { isLogin, count, increase, toggleIsLogin, setCnt, clearCnt } =
        useStore();

    return (
        <div>
            <div>
                <p>{"" + isLogin}</p>
                <button onClick={toggleIsLogin}>
                    <b>버튼 클릭 시 백만원</b>
                </button>
            </div>
            <div>
                <div>현재 Cnt == {count}</div>
                <button onClick={increase}>[+1]</button>
                <button onClick={() => setCnt(10)}>[set_10]</button>
                <button onClick={clearCnt}>[clear]</button>
            </div>
        </div>
    );
};

이미지

아름답게 잘동하는것을 볼 수 있다. 이정도 기능은 사실 useState로도 충분하고 차고 넘치지만 어디에서나 사용할 수 있는 상태라는 점에서 맘에 들고 무려 Recoil보다 사용방법이 쉬웠다.

작동원리

작동원리

redux-devtool을 사용해보자

Zustand는 Middleware로 Devtools를 지원한다. Redux DevTools를 Chrome 웹 스토어에서 설치하고 store를 devtools 로 묶어 주면된다.


// store.js
// redux devtools 사용하기

import create from "zustand";
import { devtools } from "zustand/middleware"; // #1 import 하기

const useStore = create(
    devtools((set) => ({ // set을 devtools로 묶어주기
       ...
    }))
);


// const useStore = create(devtools(myStore)); // 이렇게 해도 된다는데 난 안되더라...

export default useStore;