Redux-saga로 비동기 처리

Hyein Son·2021년 5월 5일
0

Redux 액션을 수행하면 Redux-Saga에서 디스패치해 Redux의 액션을 가로챈다. 중간에 가로챈 액션의 역할을 수행 후 다시 액션을 발행하여 데이터를 저장하거나 다른 이벤트를 수행시킨다.

  • 비동기 작업을 할 때 기존 요청을 취소 처리 할 수 있다.
  • 특정 액션이 발생했을 때 다른 액션이 디스패치 되게 하거나, 자바스크립트 코드를 실행 할 수 있다.

generator

일반 함수에서는 값을 하나만 반환 가능하지만 제너레이터를 사용하면 값을 순차적으로 반환할 수 있다. 함수의 흐름을 도중에 멈춰놓았다가 나중에 이어서 진행 할 수도 있다.
function*를 사용해 함수를 만들고 함수를 호출하면 객체가 반환되는데 그것을 제너레이터라고 한다.

function* generatorFunction() {
    console.log('안녕하세요?');
    yield 1;
    console.log('제너레이터 함수');
    yield 2;
    console.log('function*');
    yield 3;
    return 4;
}
// 호출해 반환되는 제너레이터
const generator = generatorFunction() 

호출만 한다고해서 코드가 실행되는 것이 아니다. generator.next()를 호출해야 코드 실행이 된다. 첫번째 yield값을 반환하고 gernerator.next()를 호출하면 그다음 코드가 이어서 실행된다.

generator.next()
// '안녕하세요?'
// { value: 1, done: false }
generator.next()
// '제너레이터 함수'
// { value: 2, done: false }
generator.next()
// 'function*'
// { value: 3, done: false }
generator.next()
// { value: 4, done: true }

인자를 전달해 호출할 수도 있다.

function* sumGenerator() {
    console.log('sumGenerator이 시작됐습니다.');
    let a = yield;
    console.log('a값을 받았습니다.');
    let b = yield;
    console.log('b값을 받았습니다.');
    yield a + b;
}
const sum = sumGenerator
sum.next()
// 'sumGenerator이 시작됐습니다.';
// { value: undefined, done: false }
sum.next(1)
// 'a값을 받았습니다.'
// { value: undefined, done: false }
sum.next(6)
// 'b값을 받았습니다.'
// { value: 7, done: false }

redux-saga/effects

call 특정 함수를 호출하고, 결과물이 반환 될 때까지 기다려준다.
put 새로운 액션을 디스패치
takeEvery 특정 액션 타입에 대하여 디스패치되는 모든 액션들을 처리하는 것
takeLatest 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리하는 함수
ex) 항상 마지막 버전의 데이터만 보여주어야 할 때

takeEvery vs takeLatest vs takeLeading

takeEvery

액션이 dispatch 될 때마다 새로운 task를 생성한다. 동시에 호출될 수 있고 실행순서가 보장되지 않는다. task가 동시에 중복으로 발생해도 문제가 없는 경우에 사용한다.

takeLatest

액션이 dispatch 됐을 때 이전에 이미 실행중인 task가 있으면 취소하고 새로운 task를 실행한다. 동일한 api 요청을 할 경우 가장 마지막 요청에 대한 데이터만 받아온다. 파라미터에 따라 결과값이 달라지는 GET 요청에 적합하다. 응답 데이터가 항상 같은 api 요청일 경우에 api요청이 중복으로 발생하게 되면 불필요한 딜레이가 생길 수 있다. 이런 경우에는 takeLeading로 처리하는 것이 좋다.

takeLeading

처리순서가 중요하고 중복으로 호출되면 안되는 경우에 사용하는 것이 좋다. 어떤 변하지 않는 데이터를 받아오는 경우 이미 요청한 후에 다시 재요청할 필요가 없기 때문에 takeLatest에 비해 빠르게 응답할 수 있다.


api 호출하기

import { call, put, takeEvery } from 'redux-saga/effects'

