Zustand

Wonhyun Kwon·2024년 11월 13일
0

Zustand

목록 보기
1/1
post-thumbnail

현재 버전 기준, Zustand는 클로저를 중심으로 상태를 유지/관리한다.

0. 클로저 (Closure)

클로저란, 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있는 함수

[코드]

function outerFunc() {
  var x = 10;
  var innerFunc = function () {
    console.log(x);
  };
  
  return innerFunc;
}

/**
 * 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환
 * 그리고 콜 스택에서 outerFunc는 소멸
 */
var inner = outerFunc();
inner(); // 10

outerFunc 함수는 호출이 되면서 innerFunc 함수를 반환하고 동시에 소멸된다.
즉, outerFunc 함수는 콜스택에서 제거되었기에 당연히 안에 있는 로컬 변수(클로저에선 자유 변수(Free variable)라 칭함)인 var x = 10 또한 유효하지 않게 되어 더이상 접근할 수 없다고 판단된다.

하지만, 위 코드의 실행 결과인 var inner 는 x의 값인 10 을 반환한다.

이처럼 자신을 감싼 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수의 지역 변수에 접근할 수 있는 것을 '클로저(Closure)' 라고 한다.

더 쉽게 말하자면, 자신이 생성될 때의 환경 (렉시컬 환경, Lexical environment)을 기억하는 함수다.

클로저란 이름은 자유 변수가 내부 함수에 갇혀있다 혹은 닫혀있다 (closed) 라는 의미로 해석되기도 한다.


1. Zustand의 State 생성/변경

1) createStore 코드 구성

  • createStore 는 2개의 로컬 변수를 선언
    • state
    • listeners
  • 위 변수들을 참조하는 5개의 클로저를 생성
    • setState
    • getState
    • getInitialState
    • subscribe
    • destory
  • 함수의 마지막 부분에서 createState 함수를 실행하여 state 초기값을 설정하고 5개의 클로저를 반환
  • 함수의 파라미터인 createState 는 개발자가 처음 store를 생성할 때 create 함수의 파라미터로 전달한 함수이다.
    • ex. immer, persist, set ...

[코드]

'use strict';

/**
 * createStore
 */
var createStoreImpl = function createStoreImpl(createState) {
  /**
   * 로컬 변수
   */
  var state;
  var listeners = new Set();
  
   /**
   * 클로저: setState
   */
  var setState = function setState(partial, replace) {
    var nextState = typeof partial === 'function' ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      var _previousState = state;
      state = (replace != null ? replace : typeof nextState !== 'object' || nextState === null) ? nextState : Object.assign({}, state, nextState);
      listeners.forEach(function (listener) {
        return listener(state, _previousState);
      });
    }
  };
  
  /**
   * 클로저: getState
   */
  var getState = function getState() {
    return state;
  };
  
  /**
   * 클로저: getInitialState
   */
  var getInitialState = function getInitialState() {
    return initialState;
  };
  
  /**
   * 클로저: subscribe
   */
  var subscribe = function subscribe(listener) {
    listeners.add(listener);
    return function () {
      return listeners.delete(listener);
    };
  };
  
  /**
   * 클로저: destory
   */
  var destroy = function destroy() {
    listeners.clear();
  };
  
  /**
   * return value
   */ 
  var api = {
    setState: setState,
    getState: getState,
    getInitialState: getInitialState,
    subscribe: subscribe,
    destroy: destroy
  };
  var initialState = state = createState(setState, getState, api);
  return api;
};

var createStore = function createStore(createState) {
  return createState ? createStoreImpl(createState) : createStoreImpl;
};

