Redux 맛보기 공부를 위해 TS 환경에서 만든 todoList에 Redux를 적용하며 배우고자했다.
공식문서와 구글링하며 공부하는데 어떤 것엔 Action을 지정하고 있는데 또 createSlice할 때는 없어 처음엔 헷갈렸는데, 결론은
Redux -> RTK
createAction, createReducer -> createSlice
Redux를 쉽게 사용하기 위해 RTK에서 사용하는 createAction과 createReducer를 더욱 더 함축시킨 것이 createSlice이다.
공식 문서에서도 RTK의 사용을 권장하는 문구가 보이고, api를 사용하여 async 함수를 사용하기에 RTK의 createSlice를 사용했다.
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
import './App.css';
import Router from './pages/Router';
import { Provider as MyProvider } from 'react-redux';
import { store } from './store/store';
function App() {
return (
<>
<MyProvider store={store}>
<Router />
</MyProvider>
</>
);
}
export default App;
import axios from 'axios';
import { TodoItem } from '@/types/TodoTypes';
import { createAsyncThunk } from '@reduxjs/toolkit';
//반환하는 상태 타입 정의
export type MyState = {
todoListData: TodoItem[];
selectedTodo: TodoItem;
};
// 비동기 액션에서 반환하는 타입 정의
type AsyncThunkConfig = {
state: MyState;
rejectValue: string;
};
export const getTodoList = createAsyncThunk<TodoItem[], void, AsyncThunkConfig>(
'todoData/getTodoList',
async (_, thunkAPI) => {
try {
const res = await axios.get(``);
const sortedItems = res.data.items.sort((a: TodoItem, b: TodoItem) => {
return new Date(b.updatedAt!).getTime() - new Date(a.updatedAt!).getTime();
});
return sortedItems;
} catch (error) {
console.error('Error fetching todo list:', error);
return thunkAPI.rejectWithValue('Error fetching todo list');
}
},
);
...
상태관리를 하지않는 기존의 api 통신코드를 createAsyncThunk
함수를 사용하여 수정했다. typescript를 사용하므로 전역으로 사용할 데이터와 비동기 액션에서 반환하는 타입을 지정하여 사용했다.
Typescript로 Redux-toolkit 사용하기를 보고 READ와 UPDATE에 해당하는 함수만createAsyncThunk
를 사용하여 상태를 변경하고 CREATE와 DELETE의 경우 그대로 유지하고, 함수 사용 시에 api 패칭에 성공한 뒤(fulfilled) READ에 해당하는 함수들을 불러와 상태를 변경했다.
import { createSlice } from '@reduxjs/toolkit';
import { getTodoList, getTodoItem, patchTodoItem, updateChecked } from '@/store/asyncThunks/TodoAPIRedux';
import { MyState } from './MyState';
import { defaultTodoItem } from '@/types/TodoTypes';
const initialState: MyState = {
todoListData: [],
selectedTodo: defaultTodoItem,
};
const toDoListSlice = createSlice({
name: 'toDo',
initialState,
reducers: {},
extraReducers: builder => {
// todo GET
builder.addCase(getTodoList.fulfilled, (state, action) => {
state.todoListData = action.payload;
});
builder.addCase(getTodoItem.fulfilled, (state, action) => {
state.selectedTodo = action.payload;
});
//todo UPDATE
builder.addCase(patchTodoItem.fulfilled, (state, action) => {
state.selectedTodo = action.payload;
});
builder.addCase(updateChecked.fulfilled, (state, action) => {
state.selectedTodo = action.payload;
});
},
});
// action을 export해야 dispatch를 사용가능
export const toDoActions = toDoListSlice.actions;
export type ReducerType = ReturnType<typeof toDoListSlice.reducer>;
export default toDoListSlice.reducer;
rtk에서 slice는 상태의 고유 string값과 초기 상태, 상태를 변경할 수 있는 함수들을 지정해주는 곳이다. reducer를 추가하여 일반적인 이벤트에 대한 상태 변경을, extraReducer를 추가하여 비동기 함수에 대한 상태 변경을 할 수 있다.
action을 export해야 dispatch 함수를 사용할 수 있다.
extraReducer를 사용 시에 dispatch함수의 타입 지정을 위해 reducerType
도 export한다.
createSlice
를 사용하여 slice를 만들었고, slice가 2개 이상이라면 combineReducers
병합해줘야하지만 단순한 todolist 기능으로 하나로 충분하다고 생각해서 toDoListSlice.reducer를 반환했다.
import { Action, ThunkDispatch, configureStore } from '@reduxjs/toolkit';
import todoReducer, { ReducerType } from './todoReducer';
// 스토어 생성
export const store = configureStore({
reducer: {
todoReducer,
},
});
// useSelector의 state 타입 지정
export type RootState = ReturnType<typeof store.getState>;
// useDispatch 타입 지정
export type AppThunkDispatch = ThunkDispatch<ReducerType, unknown, Action<string>>;
export type AppDispatch = typeof store.dispatch;
store에서는 기본적으로 reducer를 설정해 생성했고,
useSelector의 state와 useDispatch의 타입을 지정하여 export했다.
🚨 비동기 이벤트를 다루는 extraReducer의 경우
export type AppThunkDispatch = ThunkDispatch<ReducerType, unknown, Action>;
createAsyncThunk
함수를 사용하여 만든 비동기 상태변경 함수를 사용하기 위해서는 useDispatch의 타입을 위와 같이 지정해야 사용할 수 있다.
extraReducer의 경우는useDispatch<AppThunkDisPatch>()
와 같이 타입을 명시해주지 않으면 에러를 발생시킨다. 즉 reducer와 extraReducer의 타입을 다르게 지정해주어야한다.
import { useState, useEffect, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router';
import { getTodoList, getTodoItem, patchTodoItem, deleteTodo, updateChecked } from '@/store/asyncThunks/TodoAPIRedux';
import { TodoItem } from '@/types/TodoTypes';
import { useDispatch, useSelector } from 'react-redux';
import { AppThunkDispatch, RootState } from '@/store/store';
export const useTodoInfo = () => {
const dispatch: AppThunkDispatch = useDispatch();
const todo = useSelector((state: RootState) => state.todoReducer.selectedTodo);
...
const navigate = useNavigate();
const { _id } = useParams();
const fetchData = useCallback(async () => {
if (_id) {
try {
dispatch(getTodoItem({ _id }));
} catch (err) {
console.error('Error fetching todo:', err);
}
}
}, [_id, dispatch]);
const updateTodo = async (updatedTodo: TodoItem) => {
try {
if (updatedTodo.title === '' || updatedTodo.content === '') {
alert('제목과 내용을 입력해주세요');
return;
}
dispatch(patchTodoItem(updatedTodo));
} catch (err) {
console.error('Error updating todo:', err);
}
};
const handleEditClick = () => {
if (!isEditing) {
setIsEditing(true);
setUpdatedTitle(todo.title);
setUpdatedContent(todo.content);
} else {
if (todo.title === updatedTitle && todo.content === updatedContent) {
alert('수정된 내용이 없습니다.');
return;
}
setIsEditing(false);
updateTodo({ ...todo, title: updatedTitle, content: updatedContent });
}
};
const handleDeleteClick = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
const res = confirm('정말 삭제하시겠습니까?');
if (res) {
await deleteTodo(_id!);
await dispatch(getTodoList());
navigate('/');
}
};
const handleCheckboxChange = async () => {
if (todo._id !== undefined) {
try {
dispatch(updateChecked({ _id: todo._id, title: todo.title, content: todo.content, done: !isChecked }));
setIsChecked(prevChecked => !prevChecked);
} catch (error) {
console.error('Error updating checked state:', error);
}
}
};
useEffect(() => {
fetchData();
}, [fetchData]);
return {
todo,
isEditing,
updatedTitle,
updatedContent,
isChecked,
setIsEditing,
setUpdatedTitle,
setUpdatedContent,
setIsChecked,
handleEditClick,
handleDeleteClick,
handleCheckboxChange,
};
};
위처럼 TodoInfo에서 useSelector와 useDispatch로 Redux state와 actions를 사용했다.
https://ko.redux.js.org/introduction/why-rtk-is-redux-today/
https://redux-toolkit.js.org/usage/usage-with-typescript
https://velog.io/@kandy1002/Typescript%EB%A1%9C-Redux-toolkit-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
https://velog.io/@niboo/Redux-Toolkit-ToDoList-Redux-Toolkit%EC%9C%BC%EB%A1%9C-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%ED%95%B4%EB%B3%B4%EA%B8%B0