TIL

Redux-Saga

유댕2 2021. 4. 21. 11:18

Redux-Saga란?

💡 리덕스 사가는 비동기 작업처럼 리듀서에서 처리하면 안 되는 순수 함수가 아닌 작업을 처리하기 위한 리덕스 미들웨어이다. Redux-Saga는 일반 action을 dispatch하고 Generator라는 것을 통해 function*과 같은 문법을 사용한다.

조금 더 자세히 설명하자면, 리덕스 사가는 애플리케이션의 side Effect를 보다 쉽게 관리하고, 실행하기 쉽고, 오류를 더 잘 처리하는 것을

목표로하는 리덕스 라이브러리이다.

Generators라는 ES6의 기능을 사용해 비동기 흐름을 쉽게 읽고 테스트할 수 있다. 이렇게 하여 비동기 흐름을

표준 동기 JavaScript의 코드처럼 보인다.

여기서 Side Effect는 부작용이 아닌 부수 작용이라고 생각해야한다.

세팅

npm i redux-saga

Generator, Generator function, Effect

리덕스 사가를 학습하다 보면 계속해서 따라 나오는 키워드가 있다. 대표적으로 제너레이터와 제너레이터함수이다.

우선 사가를 직접적으로 공부하기 전 사가의 기본 동작원리인 제너레이터에 관련하여 학습해야 한다.

[ Generator, Generator function ]

Redux-Saga에서 Saga가 바로 제너레이터함수(제너레이터 X)이다.

간단히 말하면 제너레이터는 제너레이터 함수의 반환이다.

function* generateFunction(){
	 yield 1;
	 yield 2;
	 yield 3;
}

const generator = generateFunction();

console.log(generator.next().value); //1
console.log(generator.next().value); //2
console.log(generator.next().value); //3

위의 코드에서 function*이 제너레이터함수(제너레이터 아니다!!)다. 이 제너레이터 함수를 호출 시 반환되는

객체가 제너레이터이다. mdn만 찾아봐도 첫 줄에 제너레이터 함수 반환 값이 제너레이터라고 소개한다.

이 제너레이터는 아래와 같은 3가지 메서드를 가진다.

Generator.prototype.next()

  • yield라는 표현을 통해 yield값을 반환한다.

Generator.prototype.return()

  • 주어진 값을 반환해주고 생성기를 종료함

Generator.prototype.throw()

  • 생성기로 에러를 throw한다.

제너레이터는 이터레이터(Iterator) 프로토콜과 이터러블(Iterable)프로토콜을 따른다.

간단하게 이터러블과 이터레이터에 대해 알아보자.

[ 이터러블(Iterable) ]

Symbol.iterator 메소드를 가지고 있는 객체를 의미한다. 이 트러블 객체는 for of 문으로 순회할 수 있다.

대표적으로는 Array객체가있다.

const array = [1,2,3,4,5];

console.log(Symbol.iterator in array); //true

for (const item of array ) {
	console.log(item) //1,2,3,4,5
}

좀 더 설명하자면 이터러블 프로토콜은 단순히 아래와 같이 표현된다.

obj[Symbol.iterator]: Function => Iterator

객체는 이터레이터 심볼 키값에 이터레이터를 반환하는 메서드를 가지고 있으면 이터러블이다.

[ 이터레이터(Iterator) ]

이터러블 객체에서 Symbol.Iterator메소드를 호출하면 iterator객체를 반환한다.

반환된 iterator객체는 next메서드를 소유하고 있으며 IteratorResult라는 객체를 반환한다.

iteratorResult는 value와 done 을 프로퍼티로 가진다.

여기서 value는 현재 순회하는 값을 갖고 있고 done은 순회가 언제 끝나는지 알려준다.

next메서드는 반복적으로 호출되다 모든 요소를 순회하면 value는 undefined를

done프로퍼티는 true가 되며 순회를 종료한다.

const array = [1,2,3,4,5]

const iterator = array[Symbol.iterator]()

