[Redux] Redux-saga 미들웨어

LMH·2023년 1월 1일
0
post-thumbnail

이번 포스팅에서는 리덕스의 비동기적인 작업의 처리하는 방법 중 saga 미들웨어에 대해서 정리하겠습니다.

Redux-saga

saga는 thunk 보다 까다로운 상황에서 유용하게 사용할 수 있는 미들웨어로 다음과 같은 상황에 유리합니다.

  1. 기존 요청을 취소 처리할 경우
  2. 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계 없는 코드를 실행할 경우
  3. 웹소켓을 사용할 경우
  4. API 요청 실패 시 재요청해야 하는 경우

제너레이터 문법 사용

redux-saga에서는 ES6의 제너레이터 함수를 사용합니다. 제너레이터 함수 를 사용하면 함수를 특정 구간에 정지시켜 놓고 원하는 시점으로 돌아가는 것이 가능합니다. 제너레이터는 지난 포스팅에서 다룬 내용으로 이번 포스팅에서는 자세히 다루지 않겠습니다

saga는 제터레이터를 이용하여 액션을 모니터링 합니다. 제터레이터 객체에 액션 객체를 전달시키면 액션 객체의 타입에 따라 원하는 작업을 수행할 수 있습니다.

While문을 이용해서 액션 타입이 반복적으로 전달되어도 제너레이터 함수가 리턴하는 객체의 done 프로퍼티는 항상 false를 유지할 수 있습니다.

function* watchGenerator() {  // 제너레이터 함수 선언
    console.log('모니터링 시작!');
    while(true) {
        const action = yield;
        if (action.type === 'HELLO') {
            console.log('안녕하세요?');
        }
        if (action.type === 'BYE') {
            console.log('안녕히가세요.');
        }
    }
}

const watch = watchGenerator(); // 제너레이터 객체 생성

// next 메소드를 이용해서 yield값 조회
watch.next(); // { value: undefined, done : false }
// action 값에 { type : 'HELLO'}가 할당되면서 '안녕하세요?' 출력
watch.next({'HELLO'});  // '안녕하세요?' { value: undefined, done : false }
// action 값에 { type : 'BYE'}가 할당되면서 '안녕하세요?' 출력
watch.next({'BYE'});// '안녕히가세요' { value: undefined, done : false }
watch.next({'HELLO'});  // '안녕하세요?' { value: undefined, done : false }
watch.next({'BYE'});// '안녕히가세요' { value: undefined, done : false }

saga를 이용한 비동기 카운터를 만드는 코드를 살펴 보겠습니다. saga에서는 다양한 effect를 사용해서 작업을 처리할 수 있습니다. 특히 takeEvery나 takeLatest를 사용하면 보다 복잡한 작업을 간단하게 처리가 가능합니다.

// counter.js
import { handleActions } from 'redux-actions';
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effect'

// 액션 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/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 });

// 사가 함수 선언
function* increaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(increase()); // put은 특정 액션을 디스패치 합니다.
}
function* decreaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(decrease()); // put은 특정 액션을 디스패치 합니다.
}
function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC); // 들어오는 모든 액션에 대해 특정 작업을 처리합니다.
  yield takeLatest(DECREASE_ASYNC); // 기존에 진행 중이던 작업이 있으면 취소하고 가장 마지막으로 실행된 작업을 수행합니다.
}

const initialState = 0;

const counter = handleActions(
  {
  	[increase] : state => state + 1,
    [decrease] : state => state - 1,
  },
  initialState
);

export default counter;

여러가지 리듀서를 사용할 경우 리덕스 라이브러리의 combineReducer를 이용해서 하나의 rootReducer로 만들어 관리하는 것처럼 saga 함수들을 rootSaga로 합쳐서 사용합니다.

//reducer.js
import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import posts from './posts';
import { all } from 'redux-saga/effects';

const rootReducer = combineReducers({ counter });

export function* rootSaga() {
  yield all([counterSaga()]); // all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}

export default rootReducer;
//store.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './reducer';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';

const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만듭니다.

const store = createStore(
  rootReducer,

  composeWithDevTools(
    applyMiddleware( // 여러개의 미들웨어를 적용 할 수 있습니다.
      sagaMiddleware, // 사가 미들웨어를 적용합니다.
      logger   // logger 를 사용하는 경우, logger가 가장 마지막에 와야합니다.
    )
  )
); 

sagaMiddleware.run(rootSaga); // 루트 사가를 실행해줍니다.(스토어 생성이 된 다음에 위 코드를 실행해야합니다.)

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

serviceWorker.unregister();

정리

saga 미들웨어는 제너레이터 함수라는 개념으로 인해 thunk에 비해 학습장벽이 있으나, 사용하는데 익숙해지면 saga에서 제공하는 effect를 사용해서 직관적이고 가독성 좋은 코드를 작성할 수 있다는 것이 가장 큰 장점이라고 생각합니다. 그리고 takeEvery, takeLatest와 같은 특수한 effect를 필요한 순간에 사용한다면 효율적으로 비동기적인 작업을 처리할 수 있습니다.

다만, 우려되는 점은 다른 개발자들과 협업하는데 있어서 학습장벽이 있는 saga 미들웨어를 사용하는게 작업 효율에 부정적인 영향을 미칠 수도 있겠다는 생각이 들었습니다. 또한 특수한 effect를 사용할 정도로 프로젝트의 기능이 복잡하지 않다면 다른 라이브러리를 사용하는 것이 더 효율적일 수 있습니다. 그렇기에 saga를 도입하기 전에 현재 상황을 충분히 검토하여 가장 적절한 라이브러리를 사용하는 것이 중요하겠습니다.

Reference

길벗 출판사, 리액트를 다루는 기술

profile
새로운 것을 기록하고 복습하는 공간입니다.

0개의 댓글