Front-End

vsoghlv@naver.com

React.js typesafe Actions 로 리덕스 모듈 리팩토링

typesafe-actions 는 리덕스를 사용한 프로젝트에서 액션 생성 함수와 리듀서를 더욱 쉽고 깔끔하게 작성 할 수 있도록 도와주는 라이브러리이다.

먼저 사용을 위해 설치를 해야한다.

yarn add typesafe-actions

counter

이제 기존에 타입스크립트를 이용해 작성했던 counter.ts 를 하나씩 고쳐보려고 한다.

먼저 typesafe-actions 를 사용하면 기존에 액션 타입을 설정할 때 했던 as const 가 필요없다.

//전
const INCREASE = "counter/INCREASE" as const;
const DECREASE = "counter/DECREASE" as const;
const INCREASE_BY = "counter/INCREASE_BY" as const;

//후
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
const INCREASE_BY = "counter/INCREASE_BY";

액션 생성 함수도 더욱 간단하게 사용가능한데, 이를 위해 createAction 을 사용하거나 createStandardAction 을 사용하기 위해서는 아래와 같이 사용하면 불러오면 된다. 강의를 보면 createStandardAction 을 불러오는 방법이 다른데 버전이 업데이트 되면서 바뀐 것 같다.

import { deprecated, ActionType, createReducer } from 'typesafe-actions';
const { createAction, createStandardAction } = deprecated;
//or
import { ActionType, createAction, createReducer } from "typesafe-actions";
//전
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
  type: INCREASE_BY,
  payload: diff,
});

//후
export const increase = createAction(INCREASE)();
export const decrease = createAction(DECREASE)();
//받아오는 payload 의 타입을 제너릭으로 설정해줘야 한다.
export const increaseBy = createAction(INCREASE_BY)<number>();

액션에 대한 타입은 ActionType 을 이용해 바꿔줄 수 있다. 이때 사용을 위해 각 타입을 객체형태로 담아줘야 한다.

//전
type CounterAction =
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>
  | ReturnType<typeof increaseBy>;

//후
//액션 타입을 위한 객체 생성
const actions = { increase, decrease, increaseBy };
type CounterAction = ActionType<typeof actions>;

리듀서는 createReducer 을 이용한다.

//전
function counter(
  state: CounterState = initialState,
  action: CounterAction
): CounterState {
  switch (action.type) {
    case INCREASE:
      return { count: state.count + 1 };
    case DECREASE:
      return { count: state.count - 1 };
    case INCREASE_BY:
      return { count: state.count + action.payload };
    default:
      return state;
  }
}

//후
const counter = createReducer<CounterState, CounterAction>(initialState, {
  //각 액션에 대한 함수
  [INCREASE]: (state) => ({ count: state.count + 1 }),
  [DECREASE]: (state) => ({ count: state.count - 1 }),
  [INCREASE_BY]: (state, action) => ({ count: state.count + action.payload }),
});

또한 메서드 체이닝 방식으로도 이용이 가능한다. 취향에 따라 어떤 방법을 사용하든 상관은 없지만 메서드체이닝 방식은 코드를 조금 더 간결하게 바꿀 수 있다는 장점이 있다.

액션 생성 함수를 바로 넣어줄 수 있기 때문에 굳이 액션 타입을 따로 선언하지 않고 액션 생성 함수를 만들 때 바로 넣어주면 된다.

//필요없음
//const INCREASE = "counter/INCREASE";
//const DECREASE = "counter/DECREASE";
//const INCREASE_BY = "counter/INCREASE_BY";

//액션 생성 함수
export const increase = createAction("counter/INCREASE")();
export const decrease = createAction("counter/DECREASE")();
//받아오는 payload 의 타입을 제너릭으로 설정해줘야 한다.
export const increaseBy = createAction("counter/INCREASE_BY")<number>();

//필요없음
//액션 타입을 위한 객체 생성
//const actions = { increase, decrease, increaseBy };
//type CounterAction = ActionType<typeof actions>;

const counter = createReducer<CounterState, CounterAction>(initialState)
  .handleAction(increase, (state) => ({ count: state.count + 1 }))
  .handleAction(decrease, (state) => ({ count: state.count - 1 }))
  .handleAction(increaseBy, (state, action) => ({
    count: state.count + action.payload,
  }));

투두리스트

이번에는 기존에 작성했던 todoList 를 변경해 보려고 한다.

액션 생성함수부터 변경해보자.

//전
export const addTodo = (text: string) => ({
  type: ADD_TODO,
  payload: {
    id: nextId++,
    text,
  },
});

export const toggleTodo = (id: number) => ({
  type: TOGGLE_TODO,
  payload: id,
});

export const removeTodo = (id: number) => ({
  type: REMOVE_TODO,
  payload: id,
});

//후
//파라미터로 가져오는 값과 payload 가 다른 경우 createAction 을 사용하지 않는 것이 더 깔끔할 수 있다.
export const addTodo = createAction(ADD_TODO, (text: string) => ({
  id: nextId++,
  text: text,
}))<Todo>();
export const toggleTodo = createAction(TOGGLE_TODO)<number>();

export const removeTodo = createAction(REMOVE_TODO)<number>();

다만 위에 주석으로 적어놨듯이 파라미터로 가져오는 값과 payload 가 다른 경우 createAction 을 사용하지 않는 것이 더 깔끔할 수 있다. 다만 이때는 잊지말고 as const 를 사용해줘야 한다.

//전
type TodosAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof toggleTodo>
  | ReturnType<typeof removeTodo>;
//후
const actions = { addTodo, toggleTodo, removeTodo };
type TodosAction = ActionType<typeof actions>;

마지막으로 리듀서를 변경해보자.

//전
function todos(
  state: TodosState = initialState,
  action: TodosAction
): TodosState {
  switch (action.type) {
    case ADD_TODO:
      return state.concat({
        id: action.payload.id,
        text: action.payload.text,
        done: false,
      });
    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
}
//후
const todos = createReducer<TodosState, TodosAction>(initialState, {
  [ADD_TODO]: (state, action) =>
    state.concat({
      ...action.payload,
      done: false,
    }),
  [TOGGLE_TODO]: (state, action) =>
    state.map((todo) =>
      todo.id === action.payload ? { ...todo, done: !todo.done } : todo
    ),
  [REMOVE_TODO]: (state, action) =>
    state.filter((todo) => todo.id !== action.payload),
});