λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

✏️/React

React κ³΅λΆ€ν•˜κΈ°(7) - redux-saga of λ¦¬λ•μŠ€ 미듀웨어

10. redux-saga : "μ•‘μ…˜μ„ λͺ¨λ‹ˆν„°λ§ν•˜κ³  μžˆλ‹€κ°€, νŠΉμ • μ•‘μ…˜μ΄ λ°œμƒν•˜λ©΄ 이에 따라 νŠΉμ • μž‘μ—…μ„ ν•œλ‹€."

예λ₯Ό λ“€μ–΄,,,

  1. 비동기 μž‘μ—…μ„ ν•  λ•Œ κΈ°μ‘΄ μš”μ²­μ„ μ·¨μ†Œ 처리 ν•  수 μžˆμŠ΅λ‹ˆλ‹€
  2. νŠΉμ • μ•‘μ…˜μ΄ λ°œμƒν–ˆμ„ λ•Œ 이에 따라 λ‹€λ₯Έ μ•‘μ…˜μ΄ λ””μŠ€νŒ¨μΉ˜λ˜κ²Œλ” ν•˜κ±°λ‚˜, μžλ°”μŠ€ν¬λ¦½νŠΈ μ½”λ“œλ₯Ό μ‹€ν–‰ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  3. μ›Ήμ†ŒμΌ“μ„ μ‚¬μš©ν•˜λŠ” 경우 Channel μ΄λΌλŠ” κΈ°λŠ₯을 μ‚¬μš©ν•˜μ—¬ λ”μš± 효율적으둜 μ½”λ“œλ₯Ό 관리 ν•  수 μžˆμŠ΅λ‹ˆλ‹€ (μ°Έκ³ )
  4. API μš”μ²­μ΄ μ‹€νŒ¨ν–ˆμ„ λ•Œ μž¬μš”μ²­ν•˜λŠ” μž‘μ—…μ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

Generator 문법 배우기

=> ν•¨μˆ˜λ₯Ό μž‘μ„±ν•  λ•Œ ν•¨μˆ˜λ₯Ό νŠΉμ • ꡬ간에 멈좰 놓을 μˆ˜λ„ 있고, 원할 λ•Œ λ‹€μ‹œ λŒμ•„κ°€κ²Œ ν•  μˆ˜λ„ μžˆλ‹€.

=> 결과값을 μ—¬λŸ¬λ²ˆ λ°˜ν™˜ν•  μˆ˜λ„ μžˆλ‹€.

function* sumGenerator() {
    console.log('sumGenerator이 μ‹œμž‘λμŠ΅λ‹ˆλ‹€.');
    let a = yield;
    console.log('a값을 λ°›μ•˜μŠ΅λ‹ˆλ‹€.');
    let b = yield;
    console.log('b값을 λ°›μ•˜μŠ΅λ‹ˆλ‹€.');
    yield a + b;
}

 

Generator둜 μ•‘μ…˜ λͺ¨λ‹ˆν„°λ§ν•˜κΈ°

function* watchGenerator() {
    console.log('λͺ¨λ‹ˆν„°λ§ μ‹œμž‘!');
    while(true) {
        const action = yield;
        if (action.type === 'HELLO') {
            console.log('μ•ˆλ…•ν•˜μ„Έμš”?');
        }
        if (action.type === 'BYE') {
            console.log('μ•ˆλ…•νžˆκ°€μ„Έμš”.');
        }
    }
}

 

λ¦¬λ•μŠ€ 사가 μ„€μΉ˜ 및 비동기 μΉ΄μš΄ν„° λ§Œλ“€κΈ°

$ yarn add redux-saga
import createSagaMiddleware from 'redux-saga';

<moduels/counter.js>

import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';

// μ•‘μ…˜ νƒ€μž…
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 });

function* increaseSaga() {
  yield delay(1000); // 1초λ₯Ό κΈ°λ‹€λ¦½λ‹ˆλ‹€.
  yield put(increase()); // put은 νŠΉμ • μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ ν•΄μ€λ‹ˆλ‹€.
}
function* decreaseSaga() {
  yield delay(1000); // 1초λ₯Ό κΈ°λ‹€λ¦½λ‹ˆλ‹€.
  yield put(decrease()); // put은 νŠΉμ • μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ ν•΄μ€λ‹ˆλ‹€.
}

