-
[Redux] 공식문서 파먹기(3) - StoreJavaScript 2023. 5. 24. 15:14
이전 포스팅에서 리듀서를 작성하는 방법과 슬라이스 리듀서를 작성하고 combineReducers 를 이용해 루트 리듀서에 조각낸 리듀서들을 합치는 방법을 알아봤습니다.
이제 이 리듀서들을 스토어에 합쳐 스토어를 만들어보겠습니다.
Store
스토어는 여러 메서드들을 가지고있습니다.
- getState() - 현재 상태를 반환합니다
- dispatch(action) - 액션을 받아 새로운 상태를 반환합니다
- subscribe(listener) - 구독하는 이벤트 리스너를 등록해 디스패치를 호출합니다
스토어는 앱에서 단 하나의 스토어만 존재해야합니다
createStore
import { createStore } from 'redux' import rootReducer from './reducer' const store = createStore(rootReducer) export default store우리가 작성했던 리듀서들을 모은 루트리듀서, createStore API를 이용해 스토어를 만듭니다.
스토어에는 단 하나의 루트리듀서만 존재합니다.
createStore의 두 번째 인자에는 미리 로드된 값을 설정할 수 있습니다.
let preloadedState const persistedTodosString = localStorage.getItem('todos') if (persistedTodosString) { preloadedState = { todos: JSON.parse(persistedTodosString) } } const store = createStore(rootReducer, preloadedState)Store의 내부코드
스토어가 내부적으로 어떻게 구현됐는지 간단하게 살펴보겠습니다.
function createStore(reducer, preloadedState) { let state = preloadedState const listeners = [] function getState() { return state } function subscribe(listener) { listeners.push(listener) return function unsubscribe() { const index = listeners.indexOf(listener) listeners.splice(index, 1) } } function dispatch(action) { state = reducer(state, action) listeners.forEach(listener => listener()) } dispatch({ type: '@@redux/INIT' }) return { dispatch, subscribe, getState } }state - 초기값으로 받아온 상태를 저장해 외부에 공개하지않고 getState를 통해서 접근합니다.
listeners[] - 리스너들을 배열의 형태로 저장합니다.
getState() - class의 getter처럼 외부에서는 getState를 통해서만 상태를 조회할 수 있습니다.
getState는 현재 상태값이 무엇이든 반환합니다.subscribe(listener) - 리스너를 받아 리스너배열에 추가해줍니다. 반환으로는 구독을 취소하는 함수를 반환해줍니다.
dispatch(action) - 액션을 받아 현재 상태와 액션을 리듀서에 전달하여 상태를 업데이트합니다. 리스너가 있는경우 배열을 순회하며 실행해줍니다.
dispatch({ type: '@@redux/init'}) - 스토어 생성시 디스패치를 한번 호출해서 초기화해줍니다.
return { dispatch, subscribe, getState }
스토어의 반환은 앞서 살펴본 3개의 메서드들을 반환해서 외부에서는 직접 접근이 불가능하게만들어줍니다.
위의 소스가 실제 리덕스의 createStore API 소스는 아니지만 실제로 거의 비슷하게 동작합니다.
실수로 스토어의 상태를 변경하는 케이스 중 하나는 store.arrayState.sort()와 같이 스토어의 배열을 정렬할때 발생합니다.
Enhancer
스토어를 생성할때 루트 리듀서와 preloadState를 인자로 전달했습니다. 마지막 인자로는 enhancer를 전달할 수 있습니다.
enhancer는 스토어를 감싸는 레이어입니다. enhancer는 원본 스토어의 dispatch, getState등의 메서드들을 enhancer 자체 버전으로 변경하거나 대체 할 수 있습니다.
enhancer 만들기
enhancer스토어를 만들어보겠습니다.
// store.js import { createStore } from 'redux' import rootReducer from './reducer' import { sayHiOnDispatch } from './exampleAddons/enhancers' const store = createStore(rootReducer, undefined, sayHiOnDispatch) // index.js import store from './store' console.log('Dispatching action') store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' }) console.log('Dispatch complete')sayHiOnDispatch 함수는 디스패치가 발생하면 콘솔에 'Hi'를 출력합니다.
이 enhancer를 store에 전달하여 스토어의 레이어를 만들었습니다.
실제 출력은
Dispatching action
Hi
Dispatching complete순으로 출력됩니다
여러개의 enhancer
슬라이스 리듀서로 리듀서들을 분할하여 combineReducers로 루트리듀서에 합친것처럼 인핸서도 여러개를 사용해야할 때가 있습니다. 하지만 createStore의 세번째 인자는 하나밖에 넣을 수 없습니다.
리덕스의 compose 함수를 이용해 인핸서를 합칠 수 있습니다.
// store.js import { createStore, compose } from 'redux' import rootReducer from './reducer' import { sayHiOnDispatch, includeMeaningOfLife } from './exampleAddons/enhancers' const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife) const store = createStore(rootReducer, undefined, composedEnhancer) // index.js import store from './store' store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' }) console.log('State after dispatch: ', store.getState())sayHiOnDispatch, includeMeaningOfLife 인핸서 두개를 compose 함수를 이용해 합쳐 composedEnhancer에 할당하여 createStore에 전달했습니다.
preloadState에는 전달값이 없어 undefined를 전달했습니다.
includeMeaningOfLife 인핸서는 getState()를 호출할때 meaningOfLife: 42 프로퍼티를 추가합니다.
출력 결과는
Hi
{todos: [...], filters: {status, colors}, meaningOfLife: 42}이렇게 인핸서들이 각각 dispatch, getState() 호출시 추가되는 내용들이 나옵니다.
위에서 preloadState에 값을 전달하지않을거라 undefined를 전달해줬습니다.
하지만 preloadState를 전달하지 않을경우 preloadState 자리에 enhancer를 전달해도 됩니다.
createStore(rootReducer, enhancer)
Middleware
인핸서는 store의 dispatch, getState 등 메서드들의 동작방식을 변경하거나 대체 할 수 있는 강력한 기능이었습니다.
dispatch의 경우 작동하는 방법만 개발자가 정의하면 됐습니다. 하지만 dispatch가 실행될때 개발자가 지정한 동작을 추가할 수 있는 방법이 있으면 좋겠습니다.
미들웨어를 사용하면 dispatch에 개발자가 지정한 동작을 추가할 수 있는 강력한 기능을 사용할 수 있습니다.
리덕스의 미들웨어는 디스패치부터 리듀서에 도달하는 시점까지 로깅, 비동기 API처리 등등의 서드파티 확장을 추가합니다.
applyMiddleware
리덕스에서 applyMiddleware라는 자체 인핸스 제공합니다.
미들웨어도 인핸스의 한 종류로서 루트스토어를 감싸는 레이어입니다.
// store.js import { createStore, applyMiddleware } from 'redux' import rootReducer from './reducer' import { print1, print2, print3 } from './exampleAddons/middleware' const middlewareEnhancer = applyMiddleware(print1, print2, print3) const store = createStore(rootReducer, middlewareEnhancer) // index.js store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })인핸서를 합칠때 compose를 사용했습니다. 미들웨어도 인핸서를 합치듯이 applyMiddleware를 이용해 정의한 인핸서들을 합쳐서 createStore에 전달해줍니다.
미들웨어 동작방식
미들웨어는 파이프라인을 형성합니다. dispatch가 호출되고 첫번째 미들웨어를 호출하고 해당 미들웨어에서는 액션을 보고 사용자가 정의한 동작을 처리하고, 만약 액션에서 첫 번째 미들웨어의 관심사가 없다면 두 번째 미들웨어로 전달합니다.
미들웨어는 리듀서와 다르게 비동기 처리에 의한 사이드이펙트가 발생할 수 있습니다.
위의 예제코드의 실행순서를 나열해보면
- dispatch(action)이 호출됩니다.
- applyMiddleware에 전달된 인핸서들로 파이프라인을 형성합니다
- print1 호출
- print2 호출
- prtin3 호출
- reducer 호출
- 스텍에 쌓인 순서데로 print1 -> print2 -> print3 -> reducer 호출됐다가 print1이 가장 나중에 반환됩니다(FILO)
커스텀 미들웨어 작성하기
커스텀 미들웨어를 항상 사용하지는 않지만, 커스텀 미들웨어를 통해 특정 기능을 추가할 수 있습니다.
커스텀 미들웨어는 세가지 중첩 함수로 작성됩니다.
function exampleMiddleware(storeAPI) { return function wrapDispatch(next) { return function handleAction(action) { // 일련의 동작을 수행하고: next(action)으로 넘어갑니다 // 혹은 파이프라인을 통해 다음 미들웨어를 호출하거나 스토어의 dispatch(action)을 호출합니다 // 여기서 스토어의 getState()를 호출할 수 있습니다. return next(action) } } } function exampleMiddeware = storeAPI => next => action => next(action);기본적으로 미들웨어는 위의 템플릿을 따릅니다
- exampleMiddleware(storeAPI - { dispatch, getState }) - 실제 미들웨어 입니다 한 번만 호출되며, 스토어의 dispatch, getState를 인자로 받습니다. 그리고 dispatch가 호출되면 첫 번째 파이프라인으로 전달합니다.
- wrapDispatch(next) - next라는 다음 미들웨어 파이프라인을 인자로받습니다. 만약 해당 미들웨어가 마지막 파이프라인이라면 다음 파이프라인은 dispatch가 호출됩니다. 해당 함수도 한 번만 호출됩니다.
- handleAction(action) - 해당 함수는 action을 인자로 받으며 dispatch가 호출될때마다 실행됩니다.
커스텀 미들웨어 작성해보기
콘솔에 로깅을 하는 커스텀 미들웨어를 작성해보겠습니다.
const loggerMiddleware = storeAPI => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', storeAPI.getState()) return result }- console.log('dispatching', action) 부분이 먼저 실행됩니다
- next로 다음 action을 전달하는데 현재 미들웨어가 마지막 파이프라인이라면 다음 액션은 dispatch(action)일 수 있습니다.
- 여기서는 마지막 파이프라인이기때문에 위의 코드에서 next(action)이 dispatch(action)이므로 새로운 상태를 반환합니다
- 그러므로 새로운 상태를 getState()로 조회가 가능합니다
- 마지막으로 결과값을 반환합니다.
미들웨어는 항상 어떤 값을 반환할 수 있습니다.
const alwaysReturnHelloMiddleware = storeAPI => next => action => { const originalResult = next(action) // Ignore the original result, return something else return 'Hello!' } const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware) const store = createStore(rootReducer, middlewareEnhancer) const dispatchResult = store.dispatch({ type: 'some/action' }) console.log(dispatchResult) // log: 'Hello!'첫 번째 파이프라인의 return 값은 실제 dispatch(action)이 호출되어 리듀서가 호출될때 반환됩니다.
또 action.type에 따라서 반환을 결정할 수 있습니다. 또한 내부에서 비동기처리도 가능합니다.
const delayedMessageMiddleware = storeAPI => next => action => { if (action.type === 'todos/todoAdded') { setTimeout(() => { console.log('Added a new todo: ', action.payload) }, 1000) } return next(action) }해당 미들웨어는 action.type에따라 원하는 액션이 들어올때 캐치해서 1초뒤 actino.payload를 로깅합니다.
그러면 미들웨어는 어떨때 사용해야할까요?
- 로깅
- 타이머
- 비동기 API 요청의 처리
- action 수정
- action을 일시정지하거나 완전히 멈출때
'JavaScript' 카테고리의 다른 글
[Redux] 공식문서 파먹기(5) - Async Logic and Data Fetching (0) 2023.06.05 [Redux] 공식문서 파먹기(4) - UI and React (0) 2023.06.02 [Redux] 공식문서 파먹기(2) - Todo List로 리덕스 이해하기 (0) 2023.05.20 [Redux] 공식문서 파먹기(1) - Essentials - Redux Overview and Concepts (0) 2023.05.18 [Javascript]변수와 메모리 (0) 2023.03.21