[Redux-Saga] 리덕스에 사가를 함께 써보자! 리덕스 사가 콘솔에 기본 코드 찍어보며 확인하기 !
이 전 글에서는 redux를 써야하는 이유와, 기본적인 구독의 개념, 리듀서 생성하여 state 를 변경해주는 법 , 호출함수를 통해 화면에 그려주는 법까지 알아보았다! 이번 글에서는 reduxjs에서 다루기에 까다로운 비동기 작업들을 처리해주는 redux-saga와 함께 써보도록 하자!
- redux-saga를 함께 쓰면, 액션을 모니터링 하고있다가 발생했을때 특정 작업을 하는 방식으로 사용할 수 있다.
- redux-saga의 장점
1. 비동기 작업에서 기존 요청을 취소 처리 할 수 있다.
2. 특정 액션에 발생했을때, 다른 액션을 디스패치 하거나, 자바스크립트 코드를 실행 할 수 있다.
3. 웹소켓 라이브러리 사용시 Channel이라는 기능을 사용가능하다.
4. API요청이 실패했을 때 재요청하는 작업을 할 수 있다.
하지만, Generator 문법을 사용하기 때문에 진입장벽이 높다. ( function* ... yield.. 너무 생소했다 ㅜ )
제너레이터 함수를 사용하여 함수에서 값을 순차적으로 반환하고, 함수의 흐름을 도중에 멈춰놓았다가 나중에 이어서 진행할 수 있기 때문에 반드시 필요한 문법이라고 할 수 있다!
Generator문법의 작동방식을 먼저 이해하고 진행해보자 !
리덕스 사가에서는 이러한 원리로 액션을 모니터링하고, 특정 액션이 발생했을때 원하는 자바스크립트 코드를 실행한다!
리덕스 사가에서는 제너레이터 함수를 "사가" 라고 부른다 !
import { delay, put } 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은 특정 액션을 디스패치 해줍니다.
}
// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
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;
}
}
여기서 사용한 put이라는 함수는 새로운 액션을 dispatch할 수 있도록 해주는 redux-saga/effects의 유틸함수 이다!
또 다른 액션을 모니터링 해주는 유틸함수에는 takeEvery, takeLatest가 있는데 ,
takeEvery는 특정 액션 타입에 대하여 디스패치되는 모든 액션을 처리한다.
takeLatest는 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리하는 함수이다.
(-> 한 액션을 처리하는 동안 같은 타입의 새로운 액션이 디스패치되면 기존에 하던 작업을 무시하고 새로운 작업을 한다)
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;
}
}
counterSaga 함수의 경우 다른곳에서 불러와서 사용해야 하기 때문에 export 키워드를 사용했다 .
그 다음 나오는 개념은, 프로젝트에서 여러개의 사가를 만들어야 하기 때문에 이를 모두 합쳐놓은 루트사가를 만들어야 한다 !
index.js의 설정 ! (리덕스 스토어에 redux-saga 미들웨어를 적용하는것이다)
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에서 CounterContatiner를 렌더링해보자 !
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;
여기까지 예시 코드의 흐름이 이해가 되었다면,, redux-saga에서 프로미스를 다루는 방법을 배우러 가보자 !