Redux-thunk+TS 02 | 리팩토링 (thunk 함수 & 리듀서)

Kate Jung·2022년 1월 7일
0

middlewares & libraries

목록 보기
15/17
post-thumbnail
post-custom-banner

📌 createAsyncThunk 함수 만들기

  • 가장 먼저 해야 할 것

🔹 ‘createAsyncThunk 함수’ 란?

  • thunk(Promise 기반)를 만들어 줌. (1줄로)

  • thunk 만드는 법

    액션 생성함수(createAsyncAction 로 만든) & 함수(Promise 를 만드는)를 파라미터로 가져와서 만들음.

  • 단순히 데이터만 바로 조회하는 형태의 코드일 경우, 편할 것

    • 모든 API 연동 작업에 대하여 (이 유틸 함수를) 사용 할 수는 없을 것

    • 까다로운 로직을 가지고 있는 thunk 함수의 경우엔 직접 작성해야 될 수도 있음.

🔹 코드

◾ src/lib/createAsyncThunk.ts

import { Dispatch } from "redux";
import { AsyncActionCreatorBuilder } from "typesafe-actions"; // AsyncActionCreator에서 변경된 이름

type AnyAsyncActionCreator = AsyncActionCreatorBuilder<any, any, any>;

export default function createAsyncThunk<
  A extends AnyAsyncActionCreator,
  F extends (...params: any[]) => Promise<any>
>(asyncActionCreator: A, promiseCreator: F) {
  type Params = Parameters<F>;

  return function thunk(...params: Params) {
    return async (dispatch: Dispatch) => {
      const { request, success, failure } = asyncActionCreator;
      dispatch(request(undefined)); // 파라미터를 비우면 타입 에러가 나기 때문에 undefined 전달
      try {
        const result = await promiseCreator(...params);
        dispatch(success(result));
      } catch (e) {
        dispatch(failure(e));
      }
    };
  };
}
  • F extends (...params: any[]) => Promise<any>

    FGenerics 로 받아오는데, 해당 타입은 함수(프로미스를 리턴) 형태만 받아올 수 있도록 설정

  • type Params = Parameters<F>;

    함수의 파라미터들의 타입을 추론해줌

    이를 통해, F 함수의 파라미터와 thunk 함수의 파라미터가 동일하게끔 설정 가능

◾ src/modules/github/thunks.ts

import { getUserProfile } from '../../api/github';
import { getUserProfileAsync } from './actions';
import createAsyncThunk from '../../lib/createAsyncThunk';

export const getUserProfileThunk = createAsyncThunk(getUserProfileAsync, getUserProfile);

📌 리듀서 리팩토링

🔹 유틸 함수

◾ 제작

  • 만드는 이유

    현재 API 요청에 관련된 상태의 경우 { loading, error, data } 형태로 관리 중. 이 객체를 더 쉽게 만들기 위해 제작.

  • src/lib/reducerUtils.ts

    export type AsyncState<T, E = any> = {
      data: T | null;
      loading: boolean;
      error: E | null;
    };
    
    export const asyncState = {
      // 다음 코드는 화살표 함수에 Generic 을 설정 한 것입니다.
      initial: <T, E = any>(initialData?: T): AsyncState<T, E> => ({
        loading: false,
        data: initialData || null,
        error: null
      }),
      load: <T, E = any>(data?: T): AsyncState<T, E> => ({
        loading: true,
        data: data || null,
        error: null
      }),
      success: <T, E = any>(data: T): AsyncState<T, E> => ({
        loading: false,
        data,
        error: null
      }),
      error: <T, E>(error: E): AsyncState<T, E> => ({
        loading: false,
        data: null,
        error: error
      })
    };

◾ GithubState 타입 수정

  • src/modules/github/types.ts
    import * as actions from './actions';
    import { ActionType } from 'typesafe-actions';
    import { GithubProfile } from '../../api/github';
    import { AsyncState } from '../../lib/reducerUtils';
    
    export type GithubAction = ActionType<typeof actions>;
    
    export type GithubState = {
      userProfile: AsyncState<GithubProfile, Error>;
    };
  • AsyncState 의 효과

    loadingdataerror 의 타입을 1 줄로 작성 가능(매번 직접 입력x)

◾ 리듀서 수정

src/modules/github/reducer.ts

import { createReducer } from 'typesafe-actions';
import { GithubState, GithubAction } from './types';
import { GET_USER_PROFILE, GET_USER_PROFILE_SUCCESS, GET_USER_PROFILE_ERROR } from './actions';
import { asyncState } from '../../lib/reducerUtils';

const initialState: GithubState = {
  userProfile: asyncState.initial()
};

