새소식

반응형
Front-end

[React] Zustand Deep DIVE 하기

2024.11.15
  • -
반응형

1. 들어가며

개인적으로 Zustand 라이브러리를 여태껏 사용해보면서 굉장히 만족감을 느끼고 있고 가히 최신 상태 관리 라이브러리 중에 앞서고 있는 라이브러리가 아닐까 생각이 듭니다.

 

이에 Zustand의 기본적인 사용만 해보는 것이 아니라 조금 더 다양한 기능들을 알아보면서 심화된 기능들도 사용해보고자 글을 작성하게 되었습니다.

 

2. Zustand의 위엄

Zustand는 모던 React 라이브러리로 React 웹 개발 생태계에 상당히 뜨거운 바람을 불어 넣고 있습니다.

 

그 단순함은 Redux와 비교했을 때 엄청난 차이를 보여주기도 합니다.(안정성 측면은 모르겠지만...)

 

제가 체감했을 때는 난이도도 난이도지만 코드 양 자체가 별찍기 문제를 C언어로 푸는 것과 파이썬을 푸는 것을 비교하는 것과 비슷하다고 생각합니다.

#include <iostream>

int main()
{
    int val;

    std::cin >> val;

    for (int i = 1; i <= val; i++)
    {
        for (int j = 0; j < val - i; j++)
            std::cout << " ";
        for (int j = 0; j < i; j++)
            std::cout << "*";

        std::cout << "\n";
    }
}

 

val = int(input())

for i in range(1, val + 1):
    print(" " * (val - i) + "*" * i)

 

 

 

그렇다면 우선 zustand의 기본 코드를 살펴봅시다.

import { create } from 'zustand';

const useStore = create((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1})),
}));

 

단 몇 줄 밖에 되지 않는 코드로 전역 상태를 선언하였고 굉장히 직관적인 코드입니다.

 

또한 불변성과 data-UI 디커플링 등을 쉽게 활용하고 있습니다.

 

function App() {
    const store = useStore();
    
    return (
    	<div>
            <div>Count: {store.count}</div>
            <button onClick={store.increment}>Increment</button>
        </div>
    );
}

export default App;

 

3. 원하는 것만 가져오기

위와 같이 사용할 수도 있지만 다음과 같이 여러 컴포넌트에서 store를 공유하고 원하는 것만 선택할 수 있습니다.

 

function Count() {
  // 'count'만 가져오기 
  const count = useStore((state) => state.count);

  return <div>Count: {count}</div>;
}

function Controls() {
  // 'increment'만 가져오기
  const increment = useStore((state) => state.increment);

  return <button onClick={increment}>Increment</button>;
}

 

 

여러 개의 store를 생성하여 데이터를 분산시키고 더 직관적으로 확장할 수도 있습니다. 좀만 현실적으로 생각해보면 단일 상태는 모든 경우에 합리적이지 않습니다. 캡슐화에 어긋난 것이기도 하죠. 컴포넌트의 하위 요소들이 로컬화된 전역 상태를 갖도록 하는 것이 더 자연스러운 경우가 많습니다.

// count 데이터를 다루는 전역 store
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// 유저 input 로직을 처리하는 로컬 store
const useControlStore = create((set) => ({
  input: "",
  setInput: () => set((state) => ({ input: state.input })),
}));

function Controls() {
  return (
    <div>
      <CounterInput />
      <Button />
    </div>
  );
}

function Button() {
  const increment = useStore((state) => state.increment);
  const input = useControlStore((state) => state.input);

  return (
    <button onClick={() => increment(Number(input))}>
      Increment By {input}
    </button>
  );
}

function CountInput() {
  const input = useControlStore((state) => state.input);
  return <input value={input} />;
}

 

4. useShallow

기존 상태가 변경되면, 즉시 업데이트되는 파생된 상태를 가져오는 강력한 방식인 useShallow()를 사용할 수 있습니다.

import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";

