React Redux Toolkit 사용하기
React에서 Redux Toolkit을 사용하는 방법에 대해 알아보겠습니다.
1. Redux Toolkit
Redux Toolkit은 Redux를 더 쉽게 사용하기 위해 만들어졌습니다. 리덕스는 Flux 아키텍처를 기반으로 잘 설계된 라이브러리이지만 다음과 같은 문제점을 보였습니다.
- 리덕스의 복잡한 스토어 설정
- 리덕스를 유용하게 사용하기 위해서 추가되어야 하는 많은 패키지들
- 리덕스 사용을 위해 요구되는 다량의 상용구(boilerplate) 코드들
이러한 문제점을 개선하기 위해 Redux Toolkit이 만들어지게 되었습니다. 리덕스 툴킷에서 제공하는 주요 함수들을 사용하면 기존 리덕스의 복잡도를 낮추고 사용성을 높여서 코드를 작성할 수 있습니다.
1.1. Redux Toolkit 설치
다음 명령어를 실행하여 Redux Toolkit을 설치해줍니다.
$ yarn add @reduxjs/toolkit
2. Redux Toolkit 사용하기
Redux Toolkit을 사용해보면서 함께 제공되는 주요 함수들에 대해 알아보겠습니다.
src 하위에 다음과 같이 파일 구조를 만들어줍니다.
src
├── store
│ ├── config.ts
│ └── slices
│ ├── counterSlice.ts
│ ├── todoSlice.ts
│ └── userSlice.ts
├── services
│ └── userService.ts
├── screens
│ ├── Counter.tsx
│ └── Todo.tsx
├── ...
App.tsx
index.tsx
2.1. configureStore
configureStore 함수는 리덕스 라이브러리의의 createStore 함수를 추상화한 것입니다. 기존의 번거로웠던 리덕스 설정을 간편하게 할 수 있도록 해주고 설정시 디폴트로 redux-thunk와 DevTools를 제공해줍니다.
다음과 같이 index.tsx, store/config.ts 파일을 작성해줍니다.
/* index.tsx */
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './store/config';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
/* store/config.ts */
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { createLogger } from 'redux-logger';
import counterSlice from './slices/counterSlice';
import todoSlice from './slices/todoSlice';
import userSlice from './slices/userSlice';
const logger = createLogger();
const rootReducer = combineReducers({
counter: counterSlice.reducer,
todo: todoSlice.reducer,
user: userSlice.reducer
});
const initialState = {};
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState: initialState,
enhancers: (defaultEnhancers) => [...defaultEnhancers]
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export default store;
configureStore 함수에 전달된 파라미터 객체에 대해 정리하면 다음과 같습니다.
- reducer
리덕스 스토어의 rootReducer를 설정.
combineReducers 함수를 사용하여 slice reducer들을 병합한 rootReducer를 설정 가능.
단일 함수로 설정한 경우엔 스토어의 rootReducer로 사용됨.
slice reducer로 설정한 경우엔 자동으로 combineReducers에 전달하여 rootReducer를 생성. - middleware
redux-logger와 같은 리덕스 미들웨어를 설정.
미들웨어를 설정한 경우엔 자동으로 applyMiddleware에 전달.
미들웨어를 설정하지 않은 경우엔 getDefaultMiddleware를 호출. - devTools
Redux DevTools 사용 여부 설정. (기본값은 true) - preloadedState
리덕스 스토어의 초기값 설정. - enhancers
사용자 정의 미들웨어를 설정.
콜백 함수로 설정하면 미들웨어 적용 순서를 정의 가능.
configureStore 함수의 파라미터에 대한 자세한 내용은 아래 링크를 통해 확인할 수 있습니다.
또한 useAppSelector와 useAppDispatch는 기존의 useSelector와 useDispatch hooks를 추상화한 것 입니다. 이와 같이 사용하면 각각의 컴포넌트에서 useSelector나 useDispatch를 매번 설정하지 않고 애플리케이션 전역에서 사용이 가능합니다.
2.2. createSlice
createSlice 함수는 선언한 slice의 name에 따라서 액션 생성자, 액션 타입, 리듀서를 자동으로 생성해줍니다. 따라서 별도로 createAction이나 createReducer를 사용하지 않아도 됩니다.
다음과 같이 store/slices/counterSlice.ts, store/slices/todoSlice.ts 파일을 작성해줍니다.
/* store/slices/counterSlice.ts */
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CommonState {
value: number
}
const initialState: CommonState = {
value: 0
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
setCounter(state, action: PayloadAction<number>) {
state.value = action.payload;
}
}
});
export const { setCounter } = counterSlice.actions;
export default counterSlice;
/* store/slices/todoSlice.ts */
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface TodoItem {
id: number,
title: string,
checked: boolean
}
export interface CommonState {
todoList: TodoItem[]
}
const initialState: CommonState = {
todoList: []
};
export const todoSlice = createSlice({
name: 'todo',
initialState,
reducers: {
setTodo(state, action: PayloadAction<TodoItem[]>) {
state.todoList = action.payload;
}
}
});
export const { setTodo } = todoSlice.actions;
export default todoSlice;
createSlice 함수를 사용하여 slice를 작성하면 기존의 리덕스 라이브러리로 리듀서를 구성했을 때 비해서 코드가 많이 줄어든 것을 확인할 수 있습니다. slice를 생성할 때 액션 타입은 name이 앞에 붙은 형태(counter/setCounter, todo/setTodo)로 생성됩니다. 생성된 액션 타입을 가진 액션이 디스패치되면 리듀서가 실행되게 됩니다.
다음으로 screens/Counter.tsx, screens/Todo.tsx 파일을 작성해줍니다.
/* screens/Counter.tsx */
import React, { useState, useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '../store/config';
import { setCounter } from '../store/slices/counterSlice';
function Counter() {
////////////////////////////////////////
// Hooks
////////////////////////////////////////
const { value } = useAppSelector(state => state.counter);
const dispatch = useAppDispatch();
////////////////////////////////////////
// Functions
////////////////////////////////////////
const onIncrease = useCallback(() => {
dispatch(setCounter(value + 1));
}, [dispatch, value]);
const onDecrease = useCallback(() => {
dispatch(setCounter(value - 1));
}, [dispatch, value]);
////////////////////////////////////////
// View
////////////////////////////////////////
return (
<>
<div>
<h1>{value}</h1>
<div>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
</>
);
};
export default Counter;
/* screens/Todo.tsx */
import React, { useState, useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '../store/config';
import { setTodo } from '../store/slices/todoSlice';
interface TodoItem {
id: number,
title: string,
checked: boolean
}
function Todo() {
////////////////////////////////////////
// Hooks
////////////////////////////////////////
const { todoList } = useAppSelector(state => state.todo);
const [inputTitle, setInputTitle] = useState('');
const dispatch = useAppDispatch();
////////////////////////////////////////
// Functions
////////////////////////////////////////
const onChange = useCallback((event) => {
setInputTitle(event.target.value);
}, [inputTitle]);
const onSubmit = useCallback((event) => {
event.preventDefault();
const insertItem: TodoItem = {
id: (!todoList.length) ? 0 : Math.max(...todoList.map((item) => item.id)) + 1,
title: inputTitle,
checked: false
};
const setTodoList = [...todoList, insertItem];
dispatch(setTodo(setTodoList));
setInputTitle('');
}, [dispatch, inputTitle, todoList]);
const onToggle = useCallback((id) => {
const setTodoList = [...todoList];
const toggleIndex = setTodoList.findIndex(item => item.id === id);
const toggleItem = {...setTodoList[toggleIndex]};
toggleItem.checked = !toggleItem.checked;
setTodoList[toggleIndex] = {...toggleItem};
dispatch(setTodo(setTodoList));
}, [dispatch, todoList]);
const onRemove = useCallback((id) => {
const setTodoList = [...todoList];
const removeIndex = setTodoList.findIndex(item => item.id === id);
setTodoList.splice(removeIndex, 1);
dispatch(setTodo(setTodoList));
}, [dispatch, todoList]);
////////////////////////////////////////
// View
////////////////////////////////////////
return (
<div>
<form onSubmit={onSubmit}>
<input value={inputTitle} onChange={onChange} />
<button type="submit">Add</button>
</form>
<div>
{
todoList.map((item, index) => (
<div key={index}>
<input
type="checkbox"
checked={item.checked}
readOnly={true}
onClick={() => onToggle(item.id)}
/>
<span style={{ textDecoration: item.checked ? 'line-through' : 'none' }}>
{item.title}
</span>
<button onClick={() => onRemove(item.id)}>X</button>
</div>
))
}
</div>
</div>
);
};
export default Todo;
마지막으로 App.tsx 파일을 다음과 같이 작성해줍니다.
/* App.tsx */
import React from 'react';
import Counter from './screens/Counter';
import Todo from './screens/Todo';
function App() {
return (
<>
<Counter />
<hr />
<Todo />
</>
);
}
export default App;
여기까지 구성하고 애플리케이션을 실행해보면 다음과 같이 Redux Toolkit이 적용되어 상태 관리가 이루어지는 것을 확인할 수 있습니다.
2.3. createAsyncThunk
createAsyncThunk 함수는 비동기 액션을 생성할 때 사용합니다. 액션 타입을 지정하는 문자열과 프로미스를 반환하는 콜백 함수를 파라미터로 사용하여 생성합니다.
다음과 같이 services/userService.ts 파일을 작성해줍니다. getUser 함수를 createAsyncThunk를 이용하여 비동기 액션을 실행할 수 있도록 만들어줍니다.
/* services/userService.ts */
import {createAsyncThunk} from '@reduxjs/toolkit';
import {instance} from '../hooks/useAxiosLoader';
export const UserService = {
/**
* Get User
*/
getUser: createAsyncThunk(
'user/getUser',
async (userId: string, thunkApi) => {
const {data} = await instance.get(
`/users/${userId}`
);
return data;
}
)
};
다음으로 store/slices/userSlice.ts 파일을 작성해줍니다.
/* store/slices/userSlice.tsx */
import {createSlice} from '@reduxjs/toolkit';
import {UserService} from '../../services/userService';
const initialState = {};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(UserService.getUser.pending, (state, action) => {
console.log('pending');
})
.addCase(UserService.getUser.fulfilled, (state, action) => {
console.log('fulfilled');
})
.addCase(UserService.getUser.rejected, (state, action) => {
console.log('rejected');
})
}
});
export default userSlice;
userSlice에는 extraReducers를 사용했는데 이것은 createSlice에서 생성한 액션 타입 외에 다른 액션 타입을 정의할 때 사용합니다. 위의 예제에서는 비동기 액션의 처리를 위해 사용하였습니다. extreReducers에 정의된 리듀서들은 외부의 액션 타입에 대응하기 때문에 slice.actions를 이용하여 액션을 생성할 수 없다는 특징을 갖습니다.
createAsyncThunk 함수로 비동기 액션을 만들면 thunk 액션 생성자를 반환하는데 pending, fulfilled, rejected 상태에 대한 액션이 생성됩니다. 위의 예제에서 extraReducers에 정의한 리듀서를 기준으로 보면 다음과 같습니다.
- UserService.getUser.pending
user/getUser/pending 액션을 디스패치하는 thunk 액션 생성자 - UserService.getUser.fulfilled
user/getUser/fulfilled 액션을 디스패치하는 thunk 액션 생성자
프로미스가 이행된 상태 - UserService.getUser.rejected
user/getUser/rejected 액션을 디스패치하는 thunk 액션 생성자
프로미스가 거부된 상태
마지막으로 App.tsx를 다음과 같이 수정해줍니다. dispatch 함수를 호출하면서 createAsyncThunk로 정의한 비동기 액션 함수를 파라미터로 전달해줍니다.
/* App.tsx */
import React from 'react';
import {useAppDispatch} from './store/config';
import {UserService} from './services/userService';
import Counter from './screens/Counter';
import Todo from './screens/Todo';
function App() {
const dispatch = useAppDispatch();
/**
* Get User
*/
const getUser = async () => {
dispatch(UserService.getUser('1'));
};
return (
<>
<Counter />
<hr />
<Todo />
<p/>
<button onClick={getUser}>[API TEST(GET)]</button>
</>
);
}
export default App;
애플리케이션을 실행해보면 비동기 액션이 동작하는 것을 확인할 수 있습니다.
이상으로 React에서 Redux Toolkit을 사용하는 방법에 대해 알아봤습니다.
※ Reference
- redux-toolkit.js.org, Getting Started with Redux Toolkit, https://redux-toolkit.js.org/introduction/getting-started
- redux.js.org, Redux Essentials, Part 5: Async Logic and Data Fetching, https://redux.js.org/tutorials/essentials/part-5-async-logic/
- velog.io/@velopert, Redux 어떻게 써야 잘 썼다고 소문이 날까?, https://velog.io/@velopert/using-redux-in-2021
- velog.io/@gytlr01, redux toolkit todolist tutorial, https://velog.io/@gytlr01/redux-toolkit-todolist-tutorial-ww1bn9me
- velog.io/@raejoonee, Redux Toolkit의 createAsyncThunk로 비동기 처리하기, https://velog.io/@raejoonee/createAsyncThunk
- kyounghwan01.github.io, Redux Toolkit을 사용하여 간단하게 상태 관리하기, https://kyounghwan01.github.io/blog/React/redux/redux-toolkit/
- soyoung210.github.io, 기본 튜토리얼: Redux Toolkit 소개, https://soyoung210.github.io/redux-toolkit/tutorials/basic-tutorial/
- maruzzing.github.io, createSlice로 action과 reducer 생성하기, https://maruzzing.github.io/study/react/Redux-toolkit%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-1/
- blog.hwahae.co.kr, Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까?, http://blog.hwahae.co.kr/all/tech/tech-tech/6946/
- darrengwon.tistory.com, redux-toolkit (1) : redux와 RTK를 비교해보자, https://darrengwon.tistory.com/1406?category=889133