Table of Contents
Redux Toolkit(RTK) 개념 및 예제
Redux는 상태 관리 라이브러리로 전역 상태를 효과적으로 관리하는데 도움을 준다.
하지만 Redux를 제대로 활용하기 위해서는 많은 라이브러리를 추가적으로 설치해야 하는 불편함이 존재한다. 또한 많은 보일러플레이트 코드를 요구한다.
Redux 개발팀은 이 문제를 해결하기 위해 Redux Toolkit(RTK) 를 만들었고, 특히 TypeScript를 통해 Redux를 개발할 경우 공식적으로 Redux Toolkit을 사용할 것을 추천하고 있다.
Redux Toolkit(RTK)이란?
Redux 개발 시 표준 로직이 되기 위해 고안된 패키지로, Redux 앱을 구축하는 데 필수적인 패키지와 기능이 포함되어 있다. action 생성, reducer 생성, store 설정, thunk 등을 생성할 때 코드를 쉽게 작성할 수 있는 함수를 제공한다.
포함되는 기본 함수들
- configureStore() : createStore를 랩핑하여 store 설정을 단순화한다.
- createReducer() : reducer를 생성하는 함수로, immer 라이브러리를 자동으로 사용하여 더 간단한 상태 변경이 가능하다.
- createAction() : action 타입과 action 생성자를 한번에 생성하도록 도와준다.
- createSlice() : reducer와 함께 그에 맞는 action 생성자와 action 타입을 자동으로 생성해준다.
- createAsyncThunk() : 비동기 action을 생성할 때 사용한다.
Store 설정
기존 Redux에서 Store를 설정할 때는 다음과 같이 사용한다.
1import { combineReducers } from 'redux'
2import testReducer from './reducers/testReducer'
3
4// reducer를 결합하는 rootReducer 생성
5const rootReducer = combineReducers({
6 test: testReducer
7});
8
9export default rootReducer;
1import { createStore, applyMiddleware } from 'redux'
2import { composeWithDevTools } from 'redux-devtools-extension'
3import thunk from 'redux-thunk';
4import rootReducer from './reducers/rootReducer'
5
6const logger = createLogger({
7 collapsed: true,
8});
9const middlewares = [thunk, logger];
10const composedEnhancer = composeWithDevTools(applyMiddleware(...middlewares));
11
12// rootReducer와 enhancer를 결합하여 store 생성
13const store = createStore(rootReducer, composedEnhancer);
14export default store;
- combineReducers를 통해 reducers들을 하나로 묶는 rootReducer를 만든다.
- middleware와 devtools를 하나로 합친 enhancer를 만든다.
- createStore를 이용하여 store를 생성한다.
store를 설정할 때 이와 같이 필요한 패키지들이 많은 것을 알 수 있다. 하지만 Redux Toolkit을 사용하면 간편하게 strore를 설정할 수 있다.
configureStore 사용
Redux Toolkit의 configureStore는 store 설정을 단순화하도록 도와준다.
1import { configureStore } from '@reduxjs/toolkit'
2import { createLogger } from 'redux-logger';
3import testReducer from './reducers/testReducer'
4
5const loggers = createLogger({
6 collapsed: true,
7});
8
9export const store = configureStore({
10 reducer: {
11 test: testReducer,
12 },
13 middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggers),
14 devTools: true,
15});
configureStore를 사용하면 다음과 같은 이점이 있다.
- reducer들을 rootReducer로 자동으로 변환해준다.
- thunk 미들웨어와 같은 기본적인 미들웨어를 자동적으로 추가한다.
- redux DevTools를 자동으로 연결해준다.
action, reducer 생성
기존 Redux에서 action과 reducer를 생성할 때는 디렉터리를 따로 분리하여 action과 reducer 파일을 별도로 생성하였다.
1export enum TestActionTypes {
2 TEST_SUCCESS = 'TEST_SUCCESS',
3 TEST_FAILED = 'TEST_FAILED',
4}
5
6// 액션 선언
7export const testActions = {
8 testSuccess: () => ({ type: TestActionTypes.TEST_SUCCESS}),
9 testFailed: (error: any) => ({ type: TestActionTypes.TEST_FAILED, payload: error}),
10};
11
12// 액션 타입 지정
13export type TestAction = ReturnType<typeof testActions.testSuccess> | ReturnType<typeof testActions.testFailed>;
1type TestState = {
2 initialized: boolean;
3 test: string | null;
4};
5
6// 초기 상태 지정
7const initialState: TestState = {
8 initialized: false,
9 test: null,
10};
11
12// 리듀서 생성
13const testReducer = (state: TestState = initialState, action: TestAction) => {
14 switch (action.type) {
15 case TestActionTypes.TEST_SUCCESS:
16 return { initialized: true };
17 case TestActionTypes.TEST_FAILED:
18 return { initialized: false, test: action.payload };
19 default:
20 return state;
21 }
22}
23
24export default testReducer;
어플리케이션의 크기가 커진다면 action, reducer 코드 자체의 볼륨도 커져 관리가 힘들어진다. 이를 해결하기 위해 Redux Toolkit이 대안으로 내놓은 함수가 바로 createSlice
이다.
createSlice 사용
createSlice는 세가지 옵션 필드가 존재한다.
- name : action이 생성될 때 접두사로 사용되는 문자열
- initialState : reducer의 초기 상태값
- reducers : 특정 액션이 수행될 경우에 대한 reducer 설정
createSlice를 사용한 예시이다.
1// 초기 상태값에 대한 인터페이스 선언
2export interface TestState {
3 initialized: boolean;
4 test: string | null;
5}
6
7// 초기 상태값 설정
8const defaultState: TestState = {
9 initialized: false,
10 test: null,
11};
12
13// crateSlice를 활용하여 action, reducer 생성
14const testSlice = createSlice({
15 name: 'TEST',
16 initialState: defaultState,
17 reducers: {
18 [TestActionTypes.TEST_SUCCESS]: (state: TestState) => {
19 state.initialized = true;
20 },
21 [TestActionTypes.TEST_FAILED]: (state: TestState, action: PayloadAction<string>) => {
22 state.value = action.payload;
23 },
24 },
25});
26
27export default testSlice.reducer;
28export const testSuccess = testSlice.actions[TestActionTypes.TEST_SUCCESS];
29export const testFailed = testSlice.actions[TestActionTypes.TEST_FAILED];
reducer를 작성할 때 사용하던 switch 문 대신 객체 내부의 함수로 reducer를 작성할 수 있다. 또한 작성한 리듀서에 따라 액션이 자동으로 생성되어 별도로 액션을 생성할 필요가 없다.
Thunks 작성하기
기존 비동기 API를 처리하기 위해서는 Redux-thunk 미들웨어를 사용하여 다음과 같은 코드로 구현했었다.
1export enum TestActionTypes {
2 TEST_SUCCESS = 'TEST_SUCCESS',
3}
4
5// 액션 선언
6export const testActions = {
7 testSuccess: () => ({ type: TestActionTypes.TEST_SUCCESS}),
8};
9
10// 액션 타입 지정
11export type TestAction = ReturnType<typeof testActions.testSuccess>;
12
13export const test = (): ThunkAction< Promise<void>, RootState, {}, TestAction > => {
14 // 3초 뒤 실행
15 return async (dispatch: ThunkDispatch<{}, {}, TestAction>): Promise<void> => {
16 return new Promise<void>((resolve) => {
17 setTimeout(() => {
18 dispatch(testActions.testSuccess());
19 }, 3000);
20 });
21 };
22};
ThunkAction의 타입을 계속 신경써줘야 하기 때문에 상당히 번거롭다. (ThunkAction의 Custom 함수를 만들어 쓰는 방법도 있다.) 이럴 경우, createAsyncThunk
를 사용하면 비동기 thunk 액션을 손쉽게 구현할 수 있다.
createAsyncThunk 사용
createAsyncThunk를 두 가지 옵션 필드가 존재한다.
- action이 생성될 때 접두사로 사용되는 문자열
- Promise를 반환하는 콜백 함수
createAsyncThunk 를 사용한 예시이다.
1const testAsync = createAsyncThunk(
2 TestActionTypes.TEST_SUCCESS, // TEST_SUCCESS
3 async () => {
4 await new Promise((resolve) => setTimeout(resolve, 1000));
5 return 'data';
6 },
7);
createAsyncThunk는 3개의 action creator와 action type를 생성하게 된다. 생성되는 action creator와 action type는 다음과 같다.
- testAsync.pending : TEST_SUCCESS/pending
- testAsync.fulfilled : TEST_SUCCESS/fulfilled
- testAsync.rejected : TEST_SUCCESS/rejected
단, 이렇게 만든 action은 createSlice 외부에서 구현한 것이기 때문에 createSlice의 extraReducers 옵션을 활용해야 한다.
1const testSlice = createSlice({
2 name: 'TEST',
3 initialState: defaultState,
4 reducers: {},
5 extraReducers: (builder) => {
6 builder
7 .addCase(testAsync.pending, (state) => {
8 state.statue = "loading";
9 })
10 .addCase(testAsync.rejected, (state, action: PayloadAction<string>) => {
11 state.value = action.payload;
12 })
13 .addCase(testAsync.fulfilled, (state, action: PayloadAction<string>) => {
14 state.value = action.payload;
15 });
16 },
17});
이제 createAsyncThunk로 생성한 액션으로부터 상태 값을 받아서 처리할 수 있다.
Typed hooks 정의
Typed hooks를 정의해야 하는 이유는 다음과 같다.
- useSelector를 사용할 경우 state 타입을 계속 지정해줘야 하는 번거로움이 존재한다.
- useDispatch를 사용할 경우 기본 Dispatch는 thunk 타입을 인식하지 못하기 때문에 thunk를 dispatch하기 위해서는 thunk 미들웨어가 추가된 store에서 dispatch를 직접 받아와야 한다.
configureStore로 만들어진 store를 이용해 RootState와 AppDispatch를 선언한다.
1export const store = configureStore({
2 reducer: rootReducer,
3 middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggers),
4});
5
6export type RootState = ReturnType<typeof store.getState>;
7export type AppDispatch = typeof store.dispatch;
store에서 정의된 RootState와 AppDispatch를 사용하여 Typed hooks를 정의한다.
1import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
2import type { RootState, AppDispatch } from './store'
3
4export const useAppDispatch: () => AppDispatch = useDispatch
5export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
이제 useSelector와 useDispatch 대신 useAppDispatch와 useAppSelector를 사용하면 된다.