thunk
(Promise
기반)를 만들어 줌. (1줄로)
thunk
만드는 법
액션 생성함수(createAsyncAction
로 만든) & 함수(Promise
를 만드는)를 파라미터로 가져와서 만들음.
단순히 데이터만 바로 조회하는 형태의 코드일 경우, 편할 것
모든 API 연동 작업에 대하여 (이 유틸 함수를) 사용 할 수는 없을 것
까다로운 로직을 가지고 있는 thunk 함수의 경우엔 직접 작성해야 될 수도 있음.
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>
F
를 Generics
로 받아오는데, 해당 타입은 함수(프로미스를 리턴) 형태만 받아올 수 있도록 설정
type Params = Parameters<F>;
함수의 파라미터들의 타입을 추론해줌
이를 통해, F 함수의 파라미터와 thunk 함수의 파라미터가 동일하게끔 설정 가능
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
})
};
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
의 효과
loading
, data
, error
의 타입을 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;
리듀서를 만들 때(createReducer
사용), 메서드 체이닝을 하는 방식(handleAction
사용)으로 구현 하면
→ 여러 액션에 대한 하나의 업데이트 함수 설정 가능
// 예시
.handleAction([add, increment], (state, action) =>
state + (action.type === 'ADD' ? action.payload : 1)
);
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
)로 추출해내서 분리해도 oksrc/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줄만 추가하면 됨
[getUserProfileAsync.request, getUserProfileAsync.success, getUserProfileAsync.failure]
부분을 함수(transformToArray
)를 만들어서 리팩토링 가능
transformToArray
함수 제작 (src/lib/reducerUtils.ts
) (...)
export function transformToArray<AC extends AnyAsyncActionCreator>(asyncActionCreator: AC) {
const { request, success, failure } = asyncActionCreator;
return [request, success, failure];
}
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;
참고