const useLibraryStore = create((set) => ({
  fiction: 0,
  nonFiction: 0,
  borrowedBook: {},
  // ...
}));

const { fiction, nonFiction } = useLibraryStore(
  useShallow((state) => ({
    fiction: state.fiction,
    nonFiction: state.nonFiction,
  })),
);

const [fiction, nonFiction] = useLibraryStore(
  useShallow((state) => [state.fiction, state.nonFiction]),
);

const borrowedBooks = useLibraryStore(
  useShallow((state) => Object.keys(state.borrowedBooks)),
);

 

5. 특정 때에만 업데이트를 원치 않는 경우

store hook에 두 번째 인자를 전달하기만 하면 특정 때에만 업데이트를 원치 않는 경우에도 대처 가능합니다.

 

const user = useUserStore(
  (state) => state.user,
  (oldUser, newUser) => compare(oldUser.id, newUSer.id)
);

 

6. 이전 상태 기반 업데이트

zustand에서 상태는 기본 상태가 부분적 업데이트입니다.

import { create } from "zustand";

const useStore = create((set) => ({
  user: {
    username: "speardragon",
    site: "cdragon.tistory.com",
    color: "green",
  },
  premium: false,
  // 'user'객체는 영향을 받지 않습니다.
  // 'state'는 업데이트되기 전 현재 상태입니다.
  unsubscribe: () => set((state) => ({ premium: false })),
}));

 

하지만 이 코드는 first level에서만 작동하며, 더 깊은 level의 업데이트는 직접 처리해주어야 합니다.

import { create } from "zustand";

const useStore = create((set) => ({
  user: {
    username: "speardragon",
    site: "cdragon.tistory.com",
    color: "green",
  },
  premium: false,
  updateUsername: (username) => 
    // 깊은 업데이트
    set((state) => ({user: {...state.user, username}}));
}));

 

 

직접 전달하기를 원하지 않는 경우 두 번째 인자에 true값을 줘서 객체를 직접 전달하면 됩니다.

import { create } from "zustand";

const useStore = create((set) => ({
  user: {
    username: "speardragon",
    site: "cdragon.tistory.com",
    color: "green",
  },
  premium: false,
  // 데이터 초기화
  resetAccount: () => set({}, true);
}));

 

7.  비동기

zustand는 다른 라이브러리 필요 없이도 비동기를 지원합니다.

import { create } from "zustand";

const useStore = create((set) => ({
  user: {
    username: "speardragon",
    site: "cdragon.tistory.com",
    color: "green",
  },
  premium: false,
  updateFavColor: async (color) => {
    await fetch("https://api.cdragon.com", {
      method: "PUT",
      body: color,
    });
    set((state) => ({ user: { ...state.user, color } }));
  },
}));

 

8. get 매개변수로 액션 내에서 상태를 쉽게 가져오기

import { create } from "zustand";

const useStore = create((set, get) => ({
  user: {
    username: "speardragon",
    site: "cdragon.tistory.com",
    color: "green",
  },
  messages: [],
  sendMessage: ({ message, to }) => {
    const newMessage = {
      message,
      to,
      from: get().user.usernmae,
    };
    set((state) => ({
      messages: [...state.messages, newMessage],
    }));
  },
}));

 

9. 상태 값을 직접 읽고 subscribe

const count = useStore.getState().count;

useStore.subscribe((state) => {
  console.log(`new value: ${state.count}`);
})

 

따라서 속성이 많이 변경되지만 직접적인 UI가 아닌 중간 로직에 최신 값만 필요한 경우에 유용합니다.

 

export default function App() {
  const widthRef = useRef(useStore.getState().windowWidth);

  useEffect(() => {
    useStore.subscribe((state) => {
      widthRef.current = state.windowWidth;
    });
  }, []);

  useEffect(() => {
    setInterval(() => {
      console.log(`Width is now: ${widthRef.current}`);
    }, 1000);
  }, []);
  // ...
}

 

 

 

 

 

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.