Redux-saga

마데슾 : My Dev Space·2020년 3월 18일
0

[CODESTATES]Immersive Flex

목록 보기
27/28

redux-thunk

Promise를 사용하면 resolve 시점에서 객체를 직접 리턴할 수 없다. 그래서 비동기 처리를 할 때 액션 객체를 반환하는 대신 dispatch를 인자로 하는 함수를 리턴해 이 함수안에서 데이터펫칭을 비롯한 네트워킹, 다수의 디스패치 등을 할 수 있게 해주는 미들웨어이다.

예시코드

export function fetchCars() {
   // dispatch를 인자로 하는 함수를 리턴 한다.
  return dispatch => {
    // 요청이 시작됨을 알린다.
    dispatch({ type: FETCH_CARS_BEGIN });
    // API 요청을 실행하며 완료 시 함수는 종결 된다
    return axios
      .get(`${API}/cars`)
      // 성공 시 성공했음을 알리고 받아온 자료를 payload 에 담아 리듀서로 보낸다
      .then(res =>
         dispatch({ type: FETCH_CARS_SUCCESS, payload: res.data })
      )
      // 실패 시 실패했음을 알리고 받은 에러를 payload 에 담아 리듀서로 보낸다
      .catch(err => 
         dispatch({ type: FETCH_CARS_FAILURE, payload: err }));
    };
}

클로저 패턴을 사용해서 소스코드가 깔끔하지 못하다.

redux-saga

  • action이 순수한 객체(Pure Object)만을 반환해서 깔끔하다
  • 비동기 처리같은 단순하지 않은 작업들은 saga에 만들어놓고 누군가 발생시킨 액션중 일치하는 saga와 연결된 액션타입이 있으면 해당 saga를 실행시켜준다.
  • 액션엔 비동기 작업이 아닌 단순히 리듀서와만 통신하는 액션들만 있다
  • API 통신을 하는 비동기 작업 같은 것들은 saga 에 작성한다. 액션 타입명-작성한 제너레이터 함수를 연결 해놓는데 이때 액션을 계속 리스닝하다가 일치하는 액션타입명이 발생할 때 잽싸게 해당 제너레이터 함수를 실행시킨다
  • 이 때 만약 data를 fetching 하는 비동기 액션이 였다면, 내부적으로 다시 단순히 리듀서에 받아온 data를 넣어주는 단순히 리듀서와만 통신하는 액션이 제너레이터 함수 안에 존재 할 것이다.

createSagaMiddle를 Redux 미들웨어를 통해 연결하기

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// saga 미들웨어를 생성합니다.
const sagaMiddleware = createSagaMiddleware()
// 스토어에 mount 합니다.
const store = createStore(
  reducer,
  // redux의 미들웨어로 sagaMiddleware를 사용합니다.
  applyMiddleware(sagaMiddleware)
)

// 그리고 saga를 실행합니다.
sagaMiddleware.run(mySaga)

// 애플리케이션을 render합니다.

예시코드

// action
export const FETCH_CARS = 'FETCH_CARS'
export const SAVE_FETCHED_CARS = 'SAVE_FETCHED_CARS'

export const fetchCars = () => ({
	type: FETCH_CARS
})

export const saveFetchedCars = cars => ({
	type: SAVE_FETCHED_CARS,
	payload: cars
})

// reducer
const INITIAL_STATE = {
	cats: []
}

export default (state = INITIAL_STATE, {type, payload}) => {
	switch (type) {
      case SAVE_FETCHED_CARS:
        return {
        	...state,
          	cars:payload
        }
      default:
        return state
    }
}

// saga
function* fatchCarsSaga() {
	// try-catch 구문으로 오류 제어
    try {
      const { data } = yield axios.get('/cars')
      yield put(actions.saveFetchedCars(data))
      // put : redux store에 dispatch하는 역할
    } catch (error) {
      yield put(actions.에러처리 액션)
    }
}

