Redux-toolkit 비동기 함수 type typeScript (todoList RTK 상태관리)

사공 오·2023년 11월 25일
0

Redux 맛보기 공부를 위해 TS 환경에서 만든 todoList에 Redux를 적용하며 배우고자했다.
공식문서와 구글링하며 공부하는데 어떤 것엔 Action을 지정하고 있는데 또 createSlice할 때는 없어 처음엔 헷갈렸는데, 결론은

Redux -> RTK
createAction, createReducer -> createSlice

Redux를 쉽게 사용하기 위해 RTK에서 사용하는 createAction과 createReducer를 더욱 더 함축시킨 것이 createSlice이다.

공식 문서에서도 RTK의 사용을 권장하는 문구가 보이고, api를 사용하여 async 함수를 사용하기에 RTK의 createSlice를 사용했다.

순서

  1. Redux store 생성
  2. React에 store 제공
  3. slice에 넣을 상태, 상태를 변경할 수 있는 함수 생성
  4. Redux state slice 생성
  5. store에 4번에서 생성한 Slice Reducers 추가
  6. useDispatch, useSelector를 통한 Redux state와 actions 사용

1. store 생성 - store.ts

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})

2. React에 store 제공 - App.tsx

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;

3. slice에 넣을 상태, 상태변경 함수 생성 - TodoAPIRedux.ts

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에 해당하는 함수들을 불러와 상태를 변경했다.

4. Redux state slice 생성 - todoReducer.ts

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를 반환했다.

5. store에 4번에서 생성한 Slice Reducers 추가 - store.ts

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의 타입을 다르게 지정해주어야한다.

6. useDispatch, useSelector를 통한 Redux state와 actions 사용 - TodoInfo.hooks.ts

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

0개의 댓글