이번 글에서는 이전에 redux와 redux-thunk로 구현한 예제를 RTK로 변환하는 작업을 진행합니다.
Redux Toolkit (RTK)
기존 redux의 불편한 점을 해결하고자 자주 사용되는 라이브러리를 미리 포함하여 제공하는 것이 Redux Toolkit입니다.
이를 통해 코드량을 큰 폭으로 줄일 수 있고 라이브러리 구성을 위한 걱정을 덜게 됩니다.
현재 redux는 공식적으로 RTK를 사용하기를 권장하고 있습니다.
대표적으로 포함되는 라이브러리: redux-actions, reselect, immer, redux-thunk, redux-devtools-extension, etc.
설치
npm install @reduxjs/toolkit
사전 준비
현재 프로젝트의 모든 action이 비동기로 처리되고 있으므로 동기적인 처리 예시를 하나 정도 만들기 위해 로그아웃을 동기적으로 바꾸도록 하겠습니다.
/src/redux/module/user.js
로그아웃 action 수정
action에 따른 reducer 수정
로그아웃 action creator 수정
Redux -> RTK
src/redux/module/user.js
RTK에서는 redux에서의 actions, action creator, reducer를 createSlice로 한 번에 처리하게 되며 코드량이 대폭 감소됩니다.
/* Redux */
// Action
const LOG_OUT = 'my-todo/user/LOG_OUT';
// Reducer
const initialState = { data: null, logInState: AsyncState.IDLE, error: '' };
export default function reducer(state = initialState, action) {
switch (action.type) {
case LOG_OUT: {
return {
...state,
data: null,
logInState: AsyncState.IDLE,
error: '',
};
}
default:
return state;
}
}
// Action Creator
export function logOut() {
return { type: LOG_OUT };
}
/* RTK */
// Slice
const initialState = { data: null, logInState: AsyncState.IDLE, error: '' };
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logOut(state, action) {
state.data = null;
state.logInState = AsyncState.IDLE;
state.error = '';
},
},
// ...
});
export default userSlice;
비동기 처리를 위해 thunk를 사용하는 경우 createAsyncThunk method를 사용하며 slice의 extraReducers로 추가합니다.
/* Redux */
// Actions
const LOG_IN_PENDING = 'my-todo/user/LOG_IN_PENDING';
const LOG_IN_SUCCESS = 'my-todo/user/LOG_IN_SUCCESS';
const LOG_IN_REJECTED = 'my-todo/user/LOG_IN_FAILED';
// Reducer
export default function reducer(state = initialState, action) {
switch (action.type) {
case LOG_IN_PENDING: {
return {
...state,
logInState: AsyncState.PENDING,
error: '',
};
}
case LOG_IN_SUCCESS: {
return {
...state,
data: action.payload,
logInState: AsyncState.IDLE,
error: '',
};
}
case LOG_IN_REJECTED: {
return {
...state,
logInState: AsyncState.REJECTED,
error: action.payload,
};
}
// ...
default:
return state;
}
}
// Action Creators
export function logIn(id, password) {
return (dispatch, getState) => {
dispatch({ type: LOG_IN_PENDING });
const response = logInRequest(id, password);
response
.then((res) => dispatch({ type: LOG_IN_SUCCESS, payload: res }))
.catch((error) => dispatch({ type: LOG_IN_REJECTED, payload: error }));
};
}
/* RTK */
// Thunk
export const logIn = createAsyncThunk(
'my-todo/user/logIn',
async (data, thunkAPI) => {
const response = await logInRequest(data.id, data.password);
return response;
}
);
// Slice
const userSlice = createSlice({
// ...
extraReducers: {
[logIn.pending](state, action) {
state.logInState = AsyncState.PENDING;
state.error = '';
},
[logIn.fulfilled](state, action) {
state.data = action.payload;
state.logInState = AsyncState.IDLE;
state.error = '';
},
[logIn.rejected](state, action) {
state.logInState = AsyncState.REJECTED;
state.error = String(action.payload);
},
},
});
export default userSlice;
createSlice에서의 reducers와 extraReducers의 차이
reducers: 등록한 reducer의 action creator를 자동으로 생성.
extraReducers: action creator를 생성하지 않음. 이미 외부에서 생성된 action creator를 가져와 사용할 때 사용. (다른 slice의 action creator를 사용하거나 createAsyncThunk를 사용하는 경우)
/src/redux/module/todoList.js
todoList는 동기 처리가 없습니다.
이런 경우에는 createSlice를 통해 생성할 action creator가 없으므로 이를 사용하는 의미가 없습니다.
이미 생성된 action creator를 이용해 reducer만 생성해주면 되므로 createReducer method를 사용합니다.
// ...
// Thunk
export const addTodo = createAsyncThunk('todoList/addTodo', (data, thunkAPI) =>
addTodoRequest(data.todo)
);
export const getTodos = createAsyncThunk(
'todoList/getTodos',
(data, thunkAPI) => getTodosRequest()
);
export const updateTodo = createAsyncThunk(
'todoList/updateTodo',
(data, thunkAPI) => updateTodoRequest(data.id, data.todo)
);
export const deleteTodo = createAsyncThunk(
'todoList/updateTodo',
(data, thunkAPI) => deleteTodoRequest(data.id)
);
// Reducer
const initialState = { todos: {}, todosState: AsyncState.IDLE, error: '' };
const isTodoPending = isPending(addTodo, getTodos, updateTodo, deleteTodo);
const isTodoFulfilled = isFulfilled(addTodo, getTodos, updateTodo, deleteTodo);
const isTodoRejected = isRejected(addTodo, getTodos, updateTodo, deleteTodo);
const todoListReducer = createReducer(initialState, (builder) => {
builder
.addMatcher(isTodoPending, (state, action) => {
state.todosState = AsyncState.PENDING;
state.error = '';
})
.addMatcher(isTodoFulfilled, (state, action) => {
state.todos = action.payload;
state.todosState = AsyncState.IDLE;
})
.addMatcher(isTodoRejected, (state, action) => {
state.todosState = AsyncState.REJECTED;
state.error = String(action.payload);
});
});
export default todoListReducer;
/src/redux/module/index.js
slice에서 생성된 reducer와 createReducer로 생성된 reducer를 rootReducer로 합쳐줍니다.
import { combineReducers } from 'redux';
import userSlice from './user';
import todoListReducer from './todoList';
const rootReducer = combineReducers({
user: userSlice.reducer,
todoList: todoListReducer,
});
export default rootReducer;
/src/redux/index.js
configureStore method를 사용해 store를 생성합니다.
이 글에서는 나오지 않지만 middleware나 devtools 설정도 여기서 진행합니다.
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './module';
const store = configureStore({ reducer: rootReducer });
export default store;
/src/components/LogInComponent.jsx
createAsyncThunk로 생성한 action creator는 기존 사용하던대로 사용하면 됩니다. (아래 코드의 ASYNC)
createSlice로 생성한 action creator는 slice.actions.action_creator() 형태로 사용하면 됩니다. (아래 코드의 SYNC)
/* Redux */
// ...
// event handlers
const onLogIn = useCallback((id, pw) => {
dispatch(logIn(id, pw)); // ASYNC
idRef.current = '';
pwRef.current = '';
}, []);
const onLogOut = useCallback(() => {
dispatch(logOut()); // SYNC
}, []);
// ...
/* RTK */
// ...
// event handlers
const onLogIn = useCallback((id, password) => {
dispatch(logIn({ id, password })); // ASYNC
idRef.current = '';
pwRef.current = '';
}, []);
const onLogOut = useCallback(() => {
dispatch(userSlice.actions.logOut()); // SYNC
}, []);
// ...
결과
변경 사항을 컴포넌트에 모두 반영하면 이전 글의 결과와 같은 결과를 보여주게 됩니다.
소스 코드
GitHub repo: https://github.com/dev-wann/redux-practice, my-todo 폴더 내부 내용
이번 글에 해당하는 커밋:
- 사전 준비: https://github.com/dev-wann/redux-practice/commit/b470ab449cb2c778d0f6200476cde69fdcdec418
- RTK 적용: https://github.com/dev-wann/redux-practice/commit/5acf4ea542243129b3068c2bd9ce4057c0318a79
'Web development > 상태관리' 카테고리의 다른 글
Redux - 로그인 + TodoList 예제 구현 (3) redux-thunk (0) | 2023.11.15 |
---|---|
Redux - 로그인 + TodoList 예제 구현 (2) redux-thunk (1) | 2023.11.14 |
Redux - 로그인 + TodoList 예제 구현 (1) redux (0) | 2023.11.12 |
Redux - 기본 개념 (0) | 2023.11.02 |