// 아래의 함수는 추후 sagaMiddleware.run(watchSaga)에 사용되어
// 언제나 액션들을 리스닝한다.
function* watchSaga() {
  // type의 action이 실행되면 fetchCarSaga가 항상(every) 실행
  yield takeEvery(FETCH_CARS, fetchCarsSaga)
}
  1. 리액트의 어떤 컴포넌트에서 액션에 있는 fetchCars 액션 함수 호출
  2. watchSaga에서 액션을 계속 리스닝하고 있다가 FETCH_CARS 액션이 dispatch될때마다 fetchCarSaga 제너레이터 함수를 실행시킨다(takeEvery 헬퍼 함수의 역할)
  3. yield put(actions.saveFetchedCars(data))를 통해 리듀서를 실행한다

만약 여러개의 saga를 묶어서 사용하고 싶다면 all Effect를 사용해서 아래와 같이 만들어줄 수 있다.

import { all } from 'redux-saga/effects';

// all 함수를 통해 Saga들을 하나로 묶어줄수 있다.
export default function* rootSaga() {
  yield all([
    helloSaga(),
    watchIncrementAsync()
  ])
}

// 그후 아래와 같이 sagaMiddleware.run()에 넣어주면 된다.
sagaMiddleware.run(rootSaga)

redux-thunk
redux-saga의 기본
redux-saga에 대하여
redux-saga 소개

generator functions & redux-saga

Redux-saga의 Saga가 바로 제네레이터함수라는 사실..!

  • 제네레이터함수 : function* 키워드로 작성하는 함수
  • 제네레이터 : 제네레이터함수를 호출하면 반환되는 객체
  • 이터러블 프로토콜은 단순히 obj[Symbol.iterator]: Function => Iterator로 표현할 수 있다. 객체는 이터레이터 심볼 키값에 이터레이터를 반환하는 메서드를 가지고 있다면 이터러블이다.
  • 이터레이터 프로토콜도 단순하다. 객체가 next라는 메서드를 가지고 있고, 그 결과로 IteratorResult 라는 객체를 반환하면 된다. 반환되는 IteratorResult는 {done: boolean, value: any} 형태의 단순한 객체다.
// 제네레이터함수
function* myGeneratorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

// 제네레이터
const generator = myGeneratorFunction();

console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
/* 제너레이터는 이터레이터 프로토콜을 따른다. */

// 1. "function"
typeof generator.next;

// 2. {done: boolean, value: any} 반환
generator.next();

//-----//

/* 제너레이터는 이터러블 프로토콜을 따른다. */

// 1. "function"
typeof generator[Symbol.iterator];

// 2. 이터레이터가 반환된다.
const iterator = generator[Symbol.iterator]();
typeof iterator.next(); // {done: boolean, value: any}

// 제너레이터는 이터러블이면서 이터레이터라는 것인데,
// 이터러블에서 반환하는 이터레이터가 바로 자기 자신이다.

saga를 제너레이터함수로 구현하는 이유

Redux-Saga에서 말하는 Saga는 바로 제너레이터함수다. 그럼 왜 Saga를 제너레이터함수로 구현할까? 이는 곧 Redux-Saga가 이펙트라 부르는 것들을 어떻게 만들고 사용하는지와 연관된다. 우리가 Redux-Saga를 사용한다는 것은 곧 Redux-Saga 미들웨어에 우리의 Saga를 등록하고 수행시킨다는 뜻이다. 미들웨어는 Saga를 끊임없이 동작시킨다.

// Saga의 초기화, 시작 코드에는 항상 "run"이 있다.
middleware.run(RootSaga);

Saga는 제너레이터함수이고, 미들웨어는 Saga에게 yield 값을 받아서 또 다른 어떤 동작을 수행할 수 있다. Saga는 명령을 내리는 역할만 하고, 실제 어떤 직접적인 동작은 미들웨어가 처리할 수 있다는 뜻이다. redux-thunk와의 가장 큰 차이점이다.

// redux-thunk 비동기 함수
function asyncIncrement() {
  return async (dispatch) => {
    await delay(1000);
    dispatch({type: 'INCREMENT'});
  };
}

