Redux ์ฌ์ฉ์ ์์ด์ ์์ฌ์ ๋ ์ ๋ค๊ณผ ๊ทธ๊ฑธ ๊ณ ์ณ๋๊ฐ ๊ฒฝํ์ ์จ๋ณด๋ ค ํ๋ค. (์์งํ, Redux๋ MobX์ ๋นํด ๋ถ์กฑํ ์ ์ด ๋ง๋ค.)
๋ฆฌ๋์ค๋ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์๋ค.
ํ๋์ฉ ๊น์์ด๋ณด์.
Redux๋ ์คํ
์ดํธ์ ๋ณ๊ฒฝ์ด ์ผ์ด๋๋ฉด ๋ชจ๋ ์ฐ๊ฒฐ๋(connect
) ์ปดํฌ๋ํธ๋ค์๊ฒ ์๋ก์ด ์คํ
์ดํธ๋ฅผ
์ ๋ฌํด์ค๋ค. ์ฌ๊ธฐ์, ์คํ
์ดํธ์ ์ผ๋ถ์ ์ดํฐ๋ ์ด์
์ด๋(๋ฐฐ์ด์ด๋ ๋งต์ ์ ๋ ฌ, ํํฐ) ๊ณ์ฐ์ด ์์ ๊ฒฝ์ฐ, ๊ฐ๊ฐ์
์ปดํฌ๋ํธ๋ค์ ์์ ์ด ํ์๋ก ํ๋ ๋ถ๋ถ์ ๋ณ๊ฒฝ์ด ์์์๋ ๋ถ๊ตฌํ๊ณ ๋ค์ ๊ณ์ฐ์ ํด์ผํ๋ค.
์ฌ๊ธฐ์ Memoization์ ์ด์ ์ ๋ฐ์ ์ธ์๋ค๊ณผ ๋ฆฌํด ๊ฐ์ ๊ธฐ์ตํด๋์ด, ์๋ก ๋ฐ์ ์ธ์๊ฐ ์ด์ ๊ณผ ๋์ผ ํ ๊ฒฝ์ฐ, ๊ธฐ์ตํด๋ ๋ฆฌํด๊ฐ์ ๊ทธ๋๋ก ๋๋ ค์ฃผ๊ฒ ๋ง๋ค์ด์ ธ์๋ค.
์ง์ ๋ง๋ค๊ธฐ ์ด๋ ค์ด๊ฑด ์๋์ง๋ง, Reselect๋ฅผ ์ฌ์ฉํ๋ฉด ์ข ๋ ๊ฐ๋ ฅํ๊ฒ Memoization์ ์ฌ์ฉํ ์ ์๋ค.
Reselect๋ 2๊ฐ์ ๋จ๊ณ๋ก Redux์ ์คํ ์ดํธ๋ก๋ถํฐ ๊ณ์ฐ์์ ํ์ํ ์ธ์๋ฅผ ๊ตฌํ๋ ํจ์๋ค๊ณผ, ์ด ์ธ์๋ค๋ก ๊ฒฐ๊ณผ๊ฐ์ ๋ง๋๋ ๊ณ์ฐ ํจ์๋ก ๋์ด์๋ค.
๊ณ ๋ก ๊ฐ์ด ๋ณ๊ฒฝ๋์ด State์ ์ธ์คํด์ค๊ฐ ์๋ก ๋ง๋ค์ด์ ธ๋, ์ธ์๋ก ํ์๋ก ํ๋ ๊ฐ์ด ์์ง ๋ณ๊ฒฝ์ด ์๋ฌ์ผ๋ฉด ์ด์ ๊ฒฐ๊ณผ๊ฐ์ ๋ฐ๋ก ์ฌํ์ฉ ํ ์ ์๋ค.
import { createSelector } from 'reselect'
// ์คํ
์ดํธ์์ ์ด๋ค ๊ฐ์ ์ธ์๋ก ์ธ์ง ์ฐพ์์ฃผ๋ ํจ์๋ค์ด๋ค.
const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos
export const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
// ์ฐพ์์ง ์ธ์์ ๋ํด ๊ณ์ฐ์ ํํ๋ค.
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)
์ฐ์์ ์ธ ์ก์
๋์คํจ์น์, ๋ฆฌ๋์ค๋ ๋งค ๋์คํจ์น๋ง๋ค ์ฐ๊ฒฐ๋ ์ปดํฌ๋ํธ์๊ฒ ์ ์คํ
์ดํธ๋ฅผ ์ ๋ฌํด์ค๋ค.
์คํ
์ดํธ๊ฐ ์๋ฐ๋๋ค๋ฉด, mapStateToProps
๋ฅผ ๋๋๋ฆฌ์ง ์๊ฒ ์ง๋ง, ๋งค ๋์คํจ์น๋ง๋ค ์คํ
์ดํธ๊ฐ ๋ฐ๋๊ณ ์์
์ค๋ช
ํ Memoization์ด ์ ๋๋ก ์๋์ด์์ผ๋ฉด ์ฑ์ ์์ฒญ๋๊ฒ ๋๋ ค์ง ๊ฒ์ด๋ค.
๋ฌผ๋ก ๋์ฑ
์ ๋์คํจ์น๋ฅผ ์์ฃผ ์ํ๋ฉด ๋๋ค.
๋ผ๋ ๋ฐฉ๋ฒ์ด ์๋ค. ์ด๋ ์์ ์ก์
์ ์ฌ๋ฌ๊ฐ ๋์คํจ์นํ๋๊ฑธ
์ผ๊ฐ๊ณ , ์์ฃผ ๊ฐ๋ ฅํ ํ๋์ ์ก์
์ ์์ ๋ง๋ค๋ฉด ๋๋ค. ๋ฌธ์ ๋ ์ฌ๊ธฐ์ ์์๋๋ค. ์ด๋ ๊ฒ ๋ง๋ ์ก์
์ด ๊ด๋ฆฌํ๊ธฐ
์ฌ์ด ์ฝ๋์ผ๊น? ํจ์๋ฅผ ๋ง๋ค ๋๋ ์์ ์ผ์ ํ์คํ๊ฒ ํ๋ ๋
์๋ค์ ๋ง์ด ๋ง๋ค์ด์ผ ํ
์คํธ๋ ์ฝ๊ณ , ์ฌ์ฌ์ฉ์ฑ๋
๋์์ง๊ณ ์ดํดํ๊ธฐ๋ ์ฌ์์ง๋ค. ๊ณ ๋ก, ๊ฐ๋ฐ ๊ฒฝํ์ ๋์ด๊ธฐ ์ํด์๋ ์๊ณ ํ๋์ ์ผ์ ํ์คํ ํด๋ด๋ ์ก์
์
๋ง์ด ๊ฐ์ง๋๊ฒ ์ด๋กญ๋ค.
๊ณ ๋ก, ์ฐ๋ฆฌ๋ ์ผ๋ จ์ ์ก์ ์ ๋ํด ๋ฆฌ๋์ฑ์ ๋ชจ๋ ๋ค ๋๋ด์ฃผ๊ณ ์ปดํฌ๋ํธ๋ค์๊ฒ ๋๊ฒจ ์ค ํ์๊ฐ ์๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ช๊ฐ์ง์ ์์ ๊ธฐ์กด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋๋ฐ, ์ฌ์ฉ์ฑ์ด ๋ณ๋ก ๋ง์์ ์๋ค๊ณ , Redux Saga์ ์ ๋๋ก ๋์ํ์ง๋ ์๋๊ฒ ๋๋ถ๋ถ์ด์๋ค.
๊ทธ๋์, ์๋กญ๊ฒ Batch Enhancer๋ฅผ ๋ง๋ค์๋ค.
const sagaMiddleware = createSagaMiddleware()
const middlewareEnhancer = applyMiddleware(sampleMiddleware)
const enhancer = compose<Redux.StoreEnhancerStoreCreator<State>>(
middlewareEnhancer,
batchEnhancer(sagaMiddleware)
// Saga๋ฅผ ์ฐ์ง ์๋๋ค๋ฉด, ๋ฏธ๋ค์จ์ด๋ฅผ ๋๊ฒจ์ฃผ์ง ์์๋ ๋๋ค.
// batchEnhancer(),
)
// ์ ์ฉ์ ๊ฐ๋ณ๊ฒ ์ธํธ์๋ง ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
const store = createStore(reducer, enhancer)
// ์ด์ ๋ฐฐ์ด๋ก ๋์คํจ์น๊ฐ ๊ฐ๋ฅํ๋ค.
store.dispatch([
{
type: 'SayHello'
},
{
type: 'SayHello'
},
{
type: 'SayHello'
}
])
// `put` ์ดํํธ์์๋ ๋๊ฐ์ด ์ธ ์ ์๋ค.
function* saga() {
while (true) {
yield take('SayHello')
yield put([
{
type: 'SayBye'
},
{
type: 'SayBye'
},
{
type: 'SayBye'
}
])
}
}
์ด์ , ๋ฐฐ์ด์ ๋ด๊ธด ์ก์ ๋ค์ ๋ค ๋ฆฌ๋์คํ๊ณ ๋์ ์ปดํฌ๋ํธ๋ค์๊ฒ ์ต์ข ์ ์ธ ๊ฒฐ๊ณผ๋ฌผ๋ง ์๋ ค์ฃผ๊ฒ ๋๋ค.
๋ ๋ค๋ฅธ ๋ฌธ์ ๋ก, combineReducers
๊ณผ switch
๊ตฌ๋ฌธ์ด ๋ณ๋ก ๋ง์์ ์๋ค์๋ค. ์ฒ์ ์ฐ๊ธฐ์ ์ฝ์ง๋ง,
์ฑ์ด ์ปค์ง์๋ก ๋งค ์ก์
๋ค์ ๋ชจ๋ ์ค์์น ๊ตฌ๋ฌธ์ผ๋ก ํต๊ณผ์ํค๋๊ฑด ๋๋ฌด ๋นํจ์จ ์ ์ธ๋ฏ ํด๋ณด์๋ค.
์ด์, Mapped Reducer๋ฅผ ๋ง๋ค์๋๋ฐ, ์ก์ ํ์ ์ ํค๋ก ๊ทธ์ ๋ง์ถฐ ์คํ ์ดํธ๋ฅผ ๋ณ๊ฒฝํ๋ ํจ์๋ฅผ ๊ฐ์ผ๋ก ๊ฐ์ง๋ ๋งต์ ๋ง๋ค์๋ค. ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ ๋น์ทํด์ง ๋๋์ธ๋ฐ, ๊ธฐ๋ฅ์์ผ๋ก combineReducers์ ํฌ๊ฒ ๋ค๋ฅธ๊ฑด ์๋ค.
์ฅ์ ์ ์ก์ ์ ๋ํด ํน์ ๋ฆฌ๋์๋ง ์๋ ์ํฌ ์ ์๋ค๋ ์ ์ด๊ณ , Map์ ํ์ฉํ๊ธฐ์ ์ก์ ์๊ฐ ๋ง์์ง ์๋ก Switch๋ณด๋ค ์ธ๋ฑ์ฑ์์ ์ ๋ฆฌํ ๊ณ ์ง๋ฅผ ์ ํ ์ ์๋ค.(Map์ ํค๋ฅผ ํด์๊ฐ์ผ๋ก ๋ค๋ฃฌ๋ค.)
import { createStore } from 'redux'
import { MappedPipeReducer } from 'typed-redux-kit.mapped-reducer'
import { PureAction, PayloadAction } from 'typed-redux-kit.base'
enum ActionTypes {
Plus = 'test_Plus',
Set = 'test_Set'
}
namespace Actions {
export interface Plus extends PureAction<ActionTypes.Plus> {}
export interface Set
extends PayloadAction<
ActionTypes.Set,
{
count: number
}
> {}
}
interface State {
count: number
}
const plusReducer = (state: State, action: Actions.PlusAction) => ({
...state,
count: state.count + 1
})
const setReducer = (state: State, action: Actions.SetAction) => ({
...state,
...action.payload
})
// ์ด๊ธฐ ์คํ
์ดํธ๋ ์คํ ์ด์ ๋ฃ์ด์ค๋ ๋๋ค.
const reducer = new MappedPipeReducer<State>({
initialState: {
count: 0
}
})
reducer
.set(ActionTypes.Plus, plusReducer)
.set(ActionTypes.Set, setReducer)
// ๋ณต์์ ์ก์
ํ์
์ ๋ํด์๋ ๊ฐ๋จํ ๋ฐฐ์ด๋ก ๋ฃ์ด์ค ์ ์๋ค.
.set([ActionTypes.Plus, ActionTypes.Set], anotherReducer)
// String enum๋ ๋ฐ๋ก ๋ณํ์์ด ๋ฐ๋ก ๋ฃ์ด์ค ์ ์๋ค.
.set(ActionTypes, yetAnotherReducer)
const store = createStore(reducer.reduce)
store.dispatch({
type: ActionTypes.Plus
} as Actions.Plus)
Redux์์ ๊น์ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ค๋ฃจ๊ธฐ ๋งค์ฐ ๊ท์ฐฎ๋ค. ์ด๋ ๋ณ๊ฒฝ ์ฌํญ์ ํญ์ Immutableํ ์ํ๋ก ์ ์งํด์ผํ๊ธฐ ๋๋ฌธ์ธ๋ฐ, ๊ทธ๋ฅ ์ค๋ธ์ ํธ๋ก ๊น์ ๊ณณ์ ์๋ ๊ฐ์ ์์ ํ๋ ค ํ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ๋๋ค:
const myReducer = (state, action) => ({
...state,
depth1: {
...state.depth1,
depth2: {
...state.depth1.depth2,
depth3: {
...state.depth1.depth2.depth3,
depth4: action.payload
}
}
}
})
์ฝ๋ฐฑํฌ ์ฒ๋ผ ํผ๋ผ๋ฏธ๋๊ฐ ๋์ด๊ฐ...
์ด์ ํ์ด์ค๋ถ์ด ๋ง๋ Immutable.js๋ ๋ ๋์ API๋ก ์ด๋ฅผ ์ฝ๊ฒ ๋ค๋ฃจ๊ฒ ํด์ค๋ค.
const myReducer = (state, action) =>
state.setIn(['depth1', 'depth2', 'depth3', 'depth4'], action.payload)
๊ทผ๋ฐ, ๋ฌธ์ ์ ์... getIn
, setIn
, ...In
๊ณผ ๊ฐ์ ๋ฉ์๋๋ ๋ฌธ์์ด์ ๋ฐฐ์ด๋ก ํค๋ค์ ๊ฐ์ ธ์์ ๊ฐ์
์ฐพ์ ๋ด๋๋ก ๋ง๋ค์ด์ ธ ์์ผ๋ฏ๋ก, ํ์
์ถ๋ก ์ด ๋ถ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ด๋ฏ๋ก, ํค๊ฐ์ ์๋ชป๋ฃ์ผ๋ฉด ๋ฐํ์๊น์ง ๊ฐ์ผ ์๋ฌ๋ฅผ
์ฐพ์ ์ ์๊ธฐ ๋๋ฌธ์, ํญ์ ํ
์คํธ๋ฅผ ํ ํ์๊ฐ ์๋ค.
์ด๋ฅผ ํ์
์คํฌ๋ฆฝํธ๋ก ํ์ด๋ง๊ธฐ์ํด์ ...In
์ ์ฐ๋ฉด ์๋๋๋ฐ, ๊ทธ๋ฌ๋ฉด...:
const myReducer = (state, action) =>
state.update('depth1', depth1 =>
depth1.update('depth2', depth2 =>
depth2.update('depth3', depth3 =>
depth3.update('depth4', depth4 => action.payload)
)
)
)
๋ ๋ค๋ฅธ ํผ๋ผ๋ฏธ๋๊ฐ ์๊ธด๋ค.
์ด์ ํด๊ฒฐ์ฑ ์ ๊ณ ๋ฏผํ๋ค MobX์ Observable Object๋ฅผ ๋ณด๊ณ ์๊ฐ์ ๋ฐ์ Trackable์ ๋ง๋ค์๋ค.
Trackable์ ๋ง๊ทธ๋๋ก ์ถ์ ์ ํด์ฃผ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ฒด๋ก ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ํ์ ์ ๋จ๊ฒจ๋๊ณ ๋ง์ง๋ง์ผ๋ก ๋ฆฌ๋์์์ ๋๊ฐ ๋ ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ์๋ก์ด ์ธ์คํด์ค๋ก ๋ง๋ค์ด์ฃผ๋ฉด ์ค๋ค.
import * as Redux from 'redux'
import { trackEnhancer, TrackableRecord } from '../lib'
const CountRecord = TrackableRecord({
count: 0
})
type CountRecord = TrackableRecord<{
count: number
}>
type State = TrackableMap<string, CountRecord>
const defaultChildState = CountRecord({
count: 0
})
const defaultState: State = new TrackableMap({
a: defaultChildState
})
const myReducer = (state: State = defaultState, action: Redux.Action) => {
if (action.type === 'add') {
// ์ด์ ๋ถ๋ณ์ฑ ์ ๊ฒฝ์์ฐ๊ณ ์ ๋๊ฒ ๋ฐ๊ฟ๋ ๋๋ค!
state.get('a').count++
}
return state
}
// ์๋ํ๋ฉด `trackEnhancer`๊ฐ ๋ณ๊ฒฝ์ ์ถ์ ํด์ ๋๋ฌ์์ง(๋ณ๊ฒฝ๋) ๋ถ๋ถ์ ๊น๋ํ๊ฒ ํด์ฃผ๊ธฐ ๋๋ฌธ์!
const store = Redux.createStore(myReducer, trackEnhancer)
store.dispatch({
type: 'add'
})
const reducedState = store.getState()
expect(reducedState.get('a').count).toBe(1)
// ๊ณ ๋ก ๋ณ๊ฒฝ์ ๋ฎคํฐ๋ธํ๊ฒ ํ์ง๋ง ๊ฒฐ๊ณผ์ ์ธ์คํด์ค๋ ๋ฐ๋์ด์๋ค.
expect(reducedState).not.toBe(defaultState)
์ฃผ์ํ ์ ์ Object๋ Map, Array๋ฅผ ๋ชจ๋ Trackable๋ก ๊ด๋ฆฌํด์ผํ๋ค. (์์ ์ ์์ง๋ง ๋ฐฐ์ด์ ์ํด
TrackableArray
์ญ์ ์ค๋น๋์ด ์๋ค.)
๊ทธ๋ฆฌ๊ณ Immutable.js๋ณด๋ค ์กฐ๊ธ ์ฑ๋ฅ์ ๋ชจ์๋ ๋ถ๋ถ์ด ์๋๋ฐ, Immutable.js๋ HAMT๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ธ์คํด์ค๋ฅผ ๋์ฑ ํจ์จ์ ์ผ๋ก ๋ง๋ค ์ ์๋ค. ํ์ง๋ง Trackable์ ์์ง ๋จ์ํ 1์ฐจ์์ ์ผ๋ก ํค์ ๊ฐ์ ์ดํฐ๋ ์ด์ ์ผ๋ก ์ ์ธ์คํด์ค๋ฅผ ๋ง๋ค๊ณ ์์ผ๋ฏ๋ก, ํน์ ์ํ์ ์ผ๋ก ์์ฒญ ๊ฑฐ๋ํ ๋งต์ ๋ง๋ค๋ ค๊ณ ํ ๋์๋ ๋ณดํ๋ฅ์ด ๋ ๊ฒ์ด๋ค. ์๋ง ์ด๋ฒ๋ฌ ์ค์ผ๋ก ์์ ํ ๊ฒ์ด๋ฏ๋ก ์กฐ๊ธ๋ง ๋ ๊ธฐ๋ค๋ ค์คฌ์ผ๋ฉด ํ๋ค. (ํน์ ์์ง์ ์ผ๋ก ๋ ๊น์ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด ์นดํ ๊ณ ๋ฆฌํ ์ํค๋ ๊ฒ๋ ํ๊ฐ์ง ๋ฐฉ๋ฒ์ด๋ค.)
์์ผ๋ก ํ ๊ฑด HAMT์ Set ๊ตฌ์กฐ๋ฅผ ๋ ์ถ๊ฐํ ๊ณํ์ด๋ค.
๋ฆฌ๋์ค์์ ๋๊ปด์ง๋ ์ฌ๋ฌ๊ฐ์ง ๋ฌธ์ ์ ๋ค์ ํ๋์ฉ ํํดํด ๋ณด์๋ค. ๋๋ถ์ ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ข ๋ ์ ๋ค๋ฃฐ ์ ์๊ฒ ๋๋ ๋ฑ ๋ฐฐ์ด๊ฒ ๋ง์ ๊ฑฐ ๊ฐ๋ค.
๋จ, ์์ง๊น์ง ๋ฆฌ๋์ค์์ ์์ฝ๋ค๊ณ ๋๊ปด์ง๋ ์ ์ ์ธ์ ๋ ์ฝ๋๊ฐ ๋๋ฌด ์ฅํฉํด์ง๋ค๋ ๊ฒ์ด๋ค.
์ด์ฉ๋ฉด CLI๊ฐ์๊ฑธ ๋ง๋ค์ด์ ํด๊ฒฐํ ์ ์์ง ์์๊น ๋ผ๋ ์์๋ ํ๋๋ฐ ์ผ๋จ์ ์ข ๋ ์ ์ฆ์ผ์ด์ค๋ฅผ ๋๋ ค์ ํํธ๋ฅผ ์ฐพ์ผ๋ฌ ๋ค๋ ์ผ๊ฒ ๋ค.