typesafe-actions 활용하기

보통인부·2020년 7월 6일
8
post-thumbnail

TypeScript로 redux 활용하기

최근 typescript로 react앱을 만들어 보던 중 redux, 특히 redux-saga 활용 시 불편한 점이 많아 이것저것 찾아보다보니 typesafe-actions라는 패키지가 있어서 간단한 활용법을 소개하고자 한다.

typesafe-actions github 링크

액션 생성함수

typesafe-actions의 createAction 함수를 활용한다.

import { createAction } from 'typesafe-actions';

const CHANGE_INPUT = 'CHANGE_INPUT';

const changeInput = createAction(CHANGE_INPUT, ({ key, value }) => ({ key, value }))();

중요한 점은 createAction함수를 반드시 호출 해줘야 한다는 점이다.
기존 redux-actions에 익숙해져서 자꾸 저 호출부를 빠트려 에러 뜬적이 정말 많았다.

비동기 액션 생성

createAsyncAction 함수를 활용한다.

import { createAsyncAction } from 'typesafe-actions';

// login request action에 대한 payload type
interface LoginPayload {
  username: string;
  password: string;
}

// Login request success 시의 response type
interface LoginResponse {
  _id: string;
  username: string;
}

const LOGIN_REQUEST = 'LOGIN_REQUEST';
const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
const LOGIN_FAILURE = 'LOGIN_FAILURE';

const loginAsync = createAsyncAction(
  LOGIN_REQUEST, 
  LOGIN_SUCCESS, 
  LOGIN_FAILURE
)<LoginPayload, LoginResponse, Error>();

axios를 활용하는 경우 Response는 AxiosResponse<응답타입>, AxiosError 형식을 generic의 인수로 입력해주면 된다.

const loginAsync = createAsyncAction(
  LOGIN_REQUEST, 
  LOGIN_SUCCESS, 
  LOGIN_FAILURE
)<LoginPayload, AxiosResponse<LoginResponse>, AxiosError>();

마찬가지로 반드시 createAsyncAction 함수를 호출해주어야 액션 생성함수가 정상적으로 생성된다.

참고로 네번째 인자에 cancel에 대한 액션을 별도로 추가할 수 있다.(optional)

Reducer

createReducer 함수를 활용한다.

import { createReducer } from 'typesafe-actions';