위 함수는 스스로 비동기적인 처리를 직접 수행한다. 저 함수에 대한 테스트가 필요하다면, 1초를 기다리고 dispatch 하는 것을 어떻게 증명할지 생각해보자. 딱히 마음에 드는 방법은 떠오르지 않는다. 문제는 함수 내부에 비동기적인 로직이 그대로 녹아있다는 것이다.

// redux-saga
function* asyncIncrement() {
  // Saga는 아래와 같이 간단한 형태의 명령만 yield 한다.
  yield call(delay, 1000); // {CALL: {fn: delay, args: [1000]}}
  yield put({type: 'INCREMENT'}); //  {PUT: {type: 'INCREMENT'}}
}

call이든 put이든 모두 직접적인 처리를 하지 않는다(call, put은 이펙트 생성자(Effect creator)라 부른다). 명령을 만들어주기만 하고, 이 명령에 따른 직접적인 처리는 모두 미들웨어가 한다. 그래서 이런 Saga는 테스트도 정말 간단하다.

// TestCase
// 실제로 Delay 시키는게 아니라 이에 대한 명령뿐이므로 테스트에서 1초씩 기다릴 필요가 없다.
// 단지 어떤 명령이 내려지는지만 확인하면 된다.

const gen = asyncIncrement();
expect(gen.next().value).toEqual(call(delay, 1000));
expect(gen.next().value).toEqual(put({type: 'INCREMENT'}));

그리고 Saga에서 비동기 처리가 아무리 복잡해도 대부분은 if, else, for와 같은 간단한 코드만으로 구현할 수 있다. 스코프가 복잡해지는 것도 아니다. Redux-Saga는 이런 이점을 위해 제너레이터함수를 Saga로 사용한다.

이펙트

  • 이펙트는 미들웨어에 의해 수행되는 명령을 담고 있는 자바스크립트 객체라고 생각하면 된다.
  • 앞서 잠깐 살펴본 call이나 put 모두 이펙트 생성자고, 생성된 이펙트는 모두 일반 자바스크립트 객체일 뿐이다.
  • 이펙트 생성자는 항상 일반 객체를 만들기만 하고, 어느 다른 동작도 수행하지 않는다.
  • Saga는 명령을 담고 있는, 이펙트라 부르는 순수한 객체를 yield 할 것이고, 미들웨어는 이런 명령들을 해석해 처리하고, 그 결과를 다시 Saga에 돌려준다.
  • 예를 들어 call(fn, arg1, arg2) 이펙트를 Saga에서 yield 했다면, 미들웨어는 fn(arg1, arg2);으로 수행하고 그 결과를 다시 Saga에 전달한다.
  • Effect creators API

Q) Saga는 반드시 이펙트만을 yield해야 하는 것인가?
A) 아니다. 일반적인 Promise도 yield 할 수 있고, 미들웨어는 이 역시도 훌륭히 resolve나 reject를 기다려줄 것이다. 하지만 이런 비동기 로직을 Saga 내부에서 직접 처리하면 테스트, 여러 다른 이펙트들과의 상호작용이 어렵다. thunk에서 크게 달라지는 점이 없다. 때문에 되도록 이펙트만을 yield 하는 Saga를 작성하길 추천한다.

Saga의 이펙트는 10가지 이상으로 우리가 활용하는 데 큰 문제가 없을 만큼 다양하다. 비록 이 글에서 이펙트들을 하나하나 설명하진 못했지만, Effect creators API를 참고한다면 Saga를 적극적으로 활용하는 데 많은 도움이 될 것이다. 이펙트는 단순히 테스트만을 위해서 만든 것이 아니다. 이런 이펙트들은 pulling이나, non-blocking, blocking, parallel 등의 다양한 특징들을 가지고 있고, 이런 특징들을 이용해서 정말 수많은 동작들을 손쉽게 처리할 수 있다. 공식 문서의 Advanced Concepts에 그 내용들이 잘 나타나 있다.

참고블로그

Redux-Saga: 제너레이터와 이펙트
generator functions & Redux Saga
mdn yield

profile
👩🏻‍💻 🚀

0개의 댓글