2) setState 동작 방식

  var setState = function setState(partial, replace) {
    
    /**
     * partial 파라미터가 함수인지 확인
     * 함수라면, 현재 state를 partial 함수의 파라미터로 넘김
     * 함수가 아니라면, 새로운 상태 (nextState)로 간주
     */
    var nextState = typeof partial === 'function' ? partial(state) : partial;
    
    /**
     * nextState와 기존 state가 동일하지 않다면,
     */
    if (!Object.is(nextState, state)) {
      var _previousState = state;
      
      /**
       * replace의 값이 null이 아니거나 nextState가 객체가 아니라면, state를 nextState로 대체
       * 그렇지 않다면, 기존 state에 nextState를 덮어 씌움
       */
      state = (replace != null ? replace : typeof nextState !== 'object' || nextState === null) ? nextState : Object.assign({}, state, nextState);
      
      /**
       * 최종적으로 모든 listener에 state가 바뀌었음을 알림
       */
      listeners.forEach(function (listener) {
        return listener(state, _previousState);
      });
    }
  };

3) React 컴포넌트에서 zustand store 변화 감지 과정

  • create (코드단) -> createStore 호출
  • createStore -> useBoundStore 반환
  • useBoundStore -> useStore 호출
  • 최종적으로 useStore 함수가 store의 변화를 감지

[create 함수 코드]

var create = function create(createState) {
  return createState ? createImpl(createState) : createImpl;
};

[createStore 함수 코드]

var createImpl = function createImpl(createState) {
  var api = typeof createState === 'function' ? vanilla.createStore(createState) : createState;
  var useBoundStore = function useBoundStore(selector, equalityFn) {
    return useStore(api, selector, equalityFn);
  };
  Object.assign(useBoundStore, api);
  return useBoundStore;
};

[useBoundStore 함수 코드]

var useBoundStore = function useBoundStore(selector, equalityFn) {
  return useStore(api, selector, equalityFn);
};

[useStore 함수 코드]

function useStore(api, selector, equalityFn) {
  if (selector === void 0) {
    selector = identity;
  }

  /**
   * 5개의 클로저를 파라미터로 받아 useSyncExternalStoreWithSelector 함수 호출 (React 내장 함수)
   */
  var slice = useSyncExternalStoreWithSelector(api.subscribe, api.getState, api.getServerState || api.getInitialState, selector, equalityFn);
  useDebugValue(slice);
  return slice;
}

여기서 useSyncExternalStoreWithSelector 함수란, React 내장 함수이며 external store 를 구독할 수 있도록 함.
(zustand v4 부터 적용)

external storeReact에서 제공하는 prop, useState, useReducer, Context API 등을 제외한 것을 의미함.
즉, Redux, Zustand 등 외부 상태 관리 라이브러리를 의미


4) useSyncExternalStore (useSyncExternalStoreWithSelector)

useSyncExternalStoreWithSelector 함수는 zustand에서 제공하는 useSyncExternalStore 훅의 유틸리티 훅이며 내부 코드는 굉장히 복잡하기에 자세한 설명은 거두겠다.

핵심만 요약하자면, Zustand에서 던진 subscribe 클로저를 통해 store의 데이터가 바뀌었는지 확인하고, 바뀌었으면 컴포넌트를 렌더링 시킨다.

이러한 변경점을 확인하는 트리거는 setState 클로저 함수가 호출될 때 마다 한다. 위 코드를 보면 setState 함수가 반환하는 listener 에 store의 과거, 현재 값을 비교하며 렌더링하는 함수가 포함되기 때문이다.

useSyncExternalStore 를 사용해서 상태를 변경/관리 하는가?

4-1) 렌더링: 동기(synchronous) vs 동시(concurrent) 및 tearing


동기(synchronous) 렌더링은 상태 변경이 일어나면 관련된 컴포넌트가 모두 바뀔 때 까지 렌더링을 멈추지 않는다.

동시(concurrent) 렌더링은 상태 변경 요청 후 렌더링 중간에 다른 상태 변경 요청이 있을 시 즉시 그 시점에서 렌더링을 멈추고 변경 된 상태로 변경을 진행한다.

즉, 동시 렌더링은 마지막 과정에서 시각적인 불일치를 낳게 되고 이를 tearing 이라 한다.

이러한 문제를 막기 위해 React 18부터 useSyncExternalStore 훅을 내놓았다.

profile
모든 사용자가 만족하는 UI를 만드는 FE 개발자 권원현입니다.

0개의 댓글

관련 채용 정보