const github = createReducer<GithubState, GithubAction>(initialState, {
  [GET_USER_PROFILE]: state => ({
    ...state,
    userProfile: asyncState.load()
  }),
  [GET_USER_PROFILE_SUCCESS]: (state, action) => ({
    ...state,
    userProfile: asyncState.success(action.payload)
  }),
  [GET_USER_PROFILE_ERROR]: (state, action) => ({
    ...state,
    userProfile: asyncState.error(action.payload)
  })
});

export default github;

🔹 리듀서 리팩토링 (handleAction 활용)

◾ 구현 이유

리듀서를 만들 때(createReducer 사용), 메서드 체이닝을 하는 방식(handleAction 사용)으로 구현 하면

→ 여러 액션에 대한 하나의 업데이트 함수 설정 가능

// 예시
.handleAction([add, increment], (state, action) =>
    state + (action.type === 'ADD' ? action.payload : 1)
  );

1. createAsyncReducer 함수 제작

src/lib/reducerUtils.ts

import { AnyAction } from "redux";
import { AsyncActionCreatorBuilder, getType } from "typesafe-actions";

export type AsyncState<T, E = any> = {
  loading: boolean;
  data: T | null;
  error: E | null;
};

export const asyncState = {
  // 다음 코드는 화살표 함수에 Generic 을 설정 한 것.
  initial: <T, E = any>(initialData?: T): AsyncState<T, E> => ({
    loading: false,
    data: initialData || null,
    error: null,
  }),
  load: <T, E = any>(data?: T): AsyncState<T, E> => ({
    loading: true,
    data: data || null,
    error: null,
  }),
  success: <T, E = any>(data: T): AsyncState<T, E> => ({
    loading: false,
    data,
    error: null,
  }),
  error: <T, E>(error: E): AsyncState<T, E> => ({
    loading: false,
    data: null,
    error: error,
  }),
};

type AnyAsyncActionCreator = AsyncActionCreatorBuilder<any, any, any>;

export function createAsyncReducer<
  S,
  AC extends AnyAsyncActionCreator,
  K extends keyof S
>(asyncActionCreator: AC, key: K) {
  return (state: S, action: AnyAction) => {
    // 각 액션 생성 함수의 type 추출
    const [request, success, failure] = [
      asyncActionCreator.request,
      asyncActionCreator.success,
      asyncActionCreator.failure,
    ].map(getType);

    switch (action.type) {
      case request:
        return {
          ...state,
          [key]: asyncState.load(),
        };
      case success:
        return {
          ...state,
          [key]: asyncState.success(action.payload),
        };
      case failure:
        return {
          ...state,
          [key]: asyncState.error(action.payload),
        };
      default:
        return state;
    }
  };
}
  • AnyAsyncActionCreator === createAsyncThunk 에서 사용했던 타입
    → (필요 시) 따로 파일(types.ts)로 추출해내서 분리해도 ok

2. github 리듀서 리팩토링

src/modules/github/reducer.ts

import { createReducer } from "typesafe-actions";
import { GithubState, GithubAction } from "./types";
import { getUserProfileAsync } from "./actions";
import { asyncState, createAsyncReducer } from "../../lib/reducerUtils";

const initialState: GithubState = {
  userProfile: asyncState.initial(),
};

const github = createReducer<GithubState, GithubAction>(
  initialState
).handleAction(
  [
    getUserProfileAsync.request,
    getUserProfileAsync.success,
    getUserProfileAsync.failure,
  ],
  createAsyncReducer(getUserProfileAsync, "userProfile")
);

export default github;
  • 효과

    • 코드 짧아짐

    • 앞으로 새로운 API 요청 시

      → 리듀서에 코드 4줄만 추가하면 됨

3. 2에서 부분 리팩토링

[getUserProfileAsync.request, getUserProfileAsync.success, getUserProfileAsync.failure] 부분을 함수(transformToArray)를 만들어서 리팩토링 가능

  1. transformToArray 함수 제작 (src/lib/reducerUtils.ts)
    (...)
    
    export function transformToArray<AC extends AnyAsyncActionCreator>(asyncActionCreator: AC) {
      const { request, success, failure } = asyncActionCreator;
      return [request, success, failure];
    }
  1. 리듀서에 transformToArray 함수 적용 (src/modules/github/reducer.ts)
    import { createReducer } from 'typesafe-actions';
    import { GithubState, GithubAction } from './types';
    import { getUserProfileAsync } from './actions';
    import { asyncState, createAsyncReducer, transformToArray } from '../../lib/reducerUtils';
    
    const initialState: GithubState = {
      userProfile: asyncState.initial()
    };
    
    const github = createReducer<GithubState, GithubAction>(initialState).handleAction(
      transformToArray(getUserProfileAsync),
      createAsyncReducer(getUserProfileAsync, 'userProfile')
    );
    
    export default github;

참고

profile
복습 목적 블로그 입니다.
post-custom-banner

0개의 댓글