2-5. redux-saga

redux-saga 는 비동기작업처럼, 리듀서에서 처리하면 안되는 순수하지 않은 작업들을 하기 위한 리덕스 미들웨어입니다.

redux-thunk 의 경우 함수를 dispatch 해주었고, reduix-promise-middleware 나 redux-pender 에선 Promise 가 들어있는 액션을 dispatch 해주었다면, redux-saga 에서는 일반 액션을 dispatch 하게 됩니다.

그리고, 특정 액션이 발생하면 이를 모니터링 하여 이에 기반하여 추가적인 작업을 하도록 설계합니다.

redux-saga 에서는 Generator 라는 것을 사용해서 function* 같은 문법을 사용하게 됩니다. (또 다른 참고자료)

redux-saga 의 매뉴얼의 한국어 번역본은 여기 에서 참고 하실 수 있습니다.

모듈 설치

yarn add redux-saga

redux-saga 미들웨어 적용

기존의 redux-pender 와 saga 는 비활성화 하겠습니다.

src/store.js

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import modules from './modules';
const logger = createLogger();

const sagaMiddleware = createSagaMiddleware();

const store = createStore(modules, applyMiddleware(logger, sagaMiddleware));

export default store;

counterSaga 만들기

비동기 카운터를 구현하기 위해 counterSaga 를 만들어보겠습니다.

src/modules/counter.js

import { delay } from 'redux-saga';
import { put, takeEvery } from 'redux-saga/effects';
import { handleActions, createAction } from 'redux-actions';

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const INCREMENT_ASYNC = 'INCREMENT_ASYNC';
const DECREMENT_ASYNC = 'DECREMENT_ASYNC';

export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);
export const incrementAsync = createAction(INCREMENT_ASYNC);
export const decrementAsync = createAction(DECREMENT_ASYNC);

function* incrementAsyncSaga() {
  yield delay(1000);
  yield put(increment());
}

function* decrementAsyncSaga() {
  yield delay(1000);
  yield put(decrement());
}

export function* counterSaga() {
  yield takeEvery(INCREMENT_ASYNC, incrementAsyncSaga);
  yield takeEvery(DECREMENT_ASYNC, decrementAsyncSaga);
}

export default handleActions(
  {
    [INCREMENT]: (state, action) => state + 1,
    [DECREMENT]: (state, action) => state - 1
  },
  1
);
  • put: 새 액션을 dispatch 합니다
  • delay: 작업을 지연시킵니다.
  • takeEvery: 특정 액션을 모니터링 하고, 발생하면 특정 함수를 발생시킵니다.

rootSaga 만들기

우리는 여러개의 saga 를 만들고, 하나의 루트 saga 를 만들것입니다. 지금은 하나밖에 없지만 일단 다음과 같이 all 함수를 사용하여 rootSaga 를 만들어보세요.

src/modules/index.js

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

export function* rootSaga() {
  yield all([counterSaga()]);
}

export default combineReducers({
  counter,
  post
});

pender 관련 코드를 날려주었으니, App.js 에서 pender 관련 코드를 없애주세요.

src/App.js

// ...

export default connect(
  state => ({
    number: state.counter,
    post: state.post.data,
    // loading: state.pender.pending['GET_POST'],
    // error: state.pender.failure['GET_POST']
  }),
  dispatch => ({
    CounterActions: bindActionCreators(counterActions, dispatch),
    PostActions: bindActionCreators(postActions, dispatch)
  })
)(App);

sagaMiddleware 에 rootSaga 적용

rootSaga 를 만드셨다면, store 를 생성한 다음에 sagaMiddleware.run 을 통하여 등록합니다.

src/store.js

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import modules, { rootSaga } from './modules';
const logger = createLogger();

const sagaMiddleware = createSagaMiddleware();

const store = createStore(modules, applyMiddleware(logger, sagaMiddleware));
sagaMiddleware.run(rootSaga);

export default store;

App 에서 increment, decrement 대체

src/App.js

        <button onClick={() => CounterActions.increment()}>+</button>
        <button onClick={() => CounterActions.decrement()}>-</button>

카운터가 비동기적으로 작동하나요?

postSaga 만들기

이번에는, postSaga 를 만들어보겠습니다.

src/modules/post.js

import { createAction, handleActions } from 'redux-actions';
import axios from 'axios';
import { call, put, takeEvery } from 'redux-saga/effects';

function getPostAPI(postId) {
  return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`);
}

const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

export const getPost = createAction(GET_POST, postId => postId);

const something = () => ({
  data: { title: 'hello', body: 'world' }
});

function* getPostSaga(action) {
  console.log(call(something, ''));
  try {
    const response = yield call(getPostAPI, action.payload);
    yield put({ type: GET_POST_SUCCESS, payload: response });
  } catch (e) {
    yield put({ type: GET_POST_FAILURE, payload: e });
  }
}

const initialState = {
  data: {
    title: '',
    body: ''
  }
};

export function* postSaga() {
  yield takeEvery('GET_POST', getPostSaga);
}

export default handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => {
      const { title, body } = action.payload.data;
      return {
        data: { title, body }
      };
    }
  },
  initialState
);
  • call: 첫번째 파라미터로 전달한 함수에 그 뒤에 있는 파라미터들은 전달하여 호출해줍니다. 이를 사용하면 나중에 테스트를 작성하게 될 때 용이합니다. 참고

다 작성하셨으면, 이를 rootSaga 에 포함시키세요.

src/modules/index.js

import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import post, { postSaga } from './post';
import { all } from 'redux-saga/effects';

export function* rootSaga() {
  yield all([counterSaga(), postSaga()]);
}

export default combineReducers({
  counter,
  post
});

어떤가요? 잘 작동하나요?

정리

redux-saga 는 초반진입장벽이 조금 있는 편입니다. 무조건 써야 하는 것은 아니니, 이런게 있다는 것 정도만 알아두세요.

나중에 이 미들웨어에 대해서 더 자세히 알아보고싶다면 redux-saga 로 비동기처리와 분투하다 포스트를 읽어보시면 도움이 될 수 있습니다.

results matching ""

    No results matching ""