redux-saga는 어플리케이션의 사이드 이펙트(데이터 fetch와 같은 비동기 로직이나 브라우저 캐시에 접근하는 것과 같은 순수하지 않은 것들)를 더 효과적으로 관리하려고 만들어졌다. 즉, 효과적으로 실행하고, 쉽게 테스트하고, 쉽게 에러 핸들링을 하자!는 목적으로 만들어졌다.
그래서 saga는 어플리케이션에서 오로지 사이드 이펙트에만 반응하도록 만들어진 별도 쓰레드와 같다고 할 수 있다. redux-saga는 redux 미들웨어로, 보통의 리덕스 액션으로 시작되고, 중단되며, 취소될 수 있다. 또한 redux 어플리케이션의 모든 상태 값에 접근할 수 있고, redux 액션들을 dispatch할 수도 있다.
redux-saga는 비동기 플로우를 쉽게 읽고, 쓰고, 테스트할 수 있도록 ES6의 Generator라는 개념을 사용한다. 이 Generator를 차용한 덕분에 비동기 코드가 마치 스탠다드한 동기 코드처럼 보여진다. redux-thunk와 다르게 콜백 지옥에 빠질 일도 없고, 비동기 로직을 쉽게 테스트할 수 있으며, 액션들을 순수한 상태로 둘 수 있다.
일단 Generatior 문법부터 알아보도록 하겠습니다.
이 문법의 핵심 기능은 함수를 작성 할 떄 함수를 특정 구간에 멈춰놓을 수도 있고, 원할 때 다시 돌아가게 할 수도 있습니다. 그리고 결과값을 여러번 반환 할 수도 있습니다.
제네레이터 함수 예제를 보겠습니다.
function* generatorFunction() { console.log('안녕하세요?'); yield 1; console.log('제너레이터 함수'); yield 2; console.log('function*'); yield 3; return 4; }
제너레이터 함수는 function 뒤에 *를 붙이고, ex)function*
입니다.
제너레이터 함수를 호출한다고 해서 해당 함수 안의 코드가 바로 시작되지는 않습니다. generator.next() 를 호출해야만 코드가 실행되며, yield를 한 값을 반환하고 코드의 흐름을 멈춥니다.
코드의 흐름이 멈추고 나서 generator.next() 를 다시 호출하면 흐름이 이어서 다시 시작됩니다.
제너레이터 함수를 이해 하셨으면 이제 redux-saga를 사용하는 방법을 알보도록 하겠습니다.
index.js에서 redux-saga를 사용하겠다고 import를 시켜주고, 사가 미들웨어를 만들어주고 store를 생성 할 때 미들웨어에 추가를 시켜줍니다.
import createSagaMiddleware from 'redux-saga';// redux-saga 추가 const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만듭니다 const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)));
그리고 이 다음에 루트 사가를 실행을 시켜줍니다.
sagaMiddleware.run(rootSaga); // 사가를 실행해줍니다.
이렇게 작성하면 index.js에 추가해야되는 부분을 작성이 되었습니다.
이제 사가를 실행시킨 rootSaga를 보도록 하겠습니다.
import { combineReducers } from 'redux';
import counter, {counterSaga,counterSaga2 } from './counter';
import { all } from 'redux-saga/effects'
const rootReducer = combineReducers({ counter });
export function* rootSaga() {
yield all([counterSaga(),counterSaga2()]); // all은 배열안의 여러 사가를 동시에 실행시킨다.
}
export default rootReducer;
rootSaga를 보시면 function * rootSaga() 제너레이터 함수가 있습니다.
yield all([counterSaga(),counterSaga2()]);
부분은 counterSaga, counterSaga2 두개를 실행 시켜줍니다. 그렇단 말은 saga로 작동하는 함수가 2개가 있다는 뜻이겠죠
사가들이 존재하는 counter.js를 보도록하겠습니다.
import { call, delay, put, takeEvery, takeLatest } from 'redux-saga/effects'; import * as API from '../api/posts'; // 액션 타입 const INCREASE = 'INCREASE'; const DECREASE = 'DECREASE'; const INCREASE_ASYNC = 'INCREASE_ASYNC'; const DECREASE_ASYNC = 'DECREASE_ASYNC'; const GET_NUMBER = 'GET_NUMBER'; // 요청 시작 const GET_NUMBER_S = 'GET_NUMBER_S' // 성공 const GET_NUMBER_F = 'GET_NUMBER_F' // 실패 // 액션 생성 함수 export const increase = (ten) => ({ type: INCREASE, number : ten}); export const decrease = (one) => ({ type: DECREASE, number : one}); export const increaseAsync = (ten) => ({ type : INCREASE_ASYNC, number : ten}); export const decreaseAsync = (one) => ({type : DECREASE_ASYNC, number : one}); export const getNumber = () => ({type: GET_NUMBER}); export const getNumberS = () => ({type: GET_NUMBER_S}); export const getNumberF = () => ({type: GET_NUMBER_F}); // 제너레이터 함수 export function* counterSaga() { yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리 yield takeEvery(GET_NUMBER, numberSaga); } export function* counterSaga2() { yield takeLatest(DECREASE_ASYNC, decreaseSaga); //가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만 } function* numberSaga(){ try{ const number = yield call(API.getNumber); debugger; // call 을 사용하면 특정 함수를 호출하고, 결과물이 반환 될 때까지 기다려줄 수 있습니다. yield put({type: GET_NUMBER_S, number : number[0]}); } catch(e){ yield put({type:GET_NUMBER_F, error:true, payload: 0}); } } function* increaseSaga(ten) { yield delay(1000); // 1초를 기다립니다. if(ten.type === INCREASE_ASYNC){ yield put(increase(ten.number)); } else{ yield put(decrease(ten.number)); } } function* decreaseSaga(one) { yield delay(1000); yield put(decrease(one.number)); } //takeEvery는 특정 액션 타입에 대하여 디스패치되는 모든 액션들을 처리 //takeLatest는 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리 const initialState = 40; export default function counter(state = initialState, action) { switch (action.type) { case INCREASE: return state + action.number; case DECREASE: return state - action.number; case GET_NUMBER: return state; case GET_NUMBER_S: const a = action.number; debugger; return {...state, state: action.number}; case GET_NUMBER_F: return state; default: return state; } }
import axios from 'axios'; // 포스트 목록을 가져오는 비동기 함수 export const getNumber = async () => { //await sleep(500); // 0.5초 쉬고 //return posts; // posts 배열 const response = await axios.get(`http://localhost:4000/number`); debugger; return response.data;
};
import React, {useEffect} from 'react';
import Counter from '../components/Counter';
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease, increaseAsync, decreaseAsync, getNumber } from '../modules/counter';
function CounterContainer() {
const number = useSelector(state => state.counter);
debugger;
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increaseAsync(10));
};
const onDecrease = () => {
dispatch(decreaseAsync(1));
};
useEffect(() => {
dispatch(getNumber());
}, [dispatch]);
return (
);
}
export default CounterContainer;
import React from 'react'; function Counter({ number, onIncrease, onDecrease }) { debugger; return ( <div> <h1>{number.state}</h1> <button onClick={onIncrease}>+10</button> <button onClick={onDecrease}>-1</button> </div> ); } export default Counter;
{ "number": [ 50 ] }
코드를 작성했다면 cmd창에서 json-server를 실행 시켜줍니다.
npx json-server ./data.json --port 4000
이렇게 되면 data.json파일은 port 4000으로 가상서버거 만들어 졌습니다.
이제 실행해서 확인을 해보도록 하겠습니다.