export function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC, increaseSaga); // λͺ¨λ“  INCREASE_ASYNC μ•‘μ…˜μ„ 처리
  yield takeLatest(DECREASE_ASYNC, decreaseSaga); // κ°€μž₯ λ§ˆμ§€λ§‰μœΌλ‘œ λ””μŠ€νŒ¨μΉ˜λœ 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;
  }
}

takeEvery: νŠΉμ • μ•‘μ…˜ νƒ€μž…μ— λŒ€ν•˜μ—¬ λ””μŠ€νŒ¨μΉ˜λ˜λŠ” λͺ¨λ“  μ•‘μ…˜λ“€μ„ 처리

takeLatest: νŠΉμ • μ•‘μ…˜ νƒ€μž…μ— λŒ€ν•˜μ—¬ λ””μŠ€νŒ¨μΉ˜λœ κ°€μž₯ λ§ˆμ§€λ§‰ μ•‘μ…˜λ§Œμ„ 처리

 

<moduels/index.js> 루트 사가 λ§Œλ“€κΈ°

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

const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
  yield all([counterSaga()]); // all 은 λ°°μ—΄ μ•ˆμ˜ μ—¬λŸ¬ 사가λ₯Ό λ™μ‹œμ— μ‹€ν–‰μ‹œμΌœμ€λ‹ˆλ‹€.
}

export default rootReducer;

<index.js>

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga';

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware(); // 사가 미듀웨어λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

const store = createStore(
  rootReducer,
  // logger λ₯Ό μ‚¬μš©ν•˜λŠ” 경우, loggerκ°€ κ°€μž₯ λ§ˆμ§€λ§‰μ— μ™€μ•Όν•©λ‹ˆλ‹€.
  composeWithDevTools(
    applyMiddleware(
      ReduxThunk.withExtraArgument({ history: customHistory }),
      sagaMiddleware, // 사가 미듀웨어λ₯Ό μ μš©ν•˜κ³ 
      logger
    )
  )
); // μ—¬λŸ¬κ°œμ˜ 미듀웨어λ₯Ό 적용 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

sagaMiddleware.run(rootSaga); // 루트 사가λ₯Ό μ‹€ν–‰ν•΄μ€λ‹ˆλ‹€.
// 주의: μŠ€ν† μ–΄ 생성이 된 λ‹€μŒμ— μœ„ μ½”λ“œλ₯Ό μ‹€ν–‰ν•΄μ•Όν•©λ‹ˆλ‹€.

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

serviceWorker.unregister();

<App.js>

import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';
import CounterContainer from './containers/CounterContainer';

function App() {
  return (
    <>
      <CounterContainer />
      <Route path="/" component={PostListPage} exact={true} />
      <Route path="/:id" component={PostPage} />
    </>
  );
}

export default App;

 

 

11. redux-saga둜 ν”„λ‘œλ―ΈμŠ€ 닀루기

 

<modules/posts.js>

import * as postsAPI from '../api/posts'; // api/posts μ•ˆμ˜ ν•¨μˆ˜ λͺ¨λ‘ 뢈러였기
import {
  reducerUtils,
  handleAsyncActions,
  handleAsyncActionsById
} from '../lib/asyncUtils';
import { call, put, takeEvery } from 'redux-saga/effects';

/* μ•‘μ…˜ νƒ€μž… */

// 포슀트 μ—¬λŸ¬κ°œ μ‘°νšŒν•˜κΈ°
const GET_POSTS = 'GET_POSTS'; // μš”μ²­ μ‹œμž‘
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μš”μ²­ 성곡
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μš”μ²­ μ‹€νŒ¨

// 포슀트 ν•˜λ‚˜ μ‘°νšŒν•˜κΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';

export const getPosts = () => ({ type: GET_POSTS });
// payloadλŠ” νŒŒλΌλ―Έν„° μš©λ„, metaλŠ” λ¦¬λ“€μ„œμ—μ„œ idλ₯Ό μ•ŒκΈ°μœ„ν•œ μš©λ„
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });

