yunicornlab

Zustand의 create 함수 알아보기 (1) - set 함수 본문

Development/State

Zustand의 create 함수 알아보기 (1) - set 함수

yunicornlab 2024. 12. 11. 20:40
반응형

React에서 Zustand를 사용해 store를 생성하려면 create 함수를 사용한다.

create 함수에는 콜백 함수를 인자로 전달하는데, 이 콜백 함수에는 set과 get 함수를 인자로 받아서 사용할 수 있다고 했다.

단순히 공부할 때는 get은 상태값을 받아올 수 있는 함수이고 set은 상태를 변경할 수 있는 함수라는 것만 알고있었는데, 

이것밖에 없는 건지, create 함수는 어떻게 생긴건지 무엇일까 궁금해져서 공부해보았다.

 

먼저, zustand github 레포를 찾아서 이 set 함수 부분이 있는 곳을 찾아보았다.

https://github.com/pmndrs/zustand/blob/a958de910fb49392d5407eb0a9a776ec959ce8c5/src/vanilla.ts

 

zustand/src/vanilla.ts at a958de910fb49392d5407eb0a9a776ec959ce8c5 · pmndrs/zustand

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

github.com

A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks.

 

위치는 src > vanilla.ts에 있었고 이 중 createSotreImpl 안의 setState로 정의된 함수가 계속 말해왔던 set 함수였다.

타입 정의 부분은 빼고 핵심 부분만 가져와보면 아래와 같다.

const createStoreImpl: CreateStoreImpl = (createState) => {
  const listeners: Set<Listener> = new Set()

  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
    // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
    const nextState =
      typeof partial === 'function'
        ? (partial as (state: TState) => TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== 'object' || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  };

  const getState: StoreApi<TState>['getState'] = () => state

  const getInitialState: StoreApi<TState>['getInitialState'] = () =>
    initialState

  const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
  }

  const api = { setState, getState, getInitialState, subscribe }
  const initialState = (state = createState(setState, getState, api))
  return api as any
}

export const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore

createStoreImpl에서 상태 관리 관련 API 객체인 api 변수를 return하기 때문에

(이 코드의 마지막 부분인) createStore에는 { setState, getState, getInitialState, subscribe }가 담기게 된다.

 

React에서 사용할 코드는 src > react.ts에 있었다. 마찬가지로 타입 정의 부분은 빼고 핵심 부분만 가져오면 아래와 같다.

여기에 useStore와 create가 정의되어 있다.

해당 코드는 다음 글에서 다시 얘기해보려 한다.

import React from 'react'
import { createStore } from './vanilla.ts'

export function useStore<TState, StateSlice>(
  api: ReadonlyStoreApi<TState>,
  selector: (state: TState) => StateSlice = identity as any,
) {
  const slice = React.useSyncExternalStore(
    api.subscribe,
    () => selector(api.getState()),
    () => selector(api.getInitialState()),
  )
  React.useDebugValue(slice)
  return slice
}

const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  const api = createStore(createState)

  const useBoundStore: any = (selector?: any) => useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
  createState ? createImpl(createState) : createImpl) as Create

set 함수

createSotre 관련 코드 중 setState 함수 부분만 빼서 자바스크립트로 바꿔보면 아래와 같다.

const setState = (partial, replace) => {
  // nextState 계산: partial이 함수인지 확인
  const nextState =
    typeof partial === 'function'
      ? partial(state) // partial이 함수라면 state를 인자로 호출
      : partial;       // partial이 객체라면 그대로 사용

  // nextState가 현재 state와 다르면 상태를 업데이트
  if (!Object.is(nextState, state)) {
    const previousState = state; // 이전 상태 저장

    // replace가 true이거나 nextState가 객체가 아니면 nextState로 교체
    state =
      (replace || typeof nextState !== 'object' || nextState === null)
        ? nextState
        : Object.assign({}, state, nextState); // 그렇지 않으면 기존 상태와 병합

    // 상태 변경 후 listeners 호출
    listeners.forEach((listener) => listener(state, previousState));
  }
};

 

즉, set 함수는 partial라는 상태를 업데이트하는 함수와 replace, 이렇게 두 개의 인자를 받는다.

첫 번째 인자 자리(partial)에 상태를 변경하는 로직을 담은 함수를 넣어주면 되고,

두 번째 인자 자리(replace)는 기본값이 false인데 이는 상태를 병합한다는 의미다. 

만약 상태를 병합하는 것이 아니라 완전히 새로운 상태로 교체하고 싶다면 두 번째 인자 자리(replace)에 true를 넣어주면 된다.

 

상황을 나눠보면 다음과 같은 네 가지 상태가 올 수 있다. (실제로는 1번과 3번을 많이 쓰게 될 것 같다.)

1. partial 자리에 객체가 오는 경우 + replace가 기본값인 false인 경우 : 병합됨

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  text: 'hello',
  // 객체로 상태 변경
  setCount: () => set({ count: 10 }),
}));

export default useStore;
  • 기존 상태: { count: 0, text: 'hello' }
  • 새 상태: { count: 10 }
  • 결과 상태: { count: 10, text: 'hello' } (병합)

2. partial 자리에 객체가 오는 경우 + replace를 true로 둔 경우 : 완전히 새로 교체됨

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  text: 'hello',
  // 객체로 상태 변경 + 기존 상태를 완전히 교체
  setCount: () => set({ count: 10 }, true),
}));

export default useStore;

(권장하지는 않는다.)

 

  • 기존 상태: { count: 0, text: 'hello' }
  • 새 상태: { count: 10 }
  • 결과 상태: { count: 10 } (교체)

 

3. partial 자리에 함수가 오는 경우 +replace는 기본값인 false인 경우

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  text: 'hello',
  // 함수로 상태 변경 및 병합
  setCount: () =>
    set((state) => ({ count: state.count + 1 }), false),
}));
  • 기존 상태: { count: 0, text: 'hello' }
  • 새 상태 함수: (state) => ({ count: state.count + 1 })
  • 결과 상태: { count: 1, text: 'hello' } (병합)

4. partial 자리에 함수가 오는 경우 +replace는 기본값인 true인 경우

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  text: 'hello',
  // 함수로 상태 변경 및 교체
  setCount: () =>
    set((state) => ({ count: state.count + 1 }), true),
}));
  • 기존 상태: { count: 0, text: 'hello' }
  • 새 상태 함수: (state) => ({ count: state.count + 1 })
  • 결과 상태: { count: 1 } (교체)

console에 출력해보기

실제 console로 찍어보면 어떻게 나올지 궁금해서 해봤다.

useStore.js를 아래처럼 작성하면

import { create } from 'zustand';

console.log('create')
console.log(create())

const useStore = create((setState, getState, getInitialState) => {
  console.log(`set :\n\n${setState}`)
  console.log(`get :\n\n${getState}`)
  console.log(`init :\n\n${getInitialState}`)

  return {
      count: 0, // 상태 1
      text: '', // 상태 2
      increaseCount: () => set((state) => ({ count: state.count + 1 })), // count 증가
      updateText: (newText) => set({ text: newText }), // text 업데이트
}});

export default useStore;

 

 

이렇게 나온다.

 

 

실제 사용할 때 쓰는 create와 useStoresms 위에서 잠깐 언급했던 react.ts 파일에 정의되어 있는데,
이 부분은 다음 글에 적어보려 한다!!🥹
반응형