들어가며
redux는 어떻게 상태 관리를 하는가에 대해서 알아보기 위해서 createStore에 대해서 알아보려고 합니다.
createStore는 deprecated인데 왜 해당 함수에 대해서 알아보냐에 대한 답변은 아래와 같습니다.
The standard method for creating a Redux store. It uses the low-level Redux core
createStore
method internally, but wraps that to provide good defaults to the store setup for a better development experience.
공식 문서에 의하면 configureStore 는 middleware등 추가적인 기능을 createStore 함수를 기반으로 같이 쓸 수 있게 만든 함수이기 때문입니다.
따라서 핵심 상태 관리 원리는 여전히 createStore에 남아있고 다만 유저는 configureStore를 사용하는 것이 권장되기에 derpecated 된 것 입니다.
createStore 살펴보기
https://github.com/reduxjs/redux/blob/master/src/createStore.ts
코드 전문은 아래와 같습니다.
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {}
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext>
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {},
PreloadedState = S
>(
reducer: Reducer<S, A, PreloadedState>,
preloadedState?: PreloadedState | undefined,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext>
export function createStore<
S,
A extends Action,
Ext extends {} = {},
StateExt extends {} = {},
PreloadedState = S
>(
reducer: Reducer<S, A, PreloadedState>,
preloadedState?: PreloadedState | StoreEnhancer<Ext, StateExt> | undefined,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, UnknownIfNonSpecific<StateExt>> & NoInfer<Ext> {
if (typeof reducer !== 'function') {
throw new Error(
`Expected the root reducer to be a function. Instead, received: '${kindOf(
reducer
)}'`
)
}
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
)
}
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf(
enhancer
)}'`
)
}
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState | undefined
)
}
let currentReducer = reducer
let currentState: S | PreloadedState | undefined = preloadedState as
| PreloadedState
| undefined
let currentListeners: Map<number, ListenerCallback> | null = new Map()
let nextListeners = currentListeners
let listenerIdCounter = 0
let isDispatching = false
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = new Map()
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener)
})
}
}
function getState(): S {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState as S
}
function subscribe(listener: () => void) {
if (typeof listener !== 'function') {
throw new Error(
`Expected the listener to be a function. Instead, received: '${kindOf(
listener
)}'`
)
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
const listenerId = listenerIdCounter++
nextListeners.set(listenerId, listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
nextListeners.delete(listenerId)
currentListeners = null
}
}
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(
action
)}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
)
}
if (typeof action.type !== 'string') {
throw new Error(
`Action "type" property must be a string. Instead, the actual type was: '${kindOf(
action.type
)}'. Value was: '${action.type}' (stringified)`
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => {
listener()
})
return action
}
function replaceReducer(nextReducer: Reducer<S, A>): void {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
nextReducer
)}`
)
}
currentReducer = nextReducer as unknown as Reducer<S, A, PreloadedState>
dispatch({ type: ActionTypes.REPLACE } as A)
}
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer: unknown) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError(
`Expected the observer to be an object. Instead, received: '${kindOf(
observer
)}'`
)
}
function observeState() {
const observerAsObserver = observer as Observer<S>
if (observerAsObserver.next) {
observerAsObserver.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
dispatch({ type: ActionTypes.INIT } as A)
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<S, A, StateExt> & Ext
return store
}
결국 요약만 하면 currentState를 통해 상태를 기억하고 외부에서 받아온 reducer를 통해서 상태를 업데이트 합니다.
Map으로 선언한 listener를 통해서 구독자를 관리합니다. 이후 반환값으로 상태를 얻는 getState, 구독하는 subscribe, 상태를 업데이트하기 위한 dispatch함수들을 반환합니다.
(상태 관리에 좀 더 집중하기 위해 여타 에러처리와 replaceReducer , observable에 대해서는 넘어가겠습니다.)
이제 너무 방대한 양이기에 이를 간단하게 요약해서 코어만 추출해보려고 합니다.
createStore 코어만 작성하기
const createStoreCore = (reducer) => {
let state;
let listeners = new Map();
let listenerIdCounter = 0;
const getState = () => state;
const subscribe = (listener) => {
const id = listenerIdCounter++;
listeners.set(id, listener);
return () => {
listeners.delete(id);
};
};
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
dispatch({});
return { getState, subscribe, dispatch };
};
상태를 수정하는 것은 reducer를 통해서 받아서 상태가 순수할 수 있도록 하고,
listners를 통해 구독시킵니다. 이후 dispatch를 할 때 (상태가 수정될 때) 는 구독자에게 알려야 하고, 구독이 끝났을 때는 listeners에서 제거해야 합니다.
이전에 '리엑트 훅을 활용한 마이크로 상태 관리 책'을 통해서 확인한 zustand, jotai, valtio 등과 원리는 비슷하네요.
다만 상태는 createStore 내부에서 클로저를 통해 관리하고 있습니다.
react에서는 어떻게 리렌더링이 되는가?
redux에서는 상태 관리와 리렌더링의 주체가 관심사가 나뉘어져 있습니다.
리렌더링은 react-redux 라이브러리의 훅들을 기반으로 진행됩니다.
대부분의 상태관리 라이브러리는 외부 상태를 useSyncExternalStore 훅을 통해서 합니다.
이때 redux에서는 구 버전(useSyncExternalStore 훅이 나온 18버전 이전)의 리엑트에 대응하기 위해서
use-sync-external-store 을 활용합니다.
해당 코드는 useSelector 내부에서 확인할 수 있습니다.
export const initializeUseSelector = (fn: uSESWS) => {
useSyncExternalStoreWithSelector = fn
}
dispatch 시 내부에서 listener에게 상태 변화가 감지가 되고, 콜백함수로 작동하여 리렌더링을 일으킵니다.
참고문헌
https://redux-toolkit.js.org/api/configureStore
configureStore | Redux Toolkit
redux-toolkit.js.org
https://ko.react.dev/reference/react/useSyncExternalStore
useSyncExternalStore – React
The library for web and native user interfaces
ko.react.dev
https://github.com/reduxjs/react-redux
GitHub - reduxjs/react-redux: Official React bindings for Redux
Official React bindings for Redux. Contribute to reduxjs/react-redux development by creating an account on GitHub.
github.com
'프론트엔드 > 상태관리' 카테고리의 다른 글
Proxy를 활용한 Valtio의 전역상태관리 (feat. proxy-compare라이브러리) (5) | 2024.09.23 |
---|---|
React에서 Zustand 리렌더링 최적화하기 (0) | 2024.09.17 |
Redux의 상태관리와 클로저 (0) | 2024.05.11 |