function* getPostsSaga() {
  try {
    const posts = yield call(postsAPI.getPosts); // call 을 μ‚¬μš©ν•˜λ©΄ νŠΉμ • ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜κ³ , 결과물이 λ°˜ν™˜ 될 λ•ŒκΉŒμ§€ 기닀렀쀄 수 μžˆμŠ΅λ‹ˆλ‹€.
    yield put({
      type: GET_POSTS_SUCCESS,
      payload: posts
    }); // 성곡 μ•‘μ…˜ λ””μŠ€νŒ¨μΉ˜
  } catch (e) {
    yield put({
      type: GET_POSTS_ERROR,
      error: true,
      payload: e
    }); // μ‹€νŒ¨ μ•‘μ…˜ λ””μŠ€νŒ¨μΉ˜
  }
}

// μ•‘μ…˜μ΄ μ§€λ‹ˆκ³  μžˆλŠ” 값을 μ‘°νšŒν•˜κ³  μ‹Άλ‹€λ©΄ action을 νŒŒλΌλ―Έν„°λ‘œ λ°›μ•„μ™€μ„œ μ‚¬μš© ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
function* getPostSaga(action) {
  const param = action.payload;
  const id = action.meta;
  try {
    const post = yield call(postsAPI.getPostById, param); // API ν•¨μˆ˜μ— λ„£μ–΄μ£Όκ³  싢은 μΈμžλŠ” call ν•¨μˆ˜μ˜ λ‘λ²ˆμ§Έ μΈμžλΆ€ν„° μˆœμ„œλŒ€λ‘œ λ„£μ–΄μ£Όλ©΄ λ©λ‹ˆλ‹€.
    yield put({
      type: GET_POST_SUCCESS,
      payload: post,
      meta: id
    });
  } catch (e) {
    yield put({
      type: GET_POST_ERROR,
      error: true,
      payload: e,
      meta: id
    });
  }
}

// 사가듀을 ν•©μΉ˜κΈ°
export function* postsSaga() {
  yield takeEvery(GET_POSTS, getPostsSaga);
  yield takeEvery(GET_POST, getPostSaga);
}

// 3번째 인자λ₯Ό μ‚¬μš©ν•˜λ©΄ withExtraArgument μ—μ„œ λ„£μ–΄μ€€ 값듀을 μ‚¬μš© ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
export const goToHome = () => (dispatch, getState, { history }) => {
  history.push('/');
};

// initialState μͺ½λ„ λ°˜λ³΅λ˜λŠ” μ½”λ“œλ₯Ό initial() ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ„œ λ¦¬νŒ©ν† λ§ ν–ˆμŠ΅λ‹ˆλ‹€.
const initialState = {
  posts: reducerUtils.initial(),
  post: reducerUtils.initial()
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
    case GET_POSTS_SUCCESS:
    case GET_POSTS_ERROR:
      return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
    case GET_POST:
    case GET_POST_SUCCESS:
    case GET_POST_ERROR:
      return handleAsyncActionsById(GET_POST, 'post', true)(state, action);
    default:
      return state;
  }
}

<modules/index.js> rootSaga

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

const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
  yield all([counterSaga(), postsSaga()]); // all 은 λ°°μ—΄ μ•ˆμ˜ μ—¬λŸ¬ 사가λ₯Ό λ™μ‹œμ— μ‹€ν–‰μ‹œμΌœμ€λ‹ˆλ‹€.
}

export default rootReducer;

 

ν”„λ‘œλ―ΈμŠ€λ₯Ό μ²˜λ¦¬ν•˜λŠ” 사가 λ¦¬νŒ©ν† λ§

<lib/asyncUtils.js> createPromiseSaga/createPromiseSagaById λ§Œλ“€κΈ°

import { call, put } from 'redux-saga/effects';

// ν”„λ‘œλ―ΈμŠ€λ₯Ό κΈ°λ‹€λ Έλ‹€κ°€ κ²°κ³Όλ₯Ό λ””μŠ€νŒ¨μΉ˜ν•˜λŠ” 사가
export const createPromiseSaga = (type, promiseCreator) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
  return function* saga(action) {
    try {
      // μž¬μ‚¬μš©μ„±μ„ μœ„ν•˜μ—¬ promiseCreator 의 νŒŒλΌλ―Έν„°μ—” action.payload 값을 넣도둝 μ„€μ •ν•©λ‹ˆλ‹€.
      const payload = yield call(promiseCreator, action.payload);
      yield put({ type: SUCCESS, payload });
    } catch (e) {
      yield put({ type: ERROR, error: true, payload: e });
    }
  };
};

