목표
프로젝트에서 비동기 작업을 관리하기 위하여 redux-saga 를 사용하는 방법(redux-thunk 대신)을 알아볼 것
redux-saga로 똑같이 구현할 것(redux-thunk로 구현했던 것)
이유
GET_USER_PROFILE
액션에서 payload 로 사용자명을 받아오도록 설정
github/actions.ts
undefined
(기존) → string
import { createAsyncAction } from 'typesafe-actions';
import { GithubProfile } from '../../api/github';
import { AxiosError } from 'axios';
export const GET_USER_PROFILE = 'github/GET_USER_PROFILE';
export const GET_USER_PROFILE_SUCCESS = 'github/GET_USER_PROFILE_SUCCESS';
export const GET_USER_PROFILE_ERROR = 'github/GET_USER_PROFILE_ERROR';
export const getUserProfileAsync = createAsyncAction(
GET_USER_PROFILE,
GET_USER_PROFILE_SUCCESS,
GET_USER_PROFILE_ERROR
)<string, GithubProfile, AxiosError>();
src/modules/github/saga.ts
import { getUserProfileAsync, GET_USER_PROFILE } from './actions';
import { getUserProfile, GithubProfile } from '../../api/github';
import { call, put, takeEvery } from 'redux-saga/effects';
function* getUserProfileSaga(action: ReturnType<typeof getUserProfileAsync.request>) {
try {
const userProfile: GithubProfile = yield call(getUserProfile, action.payload);
yield put(getUserProfileAsync.success(userProfile));
} catch (e) {
yield put(getUserProfileAsync.failure(e));
}
}
export function* githubSaga() {
yield takeEvery(GET_USER_PROFILE, getUserProfileSaga);
}
액션 타입 유추 방법: ReturnType
프로미스 결과값의 타입 지정 방법: force type
이유: 아직까지는 Generator 를 사용 할 때, yield call
를 통해서 프로미스를 만드는 특정 함수를 호출했을 경우 프로미스의 결과값에 대한 타입을 유추 불가
src/modules/github/index.ts
export { default } from './reducer';
export * from './actions';
export * from './types';
export * from './thunks';
export * from './sagas';
src/modules/index.ts
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
import github from './github/reducer';
import { githubSaga } from './github';
import { all } from 'redux-saga/effects';
const rootReducer = combineReducers({
counter,
todos,
github
});
// 루트 리듀서를 내보내주세요.
export default rootReducer;
// 루트 리듀서의 반환값를 유추해줍니다
// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.
export type RootState = ReturnType<typeof rootReducer>;
// 루트 사가를 만들어서 내보내주세요.
export function* rootSaga() {
yield all([githubSaga()]);
}
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
변경 사항
GithubProfileLoader
컨테이너에서 thunk 함수를 디스패치request
액션을 디스패치
src/containers/GithubProfileLoader.tsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../modules';
import GithubUsernameForm from '../components/GithubUsernameForm';
import GithubProfileInfo from '../components/GithubProfileInfo';
import { getUserProfileAsync } from '../modules/github';
function GithubProfileLoader() {
const { data, loading, error } = useSelector((state: RootState) => state.github.userProfile);
const dispatch = useDispatch();
const onSubmitUsername = (username: string) => {
dispatch(getUserProfileAsync.request(username));
};
return (
<>
<GithubUsernameForm onSubmitUsername={onSubmitUsername} />
{loading && <p style={{ textAlign: 'center' }}>로딩중..</p>}
{error && <p style={{ textAlign: 'center' }}>에러 발생!</p>}
{data && <GithubProfileInfo bio={data.bio} blog={data.blog} name={data.name} thumbnail={data.avatar_url} />}
</>
);
}
export default GithubProfileLoader;
createAsyncSaga
)saga
(Promise
를 기반으로 작동) 를 쉽게 만들 수 있게 함
이걸로 리팩토링
단순 API 요청만(복잡한 로직 無) 해서 결과값을 받는 경우((createAsyncThunk
와 마찬가지로) 모든 상황에 사용은 어렵겠지만)
→ 생산성에 큰 도움(이유: 쉽게 saga 제작 가능)
src/lib/createAsyncSaga.ts
import { AsyncActionCreatorBuilder, PayloadAction } from "typesafe-actions";
import { call, put, SagaReturnType } from "redux-saga/effects";
/*
유틸함수의 재사용성을 높이기 위하여 함수의 파라미터는 언제나 하나의 값을 사용하도록 하고,
action.payload 를 그대로 파라미터로 넣어주도록 설정합니다.
만약에 여러가지 종류의 값을 파라미터로 넣어야 한다면 객체 형태로 만들어줘야 합니다.
*/
type PromiseCreatorFunction<P, T> =
| ((payload: P) => Promise<T>)
| (() => Promise<T>);
// action 이 payload 를 갖고 있는지 확인합니다.
// __ is __ 문법은 Type guard 라고 부릅니다 https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-type-assertions
function isPayloadAction<P>(action: any): action is PayloadAction<string, P> {
return action.payload !== undefined;
}
export default function createAsyncSaga<T1, P1, T2, P2, T3, P3>(
asyncActionCreator: AsyncActionCreatorBuilder<
[T1, [P1, undefined]],
[T2, [P2, undefined]],
[T3, [P3, undefined]]
>,
promiseCreator: PromiseCreatorFunction<P1, P2>
) {
return function* saga(action: ReturnType<typeof asyncActionCreator.request>) {
type promiseReturnType = SagaReturnType<typeof promiseCreator>;
try {
const result: promiseReturnType = isPayloadAction<P1>(action)
? yield call(promiseCreator, action.payload)
: yield call(promiseCreator);
yield put(asyncActionCreator.success(result));
} catch (e) {
yield put(asyncActionCreator.failure(e as any)); // as any: e의 타입 에러에 대해 내가 임시조치 해둔 것
}
};
}
SagaReturnType
saga에서 yield call의 return type으로 활용
// 1. 'redux-saga/effects' 에서 SagaReturnType를 import 한다.
import { SagaReturnType } from 'redux-saga/effects';
// 2. 타입 정의
// type 타입명 = SagaReturnType<typeof API호출코드에 대한 변수명>;
type promiseReturnType = SagaReturnType<typeof promiseCreator>;
// 3. 정의한 타입 사용
const result: promiseReturnType = yield call(promiseCreator, action.payload)
src/modules/github/sagas.ts
import { getUserProfileAsync, GET_USER_PROFILE } from './actions';
import { getUserProfile } from '../../api/github';
import { takeEvery } from 'redux-saga/effects';
import createAsyncSaga from '../../lib/createAsyncSaga';
const getUserProfileSaga = createAsyncSaga(getUserProfileAsync, getUserProfile);
export function* githubSaga() {
yield takeEvery(GET_USER_PROFILE, getUserProfileSaga);
}
참고