
목표
프로젝트에서 비동기 작업을 관리하기 위하여 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의 타입 에러에 대해 내가 임시조치 해둔 것
    }
  };
}
SagaReturnTypesaga에서 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);
}
참고