Redux core library는 의도적으로 정해진 룰이 없습니다. 이는 store setup, reducer 디자인 등 모든 것들을 사용자가 원하는대로 사용할 수 있다는 것입니다.
이러한 특징은 사용자에게 유연함을 가져다주고 코드를 원하는대로 작성하도록 도와줍니다. 하지만 동시에 경험이 적거나 유연함이 필요없는 상황에선 단점으로 작용할 수 있죠. 큰 어플리케이션을 만들게 된다면 그저 형식적인 default 코드가 필요한 경우도 많이 존재합니다.
Redux toolkit은 redux의 usecase들을 쉽게 다루기 위해 탄생했죠. 이를 위해 몇 가지 툴킷 사용법을 살펴보며 어떻게 redux-toolkit을 잘 사용할 수 있을지 알아보겠습니다.
모든 리덕스 어플리케이션은 configure와 Redux store생성이 필요합니다. 이는 일반적으로 몇 가지 스탭을 포함하고 있습니다.
Configuring Your Store의 예시는 전형적인 store setup 과정을 보여줍니다.
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
읽기 쉬운 예제이지만, 모든 단계가 직관적으로 이해되지는 않을 수 있습니다.
configureStore
는 아래와 같은 이슈들의 해결을 도와줍니다.
applyMiddleware
and compose
for you automaticallyconfigureStore
는 각각 특정한 목적이 있는 몇 가지 미들웨어를 디폴트로 더해줍니다.
redux-thunk
is the most commonly used middleware for working with both synchronous and async logic outside of components이는 store setup code 그 자체는 짧고 쉬우며 좋은 default behavior를 제공해준다는 것을 의미합니다.
이를 사용하는 가장 쉬운 방법은 단순하게 root reducer 함수를 reducer
파라미터로 넘겨주는 것입니다.
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
또한 slice reducers로 구성된 객체도 넘길 수 있으며 configureStore
는 내부적으로 combineReducers
를 호출해줍니다.
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
이런 코드는 one level of reducers에만 동작하는 것을 기억합시다. 만약 그 이상의 중첩된 reducers를 원한다면, 중첩을 처리하기 위해 combineReducers
를 호출해야 합니다.
만약 store setup을 커스터마이징 하고 싶다면, 추가적인 옵션을 전달할 수 있습니다. 아래는 hot reloading 사용에 대한 예시입니다.
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: [monitoreReducersEnhancer],
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
middleware
아규먼트를 제공하는 경우, configureStore
는 사용자가 제공하는 리스트에 들어있는 미들웨어만을 사용합니다. 만약 커스텀 미들웨어들과 default를 같이 사용하고 싶다면, callback notation을 사용해 getDefaultMiddleware
를 호출하고 리턴하는 middleware array에 포함시키며 됩니다.
Reducers는 Redux 개념중 가장 중요한 부분입니다. 전형적인 Reducer 함수는 다음과 같아야 합니다.
type
field of the action object to see how it should respondreducer에서 어떤 conditional logic이던 사용할 수 있지만, 가장 보편적인 접근은 switch
문을 사용하는 것입니다. 왜냐하면 multiple possible values for a single field를 다루는데 직관적인 방법이기 때문입니다. 한편, 많은 사람들이 switch statements를 좋아하지 않습니다. Redux docs에서는 writing a function that acts as a lookup table based on action types에 대한 예시를 보여주었지만, 함수를 커스터마이징 하는 것은 유저들의 몫으로 남겨두었습니다.
리듀서를 작성하며 마주하는 또 다른 대표적인 고통 포인트(...)는 state update를 반드시 immutable하게 해야 한다는 것입니다(updating state immutably). 자바스크립트는 mutable language이고, updating nested immutable data by hand는 어려운 작업입니다. 또한 실수를 만들기 너무 쉽습니다.
createReducer
"lookup table" 접근이 유명하기에, 리덕스 툴킷은 Redux docs에서 본 방식과 비슷한 createReducer
함수를 포함하고 있습니다. 또한 createReducer
유틸리티는 사용성을 더 좋게 만드는 특별한 "마법"을 가지고 있습니다. 내부적으로 immer
라이브러리를 사용하여 사용자가 코드에서 몇몇 데이터를 "mutates"하더라도 실제로 updates를 immutable하게 적용해줍니다. 이러한 방식은 아주 효과적으로 "reducer 내부에서 실수로 상태를 mutate하는 것"을 불가능하게 만들었습니다.
일반적으로 switch
문을 사용하는 모든 Redux reducer는 createReducer
로 즉각적인 대체가 가능합니다. switch 내의 각각의 case
들은 createReducer
에 전달되는 object의 key가 됩니다. spreading objects or copying arrays와 같은 immutable update logic은 직접적인 "mutation"으로 대체 가능합니다. 물론 immutable updates를 이전 그대로 두어도 상관 없습니다.
createReducer
를 어떻게 사용할 수 있는지 보여주는 몇 가지 예시입니다. switch문과 immutable updates를 사용하는 전형적인 "todo list" reducer부터 시작해보겠습니다.
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
copied array를 새로운 todo entry로 리턴하기 위해 state.concat()
을 사용하는 부분이나, copied array for toggle case를 리턴하기 위해 state.map()
을 사용하는 것, update가 필요한 todo를 copy하기 위해 object spread operator를 사용하는 부분들을 눈치챘을 것입니다. 상태의 Immutable을 유지하기 위해서이죠.
createReducer
를 이용하면 우리는 위의 예시를 아래와 같이 짧게 줄일 수 있습니다.
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index)
})
})
상태를 "mutate" 할 수 있는 능력은 깊게 중첩된 상태를 업데이트 하려고 할 때 특히 효과적입니다. 아래의 복잡하고 고통스러운 코드를 살펴봅시다.
case "UPDATE_VALUE":
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
이런 코드가 아래와 같이 단순하게 표시가 됩니다
updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}
코드가 훨씬 좋아진 것을 볼 수 있습니다.
createReducer
리덕스 툴킷의 createReducer
함수는 매우 유용합니다. 사용시 아래와 같은 내용을 기억해야 합니다.
createReducer
function더 자세한 부분은 createReducer
API reference을 참고할 수 있습니다.
출처