// νŠΉμ • id의 데이터λ₯Ό μ‘°νšŒν•˜λŠ” μš©λ„λ‘œ μ‚¬μš©ν•˜λŠ” 사가
// APIλ₯Ό 호좜 ν•  λ•Œ νŒŒλΌλ―Έν„°λŠ” action.payloadλ₯Ό λ„£κ³ ,
// id 값을 action.meta둜 μ„€μ •ν•©λ‹ˆλ‹€.
export const createPromiseSagaById = (type, promiseCreator) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
  return function* saga(action) {
    const id = action.meta;
    try {
      const payload = yield call(promiseCreator, action.payload);
      yield put({ type: SUCCESS, payload, meta: id });
    } catch (e) {
      yield put({ type: ERROR, error: e, meta: id });
    }
  };
};

// λ¦¬λ“€μ„œμ—μ„œ μ‚¬μš© ν•  수 μžˆλŠ” μ—¬λŸ¬ μœ ν‹Έ ν•¨μˆ˜λ“€μž…λ‹ˆλ‹€.
export const reducerUtils = {
  // 초기 μƒνƒœ. 초기 data 값은 기본적으둜 null μ΄μ§€λ§Œ
  // λ°”κΏ€ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
  initial: (initialData = null) => ({
    loading: false,
    data: initialData,
    error: null
  }),
  // λ‘œλ”©μ€‘ μƒνƒœ. prevState의 κ²½μš°μ—” 기본값은 null μ΄μ§€λ§Œ
  // λ”°λ‘œ 값을 μ§€μ •ν•˜λ©΄ null 둜 바꾸지 μ•Šκ³  λ‹€λ₯Έ 값을 μœ μ§€μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
  loading: (prevState = null) => ({
    loading: true,
    data: prevState,
    error: null
  }),
  // 성곡 μƒνƒœ
  success: payload => ({
    loading: false,
    data: payload,
    error: null
  }),
  // μ‹€νŒ¨ μƒνƒœ
  error: error => ({
    loading: false,
    data: null,
    error: error
  })
};

// 비동기 κ΄€λ ¨ μ•‘μ…˜λ“€μ„ μ²˜λ¦¬ν•˜λŠ” λ¦¬λ“€μ„œλ₯Ό λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.
// type 은 μ•‘μ…˜μ˜ νƒ€μž…, key λŠ” μƒνƒœμ˜ key (예: posts, post) μž…λ‹ˆλ‹€.
export const handleAsyncActions = (type, key, keepData = false) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
  return (state, action) => {
    switch (action.type) {
      case type:
        return {
          ...state,
          [key]: reducerUtils.loading(keepData ? state[key].data : null)
        };
      case SUCCESS:
        return {
          ...state,
          [key]: reducerUtils.success(action.payload)
        };
      case ERROR:
        return {
          ...state,
          [key]: reducerUtils.error(action.payload)
        };
      default:
        return state;
    }
  };
};

// idλ³„λ‘œ μ²˜λ¦¬ν•˜λŠ” μœ ν‹Έν•¨μˆ˜
export const handleAsyncActionsById = (type, key, keepData = false) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
  return (state, action) => {
    const id = action.meta;
    switch (action.type) {
      case type:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.loading(
              // state[key][id]κ°€ λ§Œλ“€μ–΄μ Έμžˆμ§€ μ•Šμ„ μˆ˜λ„ μžˆμœΌλ‹ˆκΉŒ μœ νš¨μ„±μ„ λ¨Όμ € 검사 ν›„ data 쑰회
              keepData ? state[key][id] && state[key][id].data : null
            )
          }
        };
      case SUCCESS:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.success(action.payload)
          }
        };
      case ERROR:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.error(action.payload)
          }
        };
      default:
        return state;
    }
  };
};

<modules/posts.js>

import * as postsAPI from '../api/posts'; // api/posts μ•ˆμ˜ ν•¨μˆ˜ λͺ¨λ‘ 뢈러였기
import {
  reducerUtils,
  handleAsyncActions,
  handleAsyncActionsById,
  createPromiseSaga,
  createPromiseSagaById
} from '../lib/asyncUtils';
import { takeEvery } from 'redux-saga/effects';

/* μ•‘μ…˜ νƒ€μž… */

// 포슀트 μ—¬λŸ¬κ°œ μ‘°νšŒν•˜κΈ°
const GET_POSTS = 'GET_POSTS'; // μš”μ²­ μ‹œμž‘
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μš”μ²­ 성곡
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μš”μ²­ μ‹€νŒ¨