interface State {
  username: string;
  password: string;
  login: LoginResponse | null;

const initialState = {
  username: '',
  password: '',
  login: null
}

const login = createReducer(initialState)
  .handleAction(changeInput, (state, { payload: { key, value } }) => ({
	...state,
  	[key]: value
  }))
  .handleAction(actionCreator, reducer)
  .handleAction([actionCreator1, actionCreator2], reducer)

handleAction chain API를 활용하여 복수의 case에 대한 reducer 함수를 등록할 수 있다.
액션 생성함수는 배열의 형태로 여러개를 한번에 입력하는것도 가능하다.

const login = createReducer(initialState, {
  [CHANGE_INPUT]: (state, action) => ({
    ...state,
    [action.payload.key]: action.payload.value
  })
})

이런식으로도 작성 가능하다.

SAGA 만들기

saga생성은 별 다를건 없다.

function *loginSaga(action: ReturnType<typeof loginAsync.request>): Generator {
  try {
    const response: LoginResponse = yield call(loginAPI.login, action.payload);
    yield put(loginAsync.success(response);
  } catch(e) {
    yield put(loginAsync.failure(e)
  }
}

createAsyncSaga utility 함수 만들기

createAsyncAction함수를 통해 생성된 비동기 액션 생성함수를 입력 받아 saga를 리턴하는 함수를 만들어보자.

먼저 Request API에 대한 타입을 정의한다.

// api별로 argument가 있을수도 없을수도 있음
type PromiseCreatorFunction<P, T> = 
  | ((payload: P) => Promise<T>)
  | (() => Promise<T>);

그다음 해당 액션의 payload 존재 유무를 확인하는 함수를 작성한다.

function isPayloadAction<P>(action: any): action is PayloadAction<string, P> {
	return action.payload !== undefined;
}

위 함수를 통해 해당 액션이 payload를 가지고 있는지 확인할 수 있다.

function createAsyncSaga<P1, P2, P3>(
  asyncActionCreator: AsyncActionCreatorBuilder<
  	[string, [P1, undefined]],
  	[string, [P2, undefined]],
    [string, [P3, undefined]]
  >,
  promiseCreator: PromiseCreatorFunction<P1, P2>
) {
  return function* saga(
  	action: ReturnType<typeof asyncActionCreator.request>
  ) {
    try {
      const response: P2 = isPayloadAction<P1>(action) // payload 존재여부 확인
        ? yield call(promiseCreator, action.payload)
        : yield call(promiseCreator);
      yield put(asyncActionCreator.success(response.data);
    } catch(e) {
        yield put(asyncActionCreator.failure(e));
    }
  }
}   

AsyncActionCreatorBuilder는 typesafe-actions의 내장 타입으로 generic에 3가지 변수를 받는다. 각각 [request type, [payload, meta]], [success type, [payload, meta]], [failure type, [payload, meta]] 형태로 입력해주면 된다.

createAsyncReducer utility 함수 만들기

asyncAction이 dispatch될 때의 action별 reducer를 utility함수를 만들어 처리해보자.

먼저 비동기 요청에 대한 상태를 미리 정의해둔다.

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

const asyncState = {
  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,
  }),
};

비동기 요청에 대한 상태는 loading, data, error 속성을 가지며 initial, load, success, error 단계별로 상태를 미리 정의해 두었다.

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

export function transformToArray(
  asyncActionCreator: AnyAsyncActionCreator
) {
  const { request, success, failure } = asyncActionCreator;
  return [request, success, failure];
}

export function createAsyncReducer<
  S,
  AC extends AnyAsyncActionCreator,
  K extends keyof S
>(asyncActionCreator: AC, key: K) {
  const [request, success, failure] = transformToArray(
	asyncActionCreator,
  ).map(getType);
  return {
    
    [request]: (state: S) => ({
      ...state,
      [key]: asyncState.load(),
    }),
    [success]: (
      state: S,
    	action: ReturnType<typeof asyncActionCreator.success>,
    ) => ({
      ...state,
      [key]: asyncState.success(action.payload),
    }),
    [failure]: (
      state: S,
      action: ReturnType<typeof asyncActionCreator.failure>,
    ) => ({
      ...state,
      [key]: asyncState.error(action.payload),
    }),
  };
}

getType은 typesafe-actions의 내장함수로 인수로 제공된 액션 생성함수의 액션 type을 리턴한다.
transformToArray함수를 통해 asyncActionCreator를 배열 형태로 변환하고 각 액션 생성함수의 액션 type을 받아 request, success, failure 변수에 저장해둔다.
그런 다음 액션 타입별로 리듀서 함수를 정의해두었다.

reducer 생성

이제 위에서 만든 함수를 활용하여 비동기 액션에 대한 reducer를 만들어 보자.

interface LoginState {
  username: string;
  password: string;
  login: AsyncState<Response, Error>
}

const login = createReducer<LoginState>(initialState, {
  ...createAsyncReducer(loginAsync, 'login'),
  [CHANGE_INPUT]: (state, action) => ({
    ...state,
    [action.payload.key]: action.payload.value
  })
})

createAsyncReducer와 일반 action을 handleAction chain API로 연결시키면 타입에러가 뜨는데 원인은 더 찾아봐야 할 듯 하다.
하나의 module에 동기식과 비동기식 액션이 같이 있는 경우 위 처럼 작성해주면 된다.

reference

velopert님의 github 'ts-react-redux-tutorial'
typesafe-actions 공식문서

0개의 댓글