ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Redux] Redux-Saga
    Front-End(Web)/React - 라이브러리들 2021. 2. 21. 00:29
    반응형

    Redux-saga 역시 공식사이트에서 비동기의 최적화를 위한 미들웨어라고 소개하고 있다.

    채용사이트를 보면 Redux 와 가장 많은 조합을 이루고 있었으며, thunk 에 비해 일관적으로 액션객체를 반환하는 장점이 있다고 한다.

    이러한 Redux-saga에 대해 공부를 하면서, Redux 필수 미들웨어를 확실히 알고 넘어가려고 한다.


    💜 Redux-Saga 알아가기

     

    redux-saga는 애플리케이션에서 일어나는 사이드 이펙트(side effects) (데이터를 불러오는 비동기 처리나 브라우저 캐쉬에 접근하는 행위들)을 쉽게 관리하며 효과적인 실행, 손쉬운 테스트 그리고 에러 핸들링을 쉽게 해준다.

    (원문)
    redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.

     

    위 본문에서 언급하듯, Redux-saga 역시 비동기 처리와 같은 사이드 이펙트를 개선하기 위한 리덕스 미들웨어이다.

    좀 더 원리에 가깝게 얘기하자면, saga는 액션에 대한 리스너로서, 액션을 통한 실행/중단/취소 등을 가능케 해주는 것이다.

    이같은 특징으로, Redux-saga 는 Javascript ES6 문법인 Generator 와 연관성이 깊다. Saga 적용에 앞서 관련개념부터 공부해보았다!

     

    1.  Saga Pattern

    Saga는 분산된 transaction 을 처리하는 패턴으로서 1987년 최초로 언급되었다. 현재, MSA(Microservice Architectual) 에서 여러 개의 서비스 트랜잭션을 핸들링하는데 사용되는 패턴이다.

    단일 포인트에서 처리하는 2PC(two-phase commit) 패턴에 비해, Saga 패턴은 개별 트랙잭션으로 담당 이벤트를 처리한다.

    또한, 다른 트랙잭션은 해당 이벤트 리스너를 통해 이벤트 실행여부를 확인하여 넘어가도록 설계한다.

    출처: https://uzihoon.com/post/181be130-63a7-11ea-a51b-d348fee141c4

    즉, Saga는 단계가 나누어진 이벤트들을 처리하고 에러 핸들링이 용이하도록 설계된 패턴인 것이다. 다음은 Saga의 2가지 방식이다.

    • Orchestraion-based Saga : Orchestrator 가 실행해야할 트랜잭션을 알려준다.
    • Choreography-based Saga: 각 서비스별로 이벤트를 실행하여 다른 트랜잭션을 담당한다.

     

    2. Generator(제너레이터)

    Saga는 언급했듯이, Javascript ES6 제네레이터 문법을 활용한다.

    Generator 를 사용하는 이유는, 일반 함수와 다르게 여러 개의 값을, 그것도 사용자의 요구에 따라 적절한 시기에 반환하는데 있다.

    • Generator Function(제너레이터 함수): yield(정지), next(진행) 등 메서드로 여러 결과값을 적절한 조건에 반환하는 함수
    • Generator(제너레이터, 객체): 제너레이터 함수를 호출하면 반환되는 객체. Iteration Protocol 을 따른다.

    Generator는 Iteration Protocol 을 따른다. Iterator 의 핵심은 루프이다. (즉, 반복가능한 객체로 현재 순서를 인식가능)

    Iterator 의 next() 메서드를 활용하는 등, Generator는 Iteration 개념을 통해 함수의 실행여부를 제어하는 원리이다.

     

    * Iteration Protocol 관련해선, 이전 포스팅을 참고해달라!(abangpa1ace.tistory.com/34?category=910462)

    제너레이터도 조만간 정리해야할듯....

     

    3. Effects(이펙트)

    Redux-saga에서 제네레이터와 함께 중요한 개념인 이펙트이다. 이펙트는 미들웨어로 수행되는 명령을 담은 객체라고 생각하면 된다.

    즉, Redux-saga는 제너레이터의 yield를 통해 이팩트들을 호출하고, 수행된 내용을 돌려받아 다음 처리로 진행되는 것이다.(next)

    이펙트의 종류에 대해 간단히 알아보자!

     

    1) put

    액션을 호출하는 dispatch() 의 역할을 수행하는 이펙트이다.

     

    2) take

    해당 액션이 디스패치되면, 제너레이터를 next() 처리하는 이펙트이다.

    function* helloSaga() {
      console.log('before saga');
      yield take(HELLO_SAGA); // HELLO_SAGA 액션이 들어오면 함수 재진행
      console.log('hello saga'); // HELLO_SAGA 액션 이후 -> 'hello saga'
    }

    위 제너레이터 함수는, yield 에 멈춰있다가, HELLO_SAGA 액션이 들어오면 함수를 재개시킨다. (take 시점부터 진행)

     

    - takeEvery : 모든 액션에 대해 Saga 함수가 동작한다. 같은 종류의 액션이라도 모든 액션에 대해 동작을 실행한다.

    - takeLatest : 마지막 액션에 대해 Saga 함수가 동작한다. 같은 종류의 액션이 여러번 요청되면 이를 파기하고 마지막 액션에 대해서만 동작한다.

     

    3) call, fork

    함수를 인자로 받아, 이를 실행시켜주는 이펙트이다. call은 동기, fork는 비동기 실행을 담당하며, 순서대로 실행하는 API요청에 쓰인다.

     

    이외에도, all() 이나 delay() 같은 이펙트들도 존재하며, 더 많은 이팩트는 공식문서에 소개되어있다. (redux-saga.js.org/docs/api/ )

    제너레이터와 이팩트를 깊게 소개한 것이 아니기에 Saga 이해도가 모호하다. 예제를 만들면서 숙달해가야겠다.


    💜 Redux-Saga 적용

    - 설치

    npm install redux-saga

     

    - 적용 (Counter App 예시)

     

    1) 액션 생성자 만들기

    아래와 같은 카운터 로직이 있다. 액션 생성자는, 액션 객체를 반환하는 기본적인 형태로 만들어준다.

    // modules/counter.js
    const INCREASE = 'INCREASE';
    const DECREASE = 'DECREASE';
    const INCREASE_ASYNC = 'INCREASE_ASYNC';
    const DECREASE_ASYNC = 'DECREASE_ASYNC';
    
    export const increase = () => ({ type: INCREASE });
    export const decrease = () => ({ type: DECREASE });
    export const increaseAsync = () => ({ type: INCREASE_ASYNC });
    export const decreaseAsync = () => ({ type: DECREASE_ASYNC });
    
    const initialState = 0;
    
    export default function counter(state = initialState, action) {
      switch (action.type) {
        case INCREASE:
          return state + 1;
        case DECREASE:
          return state - 1;
        default:
          return state;
      }
    }

     

    2) 액션 Saga 생성

    Generator Function 문법으로 액션 진행방법을 정의한다. 이러한 함수를, redux-saga 에서는 '사가(Saga)' 라고 부른다.

    // modules/counter.js
    
    import { delay, put } from 'redux-saga/effects';
    
    function* increaseSaga() {
      yield delay(1000); 
      yield put(increase()); 
    }
    
    function* decreaseSaga() {
      yield delay(1000); 
      yield put(decrease()); 
    }
    
    export function* counterSaga() {
      yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
      yield takeLatest(DECREASE_ASYNC, decreaseSaga); // 마지막으로 DECREASE_ASYNC 액션만을 처리
    }
    • 제너레이터 yield 명령어를 통해 각 단계가 실행된다.
    • 각 단계에서는 이펙트를 통해 동작을 정의할 수 있다. delay는 특정 ms동안 기다리며, put은 새로운 액션을 디스패치
    • takeEvery, takeLast 는 이펙트 관련 유틸함수이다. 특정 액션에 대해, takeEvery는 모든, takeLast는 마지막 디스패치를 처리

    3) 루트 Saga 생성

    루트 Reducer와 마찬가지로, 앱에 사용되는 많은 사가들을 하나로 묶어준다. (사가 관련부분만 주석설명 첨부)

    // modules/index.js
    
    import { combineReducers } from 'redux';
    import counter, { counterSaga } from './counter';  // counterSaga 모듈사가 가져옴
    import posts from './posts';
    import { all } from 'redux-saga/effects';  // 사가 이펙트 all 가져옴
    
    const rootReducer = combineReducers({ counter, posts });
    export function* rootSaga() {  	// 루트 사가함수 제작
      yield all([counterSaga()]); 	// all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
    }
    
    export default rootReducer;
    • counterSaga 모듈 사가를 가져온다. 루트 사가를 만들기 위한 all(이펙트)을 redux-saga/effects 모듈에서 가져온다.
    • rootSaga() 라는 루트사가 함수를 만들어준다. 마찬가지로 제너레이터 함수 형태로 만든다.
    • all() 이펙트로 사가들을 병합한다. 예시처럼 배열을 인자로 받으며, 안의 사가들을 동시에 실행시켜준다. 

    4) Store 적용

    // store.js
    
    import createSagaMiddleware from 'redux-saga';
    const customHistory = createBrowserHistory();
    const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만듭니다.
    
    const store = createStore(
      rootReducer,
        applyMiddleware(	// 여러개의 미들웨어를 적용
          ReduxThunk.withExtraArgument({ history: customHistory }),
          sagaMiddleware, 	// 사가 미들웨어를 적용 (이 부분의 순서 자체는 크게 상관이 없다.)
          logger,		  	// 로거를 사용하는 경우, 가장 마지막에 와야한다.
      )
    ); 
    sagaMiddleware.run(rootSaga); // 루트 사가를 실행
    • createSagaMiddleware 를 redux-saga 모듈에서 가져오면서, 사가 미들웨어를 만든다.
    • store의 applyMiddleware() 인자로 추가한다. 이후, run(rootSaga) 메서드를 통해 사가를 실행시켜줘야 한다.

    💜 Redux-Saga API 패치

    아래 예시는 이전에, 데이터 패칭을 하기 위해 Redux-thunk 를 적용한, 함수를 반환하는 액션 생성자의 템플릿이다.

    async function fetchAction() {
      return (dispatch) => {
        const res = await fetch('/api/data')
        dispatch({
          type: 'FETCH_ACTION_SUCCESS',
          payload: res.json()
        })
      }
    }

    이러한 비동기에는 redux-saga 역시 널리 사용된다. 사가는 비동기뿐만 아니라 다양한 이펙트로 로직을 구성한다는 장점이 있다. 

     

    - Saga for Fetch

    function* takeFetchAction() {
      yield takeEvery('FETCH_ACTION', dataFetch)
    }
    
    function* dataFetch(action) {
      try {
        const result = yield call(() => fetch('/api/data'))
        yield put({
          type: 'FETCH_ACTION_SUCCESS',
          payload: result.json()
        })
      }
      catch(err) {
        console.error(err); 	// 에러 전용 콘솔
        yield put({
          type: 'FETCH_ACTION_FAIL',
        })
      }
    }
    • takeFetchAction() : takeEvery() 이펙트로 'FETCH_ACTION' 액션마다 콜백함수(dataFetch)를 실행하는 제너레이터 함수
    • dataFetch() : 데이터를 패칭하는 제너레이터 함수. call() 이팩트로 Promise를 처리하며, put()으로 스토어에 디스패치

    * 앞서 이팩트에서, call은 동기, fork는 비동기 호출에 사용된다고 했다. 하지만, 예시에서는 call() 이팩트가 사용되었다. 

    이는 API호출 - 디스패치 순으로 이루어져야하기 때문이며, fork 비동기는 병렬(순서 상관없이 함수들이 실행)에 가깝다고 생각하자!

     

    - Redux-saga 반영 (Redux-Cart)

    // /Saga/FetchSaga.js
    
    import { put, call } from 'redux-saga/effects';
    import { loadStart, loadEnd } from '../Action/loadAction';
    import { fetchList } from '../Action/cartAction';
    import { DATA_API } from '../data';
    
    export default function* fetchSaga() {
      put(loadStart());
      try {
        const res = yield call(() => fetch(DATA_API));
        const result = yield res.json();
        yield put(fetchList(result));	
        yield put(loadEnd());
      }
      catch(err) {
        alert(err);
      }
    }

    먼저, Saga 디렉토리에 FetchSaga.js 라는 사가함수를 만들었다. call() API 통신 후, 이를 fetchList 액션 생성자에 대입한다.

    result 자리에 바로 res.json() 을 넣으면 에러가 생겼는데, 이를 yield 로 수정하니 정상적으로 패칭이 되었다.

     

    // Store.js
    
    import { applyMiddleware, createStore } from 'redux';
    // import ReduxThunk from 'redux-thunk';
    import createSagaMiddleware from 'redux-saga';
    import fetchSaga from './Saga/FetchSaga'
    import RootReducer from './Reducer';
    
    const sagaMiddleware = createSagaMiddleware();
    
    const store = createStore(
      RootReducer,
      applyMiddleware(
        // ReduxThunk,
        sagaMiddleware,
      ),
    );
    
    sagaMiddleware.run(fetchSaga)
    
    export default store;

    다음으로, 스토어에 redux-saga 적용을 해야한다. (주석은 redux-thunk 부분으로, 확실히 로직이 무거워졌다.)

     

    createSagaMiddleware 메서드를 redux-saga 모듈로부터 가져와서, sagaMiddleware 변수에 지정해주었다.

    이를, applyMiddleware 인자로 부여한다. 그리고, 반드시! run([Saga]) 를 통해 사가함수를 가동시켜줘야 한다.

    * 본래는, 여러 사가를 all() 이팩트를 통해 RootSaga 로 묶은 것을 run() 해야 한다. 


    Redux-Saga는 확실히 알아야 할 부분이 많았다. (제너레이터, 이터레이션 프로토콜, 이팩트 등등)

    사가를 프로젝트에 적용한다는 가정하에 알아야 할 최소한의 개념만 정리했는데도 블로그 포스팅이 매우 길어졌다.

     

    확실히, Redux-Thunk 에 비해 러닝커브가 느껴졌으며, 그럼에도 이팩트를 활용한 다양한 로직이 가능해서 애용되는 것 같다.

    사가 심화에 대해서도 추후 공부하겠지만, 이번 제너레이터 공부를 통해 ES6 신문법 정리의 필요성을 다시금 느꼈다.

     

    [출처]

    - Redux-saga 공식 Gitbook: mskims.github.io/redux-saga-in-korean/basics/UsingSagaHelpers.html

    - Redux-saga 벨로퍼트 님의 Gitbook: react.vlpt.us/redux-middleware/10-redux-saga.html  

    - rhostem 님의 블로그: blog.rhostem.com/posts/2017-09-07-redux-saga-toast-control

    반응형
Designed by Tistory.