console.log(iterator.next());  //{ value: 1, done: false }
console.log(iterator.next());  //{ value: 2, done: false }
console.log(iterator.next());  //{ value: 3, done: false }
console.log(iterator.next());  //{ value: 4, done: false }
console.log(iterator.next());  //{ value: 5, done: false }
console.log(iterator.next());  //{ value: undefined, done: true }

다시 제너레이터, 제너레이터함수로 돌아가 보자.

위에서 언급했듯 리덕스 사가에서 사가는 제너레이터 함수이고 미들웨어는 saga에게 yield값을 받아 또 다른 어떤

동작을 수행할 수 있다. 사가는 미들웨어에 명령을 내리는 역할을 하고 실제 비동기 작업을 처리해주는 동작은

미들웨어에서 수행한다.

조금 더 자세히 말하면 사가는 미들웨어에 이펙트를 활용해 어떠한 비동기 작업을 하도록 명령을 내리는 역할을

하고 직접적인 비동기 작업 및 side Effect를 수행하는 곳은 미들웨어이며 이 미들웨어가 동작한 후 반환되는

반환 값이 다시 사가에게 돌아오게 되는 것이다.

이렇기 때문에 saga에서 비동기 처리가 아무리 복잡해도 대부분 if, else, for와 같은 간단한 코드로 구현할 수

있다.

[ 이펙트 (Effect) ]

이펙트는 미들웨어에 의해 수행되야하는 명령을 담고 있는 자바스크립트 객체이다.

이펙트 생성자는 일반 객체를 만들기만 하고 어떠한 동작도 하지 않는다.

사가는 이런 명령을 담고 있는 이펙트라는 객체를 yield할 것이고 미들웨어는 이러한 명령을 바탕으로 동작하고

그 결과를 사가에게 돌려주는 것이다. 이제 어떤 이펙트 종류가 있는지 확인해 보자.

takeEvery(action, sagaFn)

  • action이 발생할 때마다 Task가 실행되게 한다.
  • 여러 Task를 동시에 시작할 수 있다.
  • 리덕스 성크과 비슷하게 작동한다.

takeLatest(action, sagaFn)

  • 마지막으로 발생한 하나의 action에만 Task가 실행된다.
  • 실행 중이던 Task는 action이 발생하면 취소되고 새로운 Task가 실행됨.

select

  • state에서 필요한 데이터를 꺼내온다.

put

  • Action을 Dispatch한다.

take

  • 액션 / 이벤트 발생을 기다린다.

call(fn,...args)

  • Promise의 완료를 기다린다.

fork

  • 다른 Task를 시작한다.

join

  • 다른 Task의 종료를 기다린다.

[ 태스크(Task) ]

하나의 saga가 실행되는 것을 태스크라고 한다.

Redux-Saga 사용해 보기

사가를 활용해서 몇 가지 비동기 처리를 해보도록 하겠다.

첫 번째로 날씨 API를 불러오겠다.

// store/modules/weather.js

import { createAction, createReducer } from '@reduxjs/toolkit';

export const GET_WEATHER = 'weather/GET_WEATHER';
export const GET_WEATHER_SUCCESS = 'weather/GET_WEATHER_SUCCESS';
export const GET_WEATHER_FAILURE = 'weather/GET_WEATHER_FAILURE';
export const GET_WEATHER_LOADING = 'weather/GET_WEATHER_LOADING';

export const getWeather = createAction(GET_WEATHER);
export const getWeatherSuccess = createAction(GET_WEATHER_SUCCESS);
export const getWeatherFailure = createAction(GET_WEATHER_FAILURE);
export const getWeatherLoading = createAction(GET_WEATHER_LOADING);

const initialState = {
  isLoading: false,
  weatherData: []
};

const reducer = createReducer(initialState, {
  [getWeatherLoading]: (state) => {
    state.isLoading = true;
  },
  [getWeatherSuccess]: (state, action) => {
    (state.isLoading = false), (state.weatherData = action.payload);
  },
  [getWeatherFailure]: (state) => {
    state;
  }
});

