본문 바로가기

프론트엔드

[리액트/타입스크립트] 타입스크립트 환경에서 redux 초기 세팅하기

728x90

메인 화면
메인 화면

 

오늘은 네번째 프로젝트 todolist에 redux를 적용하고 기존에 useState로 만든 지역 상태를 전역 상태로 바꿨다. 리액트 + ts 환경에서 reduxredux-thunk를 적용해본 적이 딱 한 번밖에 없어서 이번 구현이 많은 도움이 됐다.

 

npm / react-redux
npm / react-redux

 

나는 이번에 @reuduxjs/toolkit, redux, react-redux를 사용해서 redux 환경을 구축했고, 세 라이브러리 모두 ts를 지원하고 있어 @type 라이브러리를 별도로 설치할 필요는 없다. 

  • @reuduxjs/toolkitconfigureStore를 사용해서 reduxstore 객체를 생성한다. 옛날에는 reduxcreateStore를 활용했었는데 한 때 depracted가 되었었고 지금보니까 아얘 삭제된 것 같다.
  • react-redux는 리액트 환경에서 redux를 편하게 구축할 수 있게 도움을 주는 기능이 많다. 이번에는 최상위 컴포넌트에 store를 연결시켜주는 Provider와 상태를 쉽게 읽고 변경할 수 있는 useSelectoruseDispatch를 사용했다. 개인적으로 connect는 너무 직관적이지 않아서 선호하지 않는다.

redux 환경 구축은 크게 리듀서 파일 작성 -> 루트 리듀서 파일 작성 -> 최상위 컴포넌트 적용 -> 컴포넌트에서 사용으로 나눠지는 것 같다.


리듀서 파일 작성

리듀서 파일은 리액트 + js 환경에서와 마찬가지로 액션 타입 정의 - 액션 객체 생성 함수 - 초기 상태 - 리듀서 작성이다. 다만, 타입을 지정하는 부분이 까다로웠고, 자주 사용하지 않는 switch를 사용해서 더 헷갈렸다.

 

import { TodosTypes } from '../components/TodoList/TodosTypes';

// 상태 타입
interface StateTypes {
  todos: TodosTypes[];
}

// 리듀서 액션 인수 타입 - A
interface SetTodosTypes {
  type: 'todolist/SET_ITEMS';
  todos: TodosTypes[];
}

// 리듀서 액션 인수 타입 - B
type ActionTypes = SetTodosTypes;

// 1. 액션 타입 정의
const SET_ITEMS = 'todolist/SET_ITEMS';

// 2. 액션 객체 생성 함수
export const setTodoslist = (todos: TodosTypes[]) => ({
  type: SET_ITEMS,
  todos
});

// 3. 초기 상태
const initialState: StateTypes = {
  todos: []
};

// 4. 리듀서 함수
const todoReducer = (state = initialState, action: ActionTypes) => {
  switch (action.type) {
    case SET_ITEMS:
      return {
        ...state,
        todos: action.todos
      };
    default:
      return state;
  }
};

export default todoReducer;

 

액션 타입액션 객체 생성 함수는 js 환경과 똑같이 작성하면 되고 함수는 인수에 타입을 지정하면 된다. 그리고 초기 상태도 똑같이 객체 형태로 만들고 타입을 지정하면 된다. 액션 객체 생성 함수컴포넌트 파일에서 사용하기 때문에 export를 하면 된다.

 

리듀서 함수를 만들며 매개변수 action의 타입을 어떻게 정의해야 할지 몰라 까다로웠다. 근데 여러 사이트를 찾아보니 따로 제공하는 타입은 없고 그냥 직접 선언해서 사용하면 된다. 위 코드에서 새로운 액션 객체 생성 함수를 만드는 경우 새로운 타입을 정의하고 ActionTypes|로 연결해주면 된다.

 

초기 상태가 없어서 발생하는 에러
초기 상태가 없어서 발생하는 에러

 

그리고 리듀서 함수의 매개변수 state에 초기 상태를 할당하고 switch 문의 default에 state를 리턴시켜줘야 한다. 그렇지 않은 경우 Error: The slice reducer for key ... 라는 에러가 발생하는데 초기 상태가 리듀서에 제대로 할당되지 않아 발생하는 문제이다. switch 문을 보통 잘 사용하지 않으니 정확히 복습하고 사용해야한다. 익숙하지 않으면 if문으로 대체해도 될 것 같다.


루트 리듀서 파일 작성

import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { useDispatch, useSelector } from 'react-redux';