export function* fetchData(action) {
   try {
      // 두번째 인자값을 전달해 api호출함수를 실행시킨다.
      const data = yield call(Api.fetchUser, action.payload.url)
      // 액션 "FETCH_SUCCEEDED"을 디스패치
      yield put({type: "FETCH_SUCCEEDED", data})
   } catch (error) {
      // 액션 "FETCH_FAILED"을 디스패치
      yield put({type: "FETCH_FAILED", error})
   }
}
// 여러개의 fetchData 인스턴스를 동시에 시작한다. 
function* watchFetchData() {
  yield takeEvery('FETCH_REQUESTED', fetchData)
}

재사용이 가능한 api호출 saga 만들기

loading reducer

api호출할 때마다 매번 loading 상태값을 만드는 번거로움을 줄이기 위해 loading reducer를 만들어 data fetch할 때 적용할 수 있다. data fetch를 요청하는 액션 타입을 상태값으로 한다.

export const loadingSlice = createSlice({
  name: 'loading',
  initialState,
  reducers: {
    startLoading: (state, action: PayloadAction<string>) => {
      state[action.payload] = true;
    },
    finishLoading: (state, action: PayloadAction<string>) => {
      state[action.payload] = false;
    },
  },
});

createFetchAction util함수 생성

data fetch하는 saga를 util함수로 만들어 재사용성을 높인다.

import { AxiosError } from 'axios';
import { call, put } from 'redux-saga/effects';
import { PayloadAction, PayloadActionCreator } from '@reduxjs/toolkit';
import { startLoading, finishLoading } from 'store/reducers/loading';

export const createFetchAction = <P>(
  api: any,
  requestActionType: string,
  successAction: PayloadActionCreator<P>,
  failureAction: PayloadActionCreator<AxiosError>,
) => {
  return function* fetchApi(action: PayloadAction<P>) {
    const payload = action.payload;
    yield put(startLoading(requestActionType));
    try {
      const data: Promise<any> = yield call(api, payload);
      yield put(successAction(data));
    } catch (e) {
      yield put(failureAction(e));
    }
    yield put(finishLoading(requestActionType));
  };
};

data fetch 사가에서 호출할 api, 요청, 성공, 실패에 대한 액션을 인자로 넘겨 실행한다.

export default function* fetchDataSaga() {
  yield takeEvery(
    getLocationTableRequest.type,
    createFetchAction(
      getData, // api
      getDataRequest.type,
      getDataSuccess,
      getDataFailure,
    ),
  );
}

createFetchAction util함수 변형

interface IResponse {
  status: string;
  message: string;
}

 const createFetchAction = <P, T extends IResponse>(
  api: any,
  successAction: PayloadActionCreator<T>,
  failureAction: PayloadActionCreator<Error | string>,
  successFunc?: any,
  failureFunc?: any,
) => {
  return function* fetchApi(action: PayloadAction<P>) {
    const { type, payload } = action;
    yield put(startLoading(type));
    try {
      const data: T = yield call(api, payload);

      const { status, message } = data;

      // status === 'fail' && message가 존재할 때 failureAction 실행
      if (status && status !== 'success'){
        yield put(failureAction(message));

        // failureAction 실행 후 실행할 함수(ex)error message alert, 다른 액션 실행)
        if (failureFunc) {
          yield call<typeof failureFunc>(failureFunc, message);
        }
      } else {
        yield put(successAction(data));
        
        // succeessAction 실행 후 실행할 함수(ex)success alert, 다른 액션 실행)
        if (successFunc) {
          yield call<typeof successFunc>(successFunc, data);
        }
      }
    } catch (e) {
      yield put(failureAction(e));

      if (failureFunc) {
        yield call<typeof failureFunc>(failureFunc, e);
      }
    }
    yield put(finishLoading(type));
  };
};

successFunc : api 요청 성공 후 실행 함수 (ex) 성공 alert or 요청 이후 다른 액션)
failureFunc : api 요청 실패 후 실행 함수 (ex) 실패 alert or 요청 이후 다른 액션)

api 요청은 성공했지만 response가 status: fail이고 message가 있을 경우 에러 처리를 할 수 있다.

0개의 댓글