// 포슀트 ν•˜λ‚˜ μ‘°νšŒν•˜κΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';

export const getPosts = () => ({ type: GET_POSTS });
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });

const getPostsSaga = createPromiseSaga(GET_POSTS, postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST, postsAPI.getPostById);

// 사가듀을 ν•©μΉ˜κΈ°
export function* postsSaga() {
  yield takeEvery(GET_POSTS, getPostsSaga);
  yield takeEvery(GET_POST, getPostSaga);
}

// 3번째 인자λ₯Ό μ‚¬μš©ν•˜λ©΄ withExtraArgument μ—μ„œ λ„£μ–΄μ€€ 값듀을 μ‚¬μš© ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
export const goToHome = () => (dispatch, getState, { history }) => {
  history.push('/');
};

// initialState μͺ½λ„ λ°˜λ³΅λ˜λŠ” μ½”λ“œλ₯Ό initial() ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ„œ λ¦¬νŒ©ν† λ§ ν–ˆμŠ΅λ‹ˆλ‹€.
const initialState = {
  posts: reducerUtils.initial(),
  post: reducerUtils.initial()
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
    case GET_POSTS_SUCCESS:
    case GET_POSTS_ERROR:
      return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
    case GET_POST:
    case GET_POST_SUCCESS:
    case GET_POST_ERROR:
      return handleAsyncActionsById(GET_POST, 'post', true)(state, action);
    default:
      return state;
  }
}

 

 

12. sagaμ—μ„œ λΌμš°ν„° μ—°λ™ν•˜κΈ°

 

import { createBrowserHistory } from 'history';

<index.js>

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga';

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware({
  context: {
    history: customHistory
  }
}); // 사가 미듀웨어λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

const store = createStore(
  rootReducer,
  // logger λ₯Ό μ‚¬μš©ν•˜λŠ” 경우, loggerκ°€ κ°€μž₯ λ§ˆμ§€λ§‰μ— μ™€μ•Όν•©λ‹ˆλ‹€.
  composeWithDevTools(
    applyMiddleware(
      ReduxThunk.withExtraArgument({ history: customHistory }),
      sagaMiddleware, // 사가 미듀웨어λ₯Ό μ μš©ν•˜κ³ 
      logger
    )
  )
); // μ—¬λŸ¬κ°œμ˜ 미듀웨어λ₯Ό 적용 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

sagaMiddleware.run(rootSaga); // 루트 사가λ₯Ό μ‹€ν–‰ν•΄μ€λ‹ˆλ‹€.
// 주의: μŠ€ν† μ–΄ 생성이 된 λ‹€μŒμ— μœ„ μ½”λ“œλ₯Ό μ‹€ν–‰ν•΄μ•Όν•©λ‹ˆλ‹€.

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

serviceWorker.unregister();

 

<modules/posts.js> GOTO_HOME

import * as postsAPI from '../api/posts'; // api/posts μ•ˆμ˜ ν•¨μˆ˜ λͺ¨λ‘ 뢈러였기
import {
  reducerUtils,
  handleAsyncActions,
  handleAsyncActionsById,
  createPromiseSaga,
  createPromiseSagaById
} from '../lib/asyncUtils';
import { takeEvery, getContext } from 'redux-saga/effects';

/* μ•‘μ…˜ νƒ€μž… */

// 포슀트 μ—¬λŸ¬κ°œ μ‘°νšŒν•˜κΈ°
const GET_POSTS = 'GET_POSTS'; // μš”μ²­ μ‹œμž‘
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μš”μ²­ 성곡
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μš”μ²­ μ‹€νŒ¨

// 포슀트 ν•˜λ‚˜ μ‘°νšŒν•˜κΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
const GO_TO_HOME = 'GO_TO_HOME';

export const getPosts = () => ({ type: GET_POSTS });
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });
export const goToHome = () => ({ type: GO_TO_HOME });

const getPostsSaga = createPromiseSaga(GET_POSTS, postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST, postsAPI.getPostById);
function* goToHomeSaga() {
  const history = yield getContext('history');
  history.push('/');
}
// 사가듀을 ν•©μΉ˜κΈ°
export function* postsSaga() {
  yield takeEvery(GET_POSTS, getPostsSaga);
  yield takeEvery(GET_POST, getPostSaga);
  yield takeEvery(GO_TO_HOME, goToHomeSaga);
}

(...)