import todoReducer from './todolist';

const rootReducer = combineReducers({
  todos: todoReducer
});

// 최상위 컴포넌트에서 사용
const store = configureStore({
  reducer: rootReducer
});

// useSelector와 useDispatch의 타입
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

// 타입을 적용한 useSelector와 useDispatch
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();

export default store;

 

원래 js 환경에서는 루트 리듀서store를 다른 파일에서 사용했었다. 그러나 ts 환경에서 useDispatchuseSelector타입을 미리 정해두면 편한데 그러기 위해서는 store 객체에서 각각 타입을 꺼내야 한다. 그래서 store도 함께 루트 리듀서 파일에 작성했고, store만 export해서 최상단 컴포넌트 파일에서 사용했다. 그리고 미들웨어를 사용할 store에 새로운 미들웨어를 등록해서 사용하면 된다.

 

 

물론, useSeletoruseDispatch를 컴포넌트에서 타입을 지정하고 사용해도 된다. 하지만 매번 store 객체나 그 안에서 타입을 가져와와 하는데 매우 비효율적이다. 그리고 redux 공홈에 따르면 두 함수에 미리 타입을 적용해서 사용하면 js 환경에서와 동일하게 사용할 수 있고, useSelector의 경우 redux-thunk 같은 미들웨어의 타입을 쉽게 적용할 수 있는 장점이 있다고 설명한다.


최상단 컴포넌트 설정

import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';

import { GlobalStyle } from './main.style';
import Header from './components/Header/Header';
import TodoList from './components/TodoList/TodoList';
import store from './modules';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <GlobalStyle>
      <Header />
      <TodoList />
    </GlobalStyle>
  </Provider>
);

 

react-redux에서 Providerimport해서 루트 리듀서 파일에서 만든 store 객체를 어트리뷰트에 할당하면 된다. Provider는 흔한 이름이라 라이브러리마다 겹치는 경우가 있어서 조심해야 한다.


컴포넌트에서 사용

const TodoList = () => {
  const dispatch = useAppDispatch();
  const todos = useAppSelector((state) => state.todos.todos);

  const setTodos = useCallback(async () => {
    try {
      const data = (await getTodoListApi()) as TodosTypes[];

      dispatch(setTodoslist(data));
      if (data.length > 0) {
        id.current = Number(data[data.length - 1]['id']) + 1;
      }
    } catch (e) {
      console.log(e);
    }
  }, [dispatch]);

  const addTodo = async (todo: TodoInputTypes) => {
    // ...
  };

  const removeTodo = async (id: string) => {
    // ...
  };

  const updateTodoDone = async (id: string) => {
    // ...
  }, [setTodos]);
	
  useEffect(() => {
    setTodos();
  }, [setTodos]);
  
  return // ...
};

 

기존에 루트 리듀서 파일에서 만든 useAppDispatchuseAppSeletor를 import해서 js 환경에서와 똑같이 사용하면 된다. useAppSelector의 콜백함수의 인자로 state에 있는 전역 상태가 들어온다.

 

useAppDispatchdispatch 함수를 만들어서 리듀서 파일에서 작성한 액션 생성 함수를 인자로 넣어서 사용하면 된다.


우선 프로젝트의 CRUD 코드에 모두 api 요청이 있어서 redux를 적용했는데도 해당 로직을 컴포넌트에서 리듀서로 옮기지 못했다. redux-thunk나 redux-saga를 써야는데 오늘 redux를 구축하고 보니 우선 ts 환경에 익숙해지는 것이 먼저인 것 같다.

 

확실히 redux가 성능이 좋다고는 하지만 보일러 플레이트 코드가 너무 긴 것 같다. 심지어 ts를 적용하니 각 객체의 타입을 찾는 시간도 너무 길고 코드도 가독성이 더 떨어진다. 이렇게 코드의 양이 많아 유지보수를 하거나 에러를 파악하기 쉽지 않은 것 같다. 이번에도 CRUD를 마친 상태에서 redux로 수정하니 에러도 많고 시간도 오래 걸렸다. 성능 차이가 얼마나 심한지는 모르겠지만 간단하게 사용할 수 있는 jotai나 recoil에 비해 얼마나 좋은건진 모르겠다.

 


 

 

Usage With TypeScript | Redux

- Standard patterns for setting up a Redux app with TypeScript

redux.js.org

 

Style Guide | Redux

Redux Style Guide: recommended patterns and best practices for using Redux

redux.js.org

728x90