export default reducer;

액션과 리듀서에는 특별히 눈에 띄는 게 없다.

날씨 API 받아올 때 성공, 실패, 로딩 상황에 대해 액션과 리듀서를 작성해주었다.

// store/api.js

// weather API
const API = {
  key: '062f94b6879d4a4a64755999bee3a513',
  base: '<https://api.openweathermap.org/data/2.5/>'
};

export function getWeatherApi() {
  return axios.get(`${API.base}weather?q=Seoul&units=metric&APPID=${API.key}`);
}

위의 코드에서 해당 API를 fetching 한다.

// store/sagas/weather.js

import { put, call, takeEvery, fork } from 'redux-saga/effects';
import * as actions from '../modules/weather';
import { getWeatherApi } from '../api';  --(1)

function* getWeather() {
  try {
    const response = yield call(getWeatherApi); --(2)
    yield put(actions.getWeatherSuccess(response));
  } catch (err) { --(3)
    yield put(actions.getWeatherFailure(err));
  }
}

function* watchGetWeather() {
  yield takeEvery(actions.GET_WEATHER, getWeather); --(4)
}

export default function* watchSaga() {
  yield fork(watchGetWeather);
}

(1) : API.js에서 api를 fetching하는 함수를 가져온다.

(2) : Promise의 완료를 기다리기 위해 관련 이펙트인 call을 사용하였다.

(3) : api fetching실패했을때의 상황을 위해 생성한 리듀서를 사용하였다.

(4) : getWeather에서 미들웨어에서 수행한 결과 반환 값을 (4)에서 다시 돌려받는다.

// store/sagas/index.js

import { all, fork } from 'redux-saga/effects';
import weather from './weather';
import ...

export default function* rootSaga() {
  yield all([fork(weather), ...]);
}

combineReducer처럼 saga컴포넌트가 많아지면 이런 식으로 루트 사가에 한번에 담아서 스토어에 보낸다.

//store/store.js

import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import index from './modules/index';
import rootSaga from './sagas/index';
import { configureStore } from '@reduxjs/toolkit';

const sagaMiddleware = createSagaMiddleware();
const middlewares = [logger, sagaMiddleware];

export const store = configureStore({
  devTools: false,
  middleware: middlewares,
  reducer: index
});

sagaMiddleware.run(rootSaga);

이런식으로 rootSaga를 store로 import해와서 미들웨어 동작을 위해 createMiddleware 그리고

보다 명확히 개발자 도구에서 확인을 위해 logger를 import해온다.

그리고 난 후 sagaMiddleware.run(rootSaga)를 통하여 모든 사가 컴퍼넌트 결과를 실행시킨다.

실행 중인 프로젝트의 개발자 도구의 콘솔창에서 리덕스 로거를 통하여 확인할 수 있다.

위와 같이 원하는 weather API data정보가 들어오는 것을 확인할 수 있다.

참고자료

  • 리덕스 사가 velopert : 링크
  • 리덕스 사가 공식문서 : 링크
  • 제너레이터,제너레이터함수,이펙트 : 링크
  • 리덕스 사가 : 링크
  • Caller vs Callee : 링크

마치면서

리덕스 사가는 비교적 다른 라이브러리에비해 러닝커브가 높다고 소개된다.

실제 사가를 사용하기 위해 제너레이터, 제너레이터함수는 기본이며 iterator, iterable관련

Caller, Callee, Runner함수에 대한 개념 또한 필요로 하다.

또한 사가의 고급 기술에서 외부 이벤트 소스 또는 사가간의 통신을 위해 사용하는 Channels등

학습할 때 많은 어려움이 있다.

하지만 이러한 리덕스 사가를 조금 더 자세히 바라보고 이해한다면 이렇게 많은 기능을 가지고

있는 이렇게 쉽게(?) 사용할 수 있는 라이브러리도 있을까 싶다.