Redux-Toolkit 에서 Saga 사용하기 (Feat. Typescript)

2021-08-01

오늘 포스트는 Redux-Toolkit 과 Redux-saga를 사용하여 비동기로 API를 받아와보자 타입스크립트와 함께 !

우선 본격적으로 코드를 보기에 앞서 Saga에 대해 알아보자


Redux-Saga 에 대하여

redux-saga 는 애플리케이션의 사이드 이펙트, 예를 들면 데이터 fetching이나 브라우저 캐시에 접근하는 순수하지 않은 비동기 동작들을, 더 쉽고 좋게 만드는 것을 목적으로하는 라이브러리이다.

saga는 애플리케이션의 사이드 이펙트만을 담당하는 별도의 쓰레드라고 보면 되고, redux-saga는 미들웨어이기 때문에 saga라는 쓰레드가 애플리케이션의 redux 액션을 통해 실행되고, 멈추고, 취소할 수 있게 한다. 또 모든 redux 애플리케이션의 상태에 접근할 수 있고 redux 액션도 dispatch 할 수 있게 한다.

saga는 비동기 흐름을 쉽게 읽고, 쓰고, 테스트 할 수 있게 도와주는 ES6의 Generator를 사용한다. Generator를 사용함으로써, 비동기 흐름은 표준 동기식 자바스크립트 코드처럼 보이게 된다. (async/await와 비슷한데, 더 다양한 방법으로 사용할 수 있다)

redux-thunk와 다르게 콜백 지옥에 빠지지 않고 비동기 흐름들을 쉽게 테스트할 수 있게 해주며 액션들을 순수하게 유지 할 수 있다.


이제 본격적으로 코드를 작성하면서 알아보자


우선 redux-toolkit을 생성해보자 

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

//initialState
export const initialState: Description = {
  animal: [],
  isLoading: false,
  error: null,
};

export const slice = createSlice({
  name: 'animal',
  initialState,
  reducers: {
    getDataSuccess: (state, action: PayloadAction<DescriptionParams>) => {
      state.isLoading = true;
      state.animal.length = 0;
      const newState = state.animal.concat(action.payload);
      state.animal = newState;
    },
    getDataFailure: (state, { payload: error }) => {
      state.isLoading = false;
      state.error = error;
    },
    getData: (state, action: PayloadAction<ParamType>) => {
      state.isLoading = false;
    },
  },
});

export const animal = slice.name;
export const animalReducer = slice.reducer;
export const animalAction = slice.actions;

우선 createSlice를 사용할 것 이기 때문에 따로 액션의 타입을 지정해주거나 액션생성함수를 만들어 주는 일을 하지 않아도 된다

initialState를 선언한 뒤 Slice안에 넣고  reducer: {} 안에 리듀서들을 넣어주면 된다. name은 액션의 타입이다.

여기서 액션의 Payload 타입을 PayloadAction으로 지정해주었는데 리듀서에서 액션의 타입을 선언 할 때 사용할 기본 유형은 PayloadAction <PayloadType> 이다

interface로 타입지정을 해보면


// 받아오는 api Data의 type
interface DescriptionParams {
  age: number;
  careAddr: string;
  careNm: string;
  careTel: string;
  chargeNm: string;
  colorCd: string;
  desertionNo: number;
  filename: string;
  happenDt: number;
  happenPlace: string;
}

// state의 타입
interface Description {
  animal: DescriptionParams[];
  isLoading: boolean;
  error: null;
}

// api의 param 타입
interface ParamType {
  city: number;
  kind: number;
}

받아오는 api의 데이터가 객체배열 형식이기 때문에 api 데이터의 interface를 initialState의 animal 이라는 state는 그 interface를

타입으로 가진다


이제 Saga의 코드를 살펴보기 전에 몇가지 살펴보자


yield 란 ?

Promise 가 미들웨어에 yield 될 때, 미들웨어는 Promise 가 끝날때 까지 Saga 를 일시정지 시키는 것

우선 제일 아래의 animalSaga 함수부터 살펴보자 이 프로젝트에서는 어떠한 이벤트가 발생하면 api호출을 위한 param과 함께 getData라는 액션이 dispatch 되게하였다.

아래와 같은 형식으로 dispatch 된다


const param = {
  city: 411400,
  kind: 210020,
};

dispatch(getData(param));

이제 saga의 코드를 살펴보자

import { animalAction } from './animal';
import { call, put, takeEvery } from '@redux-saga/core/effects';
import * as API from '../service/get-data';

interface DescriptionParams {
  age: number;
  careAddr: string;
  careNm: string;
  careTel: string;
  chargeNm: string;
  colorCd: string;
  desertionNo: number;
  filename: string;
  happenDt: number;
  happenPlace: string;
}

interface ParamType {
  city: number;
  kind: number;
}

// get Saga
export function* getDataSaga(action: { payload: ParamType }) {
  const { getDataSuccess, getDataFailure } = animalAction;
  const param = action.payload;
  try {
    const response: DescriptionParams = yield call(API.getAnimal, param);
    // call은 미들웨어에게 함수와 인자들을 실행하라는 명령
    yield put(getDataSuccess(response));
    // put은 dispatch 를 뜻한다.
  } catch (err) {
    yield put(getDataFailure(err));
  }
}

// Main Saga
export function* animalSaga() {
  const { getData } = animalAction;
  yield takeEvery(getData, getDataSaga);
}

위에서 export한 animalAction의 getData를 선언하고 takeEvery에 넣어준다

여기서 takeEvery 는 여러개의 fetchData 인스턴스를 동시에 시작하게한다.

getData라는 액션이 호출이 된다면 getDataSaga라는 함수를 take 한다 라고 보면되겠다 !

이제 getDataSaga에서는 animalAction에서 getDataSuccess와 getDataFailure 이라는 액션 두개를 받아오고 api의 param도 받아와 try 안에서 param과 함께 api를 call (함수 실행) 하고  성공적으로 받아와지면 getDataSuccess 액션을 put(dispatch 하는것) 하고 에러가 생길 경우에는 getDataFailure를 put 하게된다

이것이 saga의 기본 구조라고 보면 되겠다


이제 rootReducer를 살펴보자 

import { animalReducer } from './animal';
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import { animalSaga } from './saga';

// animalReducer 를 rootReducer 로 합쳐 내보냄
const rootReducer = combineReducers({
  animalReducer,
});

export function* rootSaga() {
  // all 은 여러 사가를 동시에 실행시켜준다. 현재는 animalSaga 하나.
  yield all([animalSaga()]);
}

export type ReducerType = ReturnType<typeof rootReducer>;
export default rootReducer;

rootReducer에 combineReducer로 리듀서 파일에서 export한 animalReducer를 담아준다

rootSaga함수는 animalSaga라는 Saga함수를 실행시켜준다

이 함수가 실행 되어져 있어야 미들웨어가 정상적으로 작동한다

마지막으로 ReturnType이라는 type을 지정해 주었는데 TypeScript 환경에서 리덕스를 사용한다면 필수적으로 해주어야 한다.

이유는 useSelector를 이용하여 state를 받아올 때 타입을 지정해주지 않는다면 정상적으로 state를 받아오지 못한다

아래와 같이 하면된다.


const { animal } = useSelector<ReducerType, Description>(
  (state) => state.animalReducer,
);

index 파일

import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
import GlobalStyle from './assets/styles/global-styles';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer, { rootSaga } from './modules/rootReducer';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware],
});

// saga를 실행
sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <Provider store={store}>
    <GlobalStyle />
    <App />
  </Provider>,
  document.getElementById('root'),
);



이렇게 Redux-toolkit과 Saga를 TypeScript 환경에서 사용하는 방법을 간단하게 알아보았다

더 학습해서 능숙하게 다룰 수 있도록 해보자


reference

https://im-developer.tistory.com/195

© 2024 SongChangYeop All rights reserved