비동기 액션을 처리하기 위한 Redux-Middleware중 하나인 Redux-Saga에 대해 알아보자.
redux-saga는 일반 액션을 디스패치하고, 특정 액션이 발생하면 이를 모니터링해서 추가적인 작업을 하도록 설계한다. saga는 Generator라는 문법을 사용하기 때문에 우선 이 문법에 대한 이해가 필요하다.
핵심은 함수를 작성 할 때 함수를 특정 구간에 머물게 하거나, 원하는 시점에 다시 돌아가게 할 수 있다. 또한 결과값을 여러번 반환 할 수도 있다.
function weirdFunction() {
return 1;
return 2;
return 3;
return 4;
return 5;
}
일반적인 함수에서 값을 여러번에 걸쳐 반환하는 것은 불가능하고, 위 함수는 호출 시 1만을 반환 할 것이다.
function* generatorFunction() {
console.log('안녕하세요?');
yield 1;
console.log('제너레이터 함수');
yield 2;
console.log('function*');
yield 3;
return 4;
}
제너레이터 함수를 작성할 때 function*
키워드를 사용한다.
제너레이터 함수를 호출하면 객체가 반환되고, 이를 제너레이터라고 부른다.
const generator = generatorFunction(); // 제너레이터 생성
제너레이터 함수를 호출한다고 해서 해당 함수 안의 코드가 바로 시작되지는 않는다. generator.next()
를 호출해야만 코드가 실행되며, yield
를 한 값을 반환하고 코드의 흐름을 멈추게 된다.
코드의 흐름이 멈추고 나서 generator.next()
를 다시 호출하면 흐름이 이어서 다시 시작된다.
next
를 호출 할 때 인자를 전달하여 이를 제너레이터 함수 내부에서 사용 할 수도 있다.
function* sumGenerator() {
console.log('sumGenerator이 시작됐습니다.');
let a = yield;
console.log('a값을 받았습니다.');
let b = yield;
console.log('b값을 받았습니다.');
yield a + b;
}
redux-saga는 액션을 모니터링 할 수 있다. Generator를 통해 모니터링이 이루어지는 것을 코드를 통해 확인해보자.
function* watchGenerator() {
console.log('모니터링 시작!');
while(true) {
const action = yield;
if (action.type === 'HELLO') {
console.log('안녕하세요?');
}
if (action.type === 'BYE') {
console.log('안녕히가세요.');
}
}
}
const watch = watchGenerator();
이런 원리로 액션을 모니터링하고, 특정 액션이 발생했을때 그에 맞는 자바스크립트 코드를 실행시켜준다.
import { delay, put } from 'redux-saga/effects';
// 액션 생성 함수
export const increase = () => ({ type: 'INCREASE' });
export const decrease = () => ({ type: 'DECREASE' });
export const increaseAsync = () => ({ type: 'INCREASE_ASYNC' });
export const decreaseAsync = () => ({ type: 'DECREASE_ASYNC' });
// saga
function* increaseSaga() {
yield delay(1000); // 1sec delay
yield put(increase()); // action dispatch
}
function* decreaseSaga() {
yield delay(1000); // 1sec delay
yield put(decrease()); // action dispatch
}
// redux-saga/effects util func
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;
}
}
import { combineReducers } from 'redux';
import counter from './counter';
import { counterSaga } from './actions.js
import posts from './posts';
import { all } from 'redux-saga/effects';
const rootReducer = combineReducers({ counter, posts });
// rootSaga
export function* rootSaga() {
yield all([counterSaga()]); // all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}
export default rootReducer;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
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,
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')
);
import React from 'react';
import { Route } from 'react-router-dom';
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<>
<CounterContainer />
</>
);
}
export default App;