Redux-Toolkit 대표 API 정리
1. configureStore()
리덕스 코어 라이브러리인 createStore()를 추상화한 함수.
store를 구성하는 함수이다.
기본 리덕스 미들웨어인 redux-thunk를 내장하고, redux Devtools를 활성화해준다.
//기본 사용 예제
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from '../todo/tdooSlice';
export const store = configureStore({
reducer: {
todos: todoReducer,
},
});
configureStore 전체적인 구성
import logger from 'redux-logger'
import { reduxBatch } from '@manaflair/redux-batch'
import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'
const rootReducer = {
todos: todosReducer,
visibility: visibilityReducer,
}
const preloadedState = {
todos: [
{
text: 'Eat food',
completed: true,
},
{
text: 'Exercise',
completed: false,
},
],
visibilityFilter: 'SHOW_COMPLETED',
}
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
enhancers: [reduxBatch],
})
configureStore 함수는 reducer, middleware, devtools, preloadedState, enhancer 정보를 전달한다.
- reducer : 리듀서에는 단일 함수를 전달해 스토어의 루트 리듀서(root reducer)로 바로 사용할 수 있다. 슬라이스 리듀서로 구성된 객체를 전달해 루트 리듀서를 생성할 수도 있다. 이런 경우 내부적으로 combineReducers 함수를 사용해 자동으로 병합하여 루트 리듀서를 생성해줌
- middleware : 기본적으로는 리덕스 미들웨어를 담는 배열, 사용할 미들웨어를 명시적으로 배열에 담아서도 작성 가능.
기본은 getDefaultMiddleware를 호출함
- devTools : 불리언 값으로 개발자 도구를 끄거나 켬
- preloadedState : 스토어의 초기값 설정
- enhancers : 기본은 배열, 콜백으로 정의하기도 함. 아래와 같이 작성하면, 개발자가 원하는 store enhancer를 미들웨어가 적용되는 순서보다 앞서서 추가 가능
const store = configureStore({
...
enhancers: (defaultEnhancers) => [reduxBatch, ...defaultEnhancers],
})
// [reduxBatch, applyMiddleware, devToolsExtension]
2. createReducer()
상태에 변화를 일으키는 리듀서 함수를 생성하는 유틸 함수.
내부적으로 immer 라이브러리를 사용한다.
state.todo[3].completed = true 의 형태로 작성하여도 불변(immutable) 업데이트가 이루어지도록 로직을 간단히 할 수 있다.
아래는 사용 예시이다.
// 기존 스위치 문으로 이루어진 카운터 리듀서 함수입니다.
// 많은 보일러플레이트 코드와 에러를 발생시키기 쉬운 구조를 보여주고 있습니다.
function todosReducer(state = [], action) {
switch (action.type) {
case 'UPDATE_VALUE': {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
};
}
default: {
return state;
}
}
}
// 하지만 createReducer 함수를 사용하면 아래처럼 간단히 작성할 수 있습니다.
const todosReducer = createReducer(state = [], (builder) => {
builder.addCase('UPDATE_VALUE', (state, action) => {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
})
})
RTK에서 case reducer 가 액션을 처리하는 방식은 두 가지가 있다.
builder callback 표기법과 map object 표기법이다.
타입 스크립트 환경에서는 builder callback 표기법이 더 선호된다.
Builder Callback 표기법
builder callback 표기법에서는 createReducer의 콜백 인자 builder 객체 안에는 addCase, addMatcher, addDefaultCase라는 메서드가 존재한다.
// 각 라인마다 빌더 메서드를 나누어 호출합니다.
const counterReducer = createReducer(initialState, (builder) => {
builder.addCase(increment, (state) => {})
builder.addCase(decrement, (state) => {})
})
// 또는 메서드 호출을 연결하여 연속적으로 작성합니다.
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state) => {})
.addCase(decrement, (state) => {})
})
주요 메서드는 다음과 같다.
+ createReducer(initalState, bullderCallback)
- builder.addCase : 액션 타입과 맵핑되는 케이스 리듀서를 추가하여 액션을 처리한다. addMatcher 또는 addDefaultCase 메서드 보다 먼저 작성되어야 한다.
- builder.addMatcher(matcher, reducer) : 새로 들어오는 모든 액션에 대해 주어진 패턴과 일치하는지 확인하고 실행
- builder.addDefaultCase : 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았을 경우 defaultCase 가 실행
예시는 아래와 같다.
const increment = createAction('increment')
const decrement = createAction('decrement')
function isActionWithNumberPayload(
action: AnyAction
): action is PayloadAction {
return typeof action.payload === 'number'
}
const initialState = {
counter: 0,
sumOfNumberPayloads: 0,
unhandledActions: 0,
};
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.counter += action.payload
})
.addCase(decrement, (state, action) => {
state.counter -= action.payload
})
.addMatcher(isActionWithNumberPayload, (state, action) => {})
.addDefaultCase((state, action) => {})
})
Map Object 표기법
액션 타입 문자열을 '키'로 사용하는 객체를 받아 케이스 리듀서에 맵핑함
builder callback 표기법보다는 짧다는 장점이 있지만 Javascript 프로젝트에 유효한 방법.
TS는 builder callback 권장
const counterReducer = createReducer(0, {
increment: (state, action) => state + action.payload,
decrement: (state, action) => state - action.payload
})
// 위 예제처럼 작성하거나
// 또는 'createAction'에서 생성된 액션 생성자(action creator)를
// 연산된 프로퍼티(computed property) 문법을 사용해서 바로 '키'로 사용할 수 있습니다.
const increment = createAction('increment')
const decrement = createAction('decrement')
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload
})
+ createReducer(initialState, actionsMap, actionMatchers, defaultCaseReducer)
- initalState : 리듀서가 최초로 호출될 때 사용될 값
- actionsMap : 액션 타입이 케이스 리듀서에 맵핑되어 있는 객체
- actionMatchers : {matcher, reducer } 형태로 정의된 매처를 배열로 담음. 매칭 된 리듀서는 순서대로 독립적으로 실행
- defaultCaseReducer : 그 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았다면, 기본 케이스 리듀서가 실행
// matcher
const isStringPayloadAction = (action) => typeof action.payload === 'string'
const lengthOfAllStringsReducer = createReducer(
// initialState
{ strLen: 0, nonStringActions: 0 },
// actionsMap
{
/* [...]: (state, action) => {} */
},
// actionMatchers
[
{
matcher: isStringPayloadAction,
reducer(state, action) {
state.strLen += action.payload.length
},
},
],
// defaultCaseReducer
(state) => {
state.nonStringActions++
}
3. createAction()
기존 리덕스 형태에서는 액션 타입 상수와 액션 생성자 함수를 분리하여 선언하였다.
RTK에서는 이 두 과정을 createAction함수로 하나로 결합하여 사용할 수 있다.
기존 리덕스
// BEFOR
const INCREMENT = 'counter/increment'
function increment(amount: number) {
return {
type: INCREMENT,
payload: amount,
}
}
const action = increment(3)
// { type: 'counter/increment', payload: 3 }
RTK
// AFTER
import { createAction } from '@reduxjs/toolkit'
const increment = createAction('counter/increment')
const action = increment(3)
// { type: 'counter/increment', payload: 3 }
createAction 함수는 toString() 메서드를 오버 라이딩하여 액션 타입 문자열로도 표현이 가능하다.
map object 표현에서 액션 생성자를 키값으로 사용이 가능하다.
const increment = createAction('counter/increment')
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload
})
console.log(increment.toString())
// 'counter/increment'
console.log(`The action type is: ${increment}`)
// 'The action type is: counter/increment'
4. createSlice()
이 createSlice()를 사용하면 2번과 3번 항목인 createAction, createReducer를 따로 작성할 필요가 없어진다.
redux 공식문서에서는 slice파일은 feature 폴더 안에서 상태 도메인 별로 나누어 정리하라고 한다.
// features/todos/todosSlice.js
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'
interface Item {
id: string
text: string
}
// 투두 슬라이스
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
// 액션 타입은 슬라이스 이름을 접두어로 사용해서 자동 생성됩니다. -> 'todos/addTodo'
// 이에 상응하는 액션 타입을 가진 액션이 디스패치 되면 리듀서가 실행됩니다.
addTodo: {
reducer: (state, action: PayloadAction) => {
state.push(action.payload)
},
// 리듀서가 실행되기 이전에 액션의 내용을 편집할 수 있습니다.
prepare: (text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
},
},
})
const { actions, reducer } = todosSlice
export const { addTodo } = actions
export default reducer
extraReducers
extraReducers는 createSlice가 생성한 액션 타입 외 다른 액션 타입에 응답할 수 있도록 한다.
외부의 액션을 참조할 때 사용할 수 있다.
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
extraReducers: (builder) => {
// 'users/fetchUserById' 액션 타입과 상응하는 리듀서가 정의되어 있지 않지만
// 아래처럼 슬라이스 외부에서 액션 타입을 참조하여 상태를 변화시킬 수 있습니다.
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
5. createAsyncThunk()
createAction의 비동기 버전을 위해 만들어졌다.
액션 타입 문자열과 프로미스를 반환하는 콜백 함수를 인자로 받아 주어진 액션 타입을 접두어로 사용하는 프로미스 생명주기 기반의 액션 타입을 생성한다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
// extraReducers에 케이스 리듀서를 추가하면
// 프로미스의 진행 상태에 따라서 리듀서를 실행할 수 있습니다.
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
.addCase(fetchUserById.rejected, (state) => {})
},
})
// 위에서 fetchUserById, 즉 thunk를 작성해두고
// 앱에서 필요한 시점에 디스패치 하여 사용합니다.
// ...
dispatch(fetchUserById(123))
6. createSelector()
createSelector는 리덕스 스토어에서 데이터를 추출하도록 도와준다. Reselect 라이브러리에서 제공하는 함수를 그대로 가져온 것인데 useSelector 함수의 결점을 보완하기 위한 좋은 설루션이기 때문이다.
// useSelector는 스토어에서 값을 조회합니다.
const users = useSelector((state) => state.users)
Reselect 라이브러리는 createSelector 함수가 메모이제이션 기반으로(이전의 값을 메모리에 저장하고 변경된 값이 있을 때만 변경되도록) 동작한다.
이는 아래와 같은 문제점을 개선할 수 있다.
예제를 보면
const users = useSelector(
(state) => state.users.filter(user => user.subscribed)
);
이 예제의 useSelector는 스토어를 자동으로 구독하기 때문에 컴포넌트를 다시 렌더링 하는 경우 매번 새로운 인스턴스를 생성한다.
filter 함수는 useSelector가 실행될 때마다. 매번 새로운 배열을 반환하고, 이전 주소와 새로운 주소가 차이를 발생시켜 리 렌더링을 발생시킨다.
그래서 createSelector를 사용하면 애플리케이션 최적화를 할 수 있다.
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent
// subtotal 값을 메모이제이션 합니다.
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((subtotal, item) => subtotal + item.value, 0)
)
// 메모이제이션된 subtotal 값과 taxPercentSelector를 합성하여
// 새로운 값을 메모이제이션 합니다.
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => ({ total: subtotal + tax })
)
const exampleState = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 },
]
}
}
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState)) // 0.172
console.log(totalSelector(exampleState)) // { total: 2.322 }
나의 정리
아직 RTK는 간단한 사이드 프로젝트 밖에 만들지 않아서, 또 Redux를 제대로 사용해보지 않아.
이런 예제나 이론들이 한눈에 들어오지 않는다.
하지만 이런 식으로 redux의 개념을 정리하고, 내가 작성한 글로 찾아보고 적용해 보는 습관을 들이고,
이러한 세부적인 기능들도 구현해본다면 좀 더 RTK랑 친해질 수 있지 않을까..!?
참고한 글
- http://blog.hwahae.co.kr/all/tech/tech-tech/6946/
- https://velog.io/@yoonvelog/Redux-toolkit
'개발 > React' 카테고리의 다른 글
[React] 디바운스(Debounce), 쓰로틀(Throttle) (0) | 2023.03.19 |
---|---|
Next + RTK + Typescript 개발 환경 설정하기 (0) | 2022.11.18 |
[RTK] RTK-Query? (0) | 2022.11.05 |
[React] Redux-Toolkit Quick Start 공식 문서 따라하기 (1) | 2022.10.04 |
리액트 가상 DOM(React Virtual Dom) (0) | 2022.09.28 |
댓글