Redux-saga+TS | saga 사용 / 리팩토링

Kate Jung·2022년 1월 11일
0

middlewares & libraries

목록 보기
16/17
post-thumbnail

📌 개요

  • 목표

    • 프로젝트에서 비동기 작업을 관리하기 위하여 redux-saga 를 사용하는 방법(redux-thunk 대신)을 알아볼 것

    • redux-saga로 똑같이 구현할 것(redux-thunk로 구현했던 것)

  1. redux-saga 설치하기

📌 액션 수정

이유

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>();

📌 saga

🔹 saga(비동기 액션을 처리 할) 작성

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 를 통해서 프로미스를 만드는 특정 함수를 호출했을 경우 프로미스의 결과값에 대한 타입을 유추 불가

🔹 github/index.ts에 saga 불러와서 내보내기

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()]);
}

🔹 리덕스 스토어에 redux-saga 미들웨어 적용

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;

📌 saga 리팩토링

🔹 유틸함수 제작

◾ 유틸함수(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)

◾ saga파일 수정

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);
}

참고

profile
복습 목적 블로그 입니다.

0개의 댓글