(탐구) Zustand 파고들기, 그리고 다른 친구들과의 비교

한중일 개발자·2024년 3월 20일
0

React Libraries

목록 보기
3/3


곰돌이 귀엽다.

상태 관리 라이브러리를 사실 그동안 사용해오지 않았다. 웹개발 접한지 1년이 되어가고, 프론트의 길을 걷는 사람 입장에선 이게 말이 되나 싶지만, 진행하던 학교 커뮤니티 프로젝트가 전역 상태 관리를 딱히 필요로 하지 않았던 이유가 크다. 공부 목적으로 억지로 넣어볼 수는 있었겠지만, 정말... 유저 정보 외에는 전역으로 사용할 정보도 딱히 없고, 그마저도 필요할 경우에 Amplify에서 준 내장 메서드를 사용하면 간편하게 가져올 수 있고, 이를 사용하는 컴포넌트도 많지 않아 사용하지 않았다.

다만 최근 새로운 프로젝트를 진행하면서는 전역 상태 관리의 필요성이 생겼고, 수많은 라이브러리들 중 고민 또 고민 끝에 Zustand를 도입하여 사용하고 있다. 이와 관련해서 최근 본 면접에 왜 다른 라이브러리 Zustand를 택하였는지 자세히 물어봤어서, Zustand의 기초와 다른 라이브러리와의 비교점을 정리해보려 한다.

Zustand?

A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn't boilerplatey or opinionated, but has enough convention to be explicit and flux-like.

Don't disregard it because it's cute, it has claws! Lots of time was spent to deal with common pitfalls, like the dreaded zombie child problem, React concurrency, and context loss between mixed renderers. It may be the one state manager in the React space that gets all of these right.

공식 소개다. 작은 용량과 속도, 확장 가능성을 무기로 내세우는 상태 관리 라이브러리다. Hook을 기반으로 만들어졌고, boilerplate가 딱히 있는건 아니지만 쉽게 flux-like 하게 사용 가능하도록 만들어져있다.

또한 dreaded zombie child problem, React concurrency, context loss between mixed renderers같은 문제도 해결한다고 한다. 이게 뭔진 천천히 알아보자.

장점

  • 번들 사이즈가 정말 작다. 1.16kb.
  • Async를 지원한다.
  • 사용하기 정말 쉽다.
  • 상태가 변경된다고 불필요한 제랜더링이 모두 일어나지 않는다.

사용법

코드들은 공식 문서의 것을 사용한다.

Store 만들기

// Create your store, which includes both state and (optionally) actions
import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  updateBears: (newBears) => set({ bears: newBears }),
}))

Hook의 방식으로 스토어가 만들어지는 방식이 꽤나 독특하다. 내부에 원하는 상태들, 그리고 함수들을 넣을 수 있다. 위는 곰의 마릿수와 이 마릿수를 변경하는 함수들을 포함한다.

컴포넌트에 바인딩하여 사용하기

function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

Hook처럼 useStore를 사용해 상태를 불러오고, 함수도 사용하고... 끝이다! 정말 간단하다.

사용 예시

아날로그 시계 프로젝트를 진행한다고 해보고, 여기에 Zustand로 상태 관리를 진행해보자.

Store 만들기

import { create } from "zustand";

interface TimeState {
  currentTime: Date;
  hourAngle: number;
  minuteAngle: number;
  secondAngle: number;
  currentTimeString: string;
  updateTime: () => void;
}

// 현재 시간과 문자열 표시, 시침, 분침, 초침의 각도 계산
const calculateTimeDetails = () => {
  // 현재 시간 업데이트
  const currentTime = new Date();
  const currentTimeString = currentTime.toLocaleTimeString();
  const hours = currentTime.getHours();
  const minutes = currentTime.getMinutes();
  const seconds = currentTime.getSeconds();
  
  // 시침, 분침, 초침 각도 업데이트
  const hourAngle = ((hours % 12) + minutes / 60) * (360 / 12);
  const minuteAngle = (minutes + seconds / 60) * (360 / 60);
  const secondAngle = seconds * (360 / 60);

  return {
    currentTime,
    currentTimeString,
    hourAngle,
    minuteAngle,
    secondAngle,
  };
};

export const useTimeStore = create<TimeState>((set) => ({
  ...calculateTimeDetails(), // 현재 시간 기반으로 초기 상태 설정
  updateTime: () => {
    set(calculateTimeDetails());
  },
}));

해당 프로젝트에선, 현재 시간을 받아 해당 시간 기반으로 시간의 문자열 값, 시, 분, 초 값과 시침, 분침, 초침의 각도를 중앙적으로 관리한다. 현재 시간 기반으로 위처럼 함수로 초기값을 계산하고, 할당 가능하며, 시간을 업데이트 할 함수인 updateTime도 포함한다.

컴포넌트에서의 사용

export default function Clock() {
  ...
  const { hourAngle, minuteAngle, secondAngle, updateTime, currentTimeString } =
    useTimeStore((state) => ({
      hourAngle: state.hourAngle,
      minuteAngle: state.minuteAngle,
      secondAngle: state.secondAngle,
      updateTime: state.updateTime,
      currentTimeString: state.currentTimeString,
    }));
}

훅의 형태로 state들을 불러오고 있다. 그런데 state => 방식으로 불러오고있는데, 왜일까?

전체 스토어를 구독하게 된다면, 스토어의 값이 하나라도 변경되면 구독중인 컴포넌트가 불필요하게 재렌더링 된다. 여기서 zustand가 shallow 함수로 위처럼 필요한 state만 구현하게 해주는데 (사실 여기선 안써도 상관 없긴 하다), 해당 스테이트의 값이 이전값과 다른지 비교후 달라졌을 때만 렌더링을 시켜준다.

또한 object를 상태로 사용할 경우 보통 ===으로 비교하지만, 메모리 주소 값이 변경되는 경우에도 렌더링이 다시 일어남으로 shallow를 사용하여 == 비교를 통해 쓸모없는 렌더링을 줄일 수 있다.

이렇게 간단하다!

다른 상태관리 라이브러리와의 비교

Redux와 비교해보는것이 면접에서 나왔던 질문인데, 적당히 잘 답했지만 다음번엔 더 잘 답하기 위해 정리해본다.

Redux

Conceptually, Zustand and Redux are quite similar, both are based on an immutable state model. However, Redux requires your app to be wrapped in context providers; Zustand does not.

공식 문서에서의 설명이다. 둘다 불변 상태 모델 기반이지만, Redux는 Context Provider로 Wrapping을 해야 하지만 Zustand는 Hook 호출 방식이기에 그럴 필요가 없다.

두 라이브러리 모두 selector를 사용하여 선택적으로 재렌더링이 이루어 지게 할 수 있다.

Redux와 비교 시 기본적으로 초기 설정 코드가 매우 간결하다.

참고 자료

Zustand vs Redux (feat. data fetching)
Zustand Documentation

profile
한국에서 태어나고, 중국 베이징에서 대학을 졸업하여, 일본 도쿄에서 개발자로 일하고 있습니다. 유창한 한국어, 영어, 중국어, 일본어와 약간의 자바스크립트를 구사합니다.

0개의 댓글