Zustand 렌더링 최적화

9rganizedChaos·2023년 5월 18일
1
post-custom-banner

최근 프로젝트 내의 ContextAPI를 모두 Zustand와 CustomHook 조합으로 갈아치우는 작업을 하였는데요. Zustand를 처음 도입해보느라, 엉성하게 코드를 작성한 것이 결국 작은 화를 불러일으켰습니다.

그럼 먼저 엉성한 코드의 슬픈 최후를 보고 가시겠습니다

보이시나요..? 이 아름다운 렌더링의 향연...!

AS-IS

기존에 headerContext로 관리하던 코드를 headerStore로 변경해, header에 관련된 상태와 setter함수를 모두 store에 보관하고, useHeaderStore를 통해 가져오도록 코드를 작성하였습니다.

export const useHeaderStore = create<HeaderStoreType>()(
  devtools(
    set => ({
      isTopBannerVisible: undefined,
      isScrollNavSticky: false,
      setIsTopBannerVisible: (isTopBannerVisible: boolean | undefined) =>
        set({
          isTopBannerVisible
        }),
      setScrollNavSticky: (isScrollNavSticky: boolean) =>
        set({
          isScrollNavSticky
        })
    }),
    { name: 'header-storage' }
  )
);

문제는 바로 이 zustand(정확히는 제가 쓴 Zustand 코드...)에 있었습니다.

const { isScrollNavSticky, setScrollNavSticky } = useHeaderStore(state => state);

저는 위와 같은 형태로 상태를 각 컴포넌트에서 구독하고 있었습니다!

TO-BE

위 코드의 문제는 두 가지가 있는데요. 아래와 같습니다.

1) useHeaderStore 내부에 콜백함수 전달 시 전체 state를 리턴시키고 있다는 점
2) 객체 자체를 strict하게 비교하고 있었다는 점

1 복수의 스테이트를 구독해야 할 때!

스토어로 부터 단일한 상태를 구독할 때는 아래와 같이 간단하게 작성할 수 있었습니다.

  const setIsTopBannerVisible = useHeaderStore(state => state.setIsTopBannerVisible);

두 개 이상의 상태를 구독해야 하다보니, state 전체를 반환하고, 구조분해 할당을 통해 상태를 추출해왔었는데요! 공식문서를 살펴보면, Zustand는 기본적으로 이전 상태와 새 상태를 엄격하게 비교하므로, atomic하게 상태를 추출할 것을 제안합니다!

It detects changes with strict-equality (old === new) by default, this is efficient for atomic state picks.

const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)

더 나아가 하나의 객체로 복수 상태를 뽑아낼 수도 있는데, 그럴 경우 아래와 같은 방법을 사용하라고 제안합니다.

2 얕은 비교하기!

useCustomStore의 옵션으로 우리는 Equality Function을 전달할 수 있습니다. 그리고 친절하게도 Zustand는 엄격한 비교가 아닌, 얕은 비교를 할 수 있는 Shallow라는 비교 함수를 제공해줍니다. 이 함수를 두번째 인자로 전달하면, 더 이상 변화된 상태를 비교할 때 객체의 주소값을 비교하는 것이 아니라, 내부의 원시값들 자체를 비교하게 되는 것이죠.

If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can tell zustand that you want the object to be diffed shallowly by passing the shallow equality function.

import { shallow } from 'zustand/shallow'

// Object pick, re-renders the component when either state.nuts or state.honey change
const { nuts, honey } = useBearStore(
  (state) => ({ nuts: state.nuts, honey: state.honey }),
  shallow
)

// Array pick, re-renders the component when either state.nuts or state.honey change
const [nuts, honey] = useBearStore(
  (state) => [state.nuts, state.honey],
  shallow
)

// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useBearStore((state) => Object.keys(state.treats), shallow)

그래서 저도 아래와 같은 코드를 완성하였습니다.

import { shallow } from 'zustand/shallow';

  const { isScrollNavSticky, setScrollNavSticky } = useHeaderStore(
    state => ({
      isScrollNavSticky: state.isScrollNavSticky,
      setScrollNavSticky: state.setScrollNavSticky
    }),
    shallow
  );

위와 같이 코드를 변경해주고 나니, 렌더링이 최소화된 것을 살펴볼 수 있었습니다.

참고

useStore에 두번째 인자로 전달하는 Equality Function에는 커스텀 Function도 얼마든지 적용할 수 있다고 합니다.

const treats = useBearStore(
  (state) => state.treats,
  (oldTreats, newTreats) => compare(oldTreats, newTreats)
)
profile
부정확한 정보나 잘못된 정보는 댓글로 알려주시면 빠르게 수정토록 하겠습니다, 감사합니다!
post-custom-banner

0개의 댓글