Saga
는 제너레이터 문법을 사용해 특정Action
이 발생하면 비동기적으로Dispatch
기본적으로 Redux
의 Action Dispatch
는 동기적으로 실행된다.
그래서 여러 Diapatch
가 실행되는 경우에는 컴포넌트 파일에서도 Dispatch
로직을 여러번 작성해야하는 단점이 존재하는데 이것을 해결하기 위해 Redux_Saga
라는 미들웨어를 사용한다.
Redux_Saga
는 앞선 포스팅에서 소개한 제너레이터 문법을 사용하여 비동기적으로 Dispatch
를 수행한다.
즉 Redux_Saga
의 내부 메소드를 활용하면 무한의 개념과 이벤트리스너의 역활을 수행할 수 있다.
Redux_Saga
에서는 다음과 같은 내부 메서드를 제공한다.
Effect | 용도 |
---|---|
all | 함수 배열을 인자로 받은 뒤 동시에 전부 등록 |
take | 첫번째 인자로 받은 Action 이 실행되기까지 기다린다, 실행이 완료되면 두번째 인자로 받은 함수가 실행 |
put | Action 을 Dispatch |
delay | 서버를 구현하기전 비동기적 효과 |
fork | 함수를 첫번째 인자로 받은 뒤 비동기 호출, 나머지 인자들은 첫번째 인자 함수에 매개변수로 전달될 값 |
call | 함수를 첫번째 인자로 받은 뒤 동기 호출, 나머지 인자들은 첫번째 인자 함수에 매개변수로 전달될 값 |
Saga Effect
의 fork
는 비동기로 함수를 호출하기때문에 결과값을 기다리지 않고 다음 코드를 실행한다.
하지만 call
은 함수를 동기적으로 실행하기 때문에 결과값이 전달될때까지 기다린다.
// fork
const result = yield fork(logInAPI);
axios.post('/api/login')
yield put({
type: 'LOG_IN_SUCCESS',
data: result.data
});
// call
const result = yield call(logInAPI);
axios.post('/api/login')
.then((result) => {
yield put({
type: 'LOG_IN_SUCCESS',
data: result.data
});
});
제너레이터 문법의 yeild
를 사용해 이벤트리스너를 구현하면 그 기능은 한번밖에 사용하지 못하는 일회용이다.
이러한 단점은 while
의 무한반복문으로 해결할 수 있지만 직관적이지 않다.
function* watchLogIn() {
while (true) {
yield take('LOG_IN_REQUEST', logIn);
}
}
그래서 takeEvery
, takeLatest
, takeLeading
, throttle
...등과 같은 내부 메서드를 사용해 위와 같은 문제를 해결한다.
takeEvery
를 사용하면 위 코드의 while(true)
문을 대체할 수 있다.function* watchLogIn() {
yield takeEvery('LOG_IN_REQUEST', logIn);
}
takeLatest
는 마우스 이벤트의 중복 발생을 방지하게 위해 마지막 이벤트만 실행한다.function* watchLogIn() {
yield takeLatest('LOG_IN_REQUEST', logIn);
}
takeLatest
는 요청을 취소하는 것이 아닌 응답을 취소하는 것을 주의
takeLeading
은 takeLatest
와 다르게 마우스 이벤트의 중복 발생시 첫 이벤트만 실행한다.function* watchLogIn() {
yield takeLeading('LOG_IN_REQUEST', logIn);
}
throttle
은 요청 제한 시간을 지정해 해당 시간동안 단 한번의 서버 요청만 호출한다.function* watchLogIn() {
yield throttle('LOG_IN_REQUEST', logIn, 10000);
}
프로젝트에 Redux_Saga
를 적용하는 방법은 다음과 같다.
Saga
를 설치한다.npm i redux-saga
configureStore.js
에 saga middleware
를 추가// store/configureStore.js
import { createWrapper } from 'next-redux-wrapper';
import { applyMiddleware, compose, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga'; // redux-saga불러오기
import reducer from '../reducers/index';
import rootSaga from '../sagas'; // saga파일 불러오기
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware(); // redux-saga변수에 할당
const middlewares = [sagaMiddleware]; // redux-saga middleware에 추가
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares))
const store = createStore(reducer, enhancer);
store.sagaTask = sagaMiddleware.run(rootSaga); // store에 추가
return store;
};
const wrapper = createWrapper(configureStore, {
debug: process.env.NODE_ENV === 'development',
});
export default wrapper;
Saga
모듈 생성// sagas/index.js
import { all, fork, take, call, put } from 'redux-saga/effects'; // effect앞에는 yield사용
import axios from 'axios';
function logInAPI(data) {
return axios.post('/api/login', data); // 서버에 로그인 요청을 보낸다.
}
function* logIn(action) { // 요청결과에 따라 try ~ catch문작성
try {
// fork는 비동기함수 호출, call은 동기함수 호출
const result = yield call(logInAPI, action.data); // logInAPI함수의 요청결과를 변수에 저장
yield put({
type: 'LOG_IN_SUCCESS',
data: result.data // 서버요청 성공의 실제 데이터
})
} catch(err) {
yield put({ // put은 action객체를 dispatch
type: 'LOG_IN_FAILURE',
data: err.response.data // 서버요청 실패의 실제 데이터
})
}
}
function* watchLogIn() {
// LOG_IN action이 실행될때까지 기다린다.
// LOG_IN action이 실행되면 logIn함수를 실행한다.
// thunk와 다르게 비동기 action creator 직접실행하는 것이 아닌 이벤트리스너와 같은 역활
yield take('LOG_IN_REQUEST', logIn);
}
function* watchLogOut() {
yield take('LOG_OUT_REQUEST');
}
function* watchAddPost() {
yield take('ADD_POST_REQUEST');
}
export default function* rootSaga() {
yield all([ // all은 배열안에 제너레이터 함수를 동시에 실행
fork(watchLogIn), // fork는 제너레이터 함수룰 실행
fork(watchLogOut),
fork(watchAddPost),
]);
}
앞선 포스팅에서 소개한 Reducer
와 마찬가지로 Saga
또한 코드의 양이 길어진다는 단점이 존재한다.
그래서 Saga
의 Action
코드를 가독성을 위해 Reducer
와 동일한 방식으로 분리한다.
다만 Reducer
는 combineReducers
로 파일들을 병합했지만 Saga
는 위 과정을 생략하여 좀 더 간편하게 병합이 가능하다.
// sagas/index.js
import { all, fork } from 'redux-saga/effects';
import userSaga from './user';
import postSaga from './post';
export default function* rootSaga() {
yield all([
fork(userSaga),
fork(postSaga),
]);
}
// sagas/post.js
import { all, fork, delay, put, takeLatest } from 'redux-saga/effects';
import {
ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE,
} from '../reducers/post';
function* addPost(action) {
try {
yield delay(1000);
yield put({
type: ADD_POST_SUCCESS,
data: action.data,
})
} catch(err) {
yield put({
type: ADD_POST_FAILURE,
data: err.response.data
})
}
}
function* watchAddPost() {
yield takeLatest(ADD_POST_REQUEST, addPost, 2000);
}
export default function* postSaga() {
yield all([
fork(watchAddPost),
]);
}
// sagas/user.js
import { all, fork, delay, put, takeLatest } from 'redux-saga/effects';
import {
LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_IN_FAILURE,
} from '../reducers/user';
function* logIn(action) {
try {
yield delay(1000);
yield put({
type: LOG_IN_SUCCESS,
data: action.data,
})
} catch(err) {
yield put({
type: LOG_IN_FAILURE,
error: err.response.data
})
}
}
function* watchLogIn() {
yield takeLatest(LOG_IN_REQUEST, logIn);
}
export default function* userSaga() {
yield all([
fork(watchLogIn),
]);
}