현재 버전 기준, Zustand는 클로저를 중심으로 상태를 유지/관리한다.
클로저란, 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있는 함수
[코드]
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) 라는 의미로 해석되기도 한다.
createStore
는 2개의 로컬 변수를 선언state
listeners
setState
getState
getInitialState
subscribe
destory
createState
함수를 실행하여 state
초기값을 설정하고 5개의 클로저를 반환createState
는 개발자가 처음 store를 생성할 때 create
함수의 파라미터로 전달한 함수이다.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;
};
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);
});
}
};
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 store
는 React에서 제공하는 prop, useState, useReducer, Context API 등을 제외한 것을 의미함.
즉, Redux, Zustand 등 외부 상태 관리 라이브러리를 의미
useSyncExternalStoreWithSelector
함수는 zustand에서 제공하는 useSyncExternalStore
훅의 유틸리티 훅이며 내부 코드는 굉장히 복잡하기에 자세한 설명은 거두겠다.
핵심만 요약하자면, Zustand에서 던진 subscribe
클로저를 통해 store의 데이터가 바뀌었는지 확인하고, 바뀌었으면 컴포넌트를 렌더링 시킨다.
이러한 변경점을 확인하는 트리거는 setState
클로저 함수가 호출될 때 마다 한다. 위 코드를 보면 setState
함수가 반환하는 listener
에 store의 과거, 현재 값을 비교하며 렌더링하는 함수가 포함되기 때문이다.
왜 useSyncExternalStore
를 사용해서 상태를 변경/관리 하는가?
동기(synchronous) 렌더링은 상태 변경이 일어나면 관련된 컴포넌트가 모두 바뀔 때 까지 렌더링을 멈추지 않는다.
동시(concurrent) 렌더링은 상태 변경 요청 후 렌더링 중간에 다른 상태 변경 요청이 있을 시 즉시 그 시점에서 렌더링을 멈추고 변경 된 상태로 변경을 진행한다.
즉, 동시 렌더링은 마지막 과정에서 시각적인 불일치를 낳게 되고 이를 tearing 이라 한다.
이러한 문제를 막기 위해 React 18부터 useSyncExternalStore
훅을 내놓았다.