[React다루는기술] 24. Redux Saga Middleware(비동기 작업)
in React on React 다루는 기술
Redux Saga는 Redux 애플리케이션에서 비동기 작업을 관리하기 위한 미들웨어로 제너레이터 함수를 이용한 라이브러리이다.
1. redux-saga
Redux Saga는 Redux 애플리케이션에서 비동기 작업을 관리하기 위한 미들웨어이다. 주로 네트워크 요청, 데이터 가져오기, 캐시 처리 등의 비동기 작업을 처리하는 데 사용된다. Redux Saga는 제너레이터 함수를 기반으로 동작하며, 액션들을 모니터링하고 필요한 비동기 작업을 실행한다.
비동기 작업의 분리: Redux Saga를 사용하면 애플리케이션의 비즈니스 로직과 비동기 작업을 분리할 수 있다. 이는 코드를 더 읽기 쉽고 유지보수하기 쉽게 만든다. 비동기 작업의 로직이 액션과 리듀서 사이에 분산되지 않고 Saga에서 중앙 집중적으로 관리되므로 코드가 더 깔끔하고 직관적이게 된다.
효율적인 비동기 제어: Redux Saga는 제너레이터 함수를 사용하여 비동기 흐름을 명확하게 제어할 수 있다. 비동기 작업의 세부 사항을 명시적으로 제어할 수 있으므로 복잡한 비즈니스 로직을 보다 쉽게 처리할 수 있다. 또한, 다양한 비동기 작업들을 순차적으로 또는 병렬로 처리할 수 있어서 성능을 최적화할 수 있다.
테스트 용이성: Redux Saga를 사용하면 테스트하기 쉬운 코드를 작성할 수 있다. Saga의 비동기 작업은 순수 자바스크립트 함수로 분리되어 있기 때문에 모의(mock) 함수를 사용하여 쉽게 테스트할 수 있다. 이는 코드의 품질을 높이고 버그를 줄이는 데 도움이 된다.
다양한 기능 제공: Redux Saga는 다양한 기능을 제공하여 비동기 작업을 더욱 효율적으로 처리할 수 있다. 예를 들어,
takeEvery
,takeLatest
,throttle
,delay
등의 여러 사가 이펙트를 사용하여 비동기 작업의 동작을 세밀하게 제어할 수 있다.
이러한 이유로 Redux Saga는 Redux 애플리케이션에서 비동기 작업을 처리하는 데 매우 실용적이다.
제너레이터 함수
redux-saga 에서는 ES6의 generator 함수라는 문법을 사용하기에 redux-saga 를 접하기전에 제너레이터 사용 및 작동 방식에 알아보자.
function* generatorFunction() {
let obj = yield;
console.log('# obj 1 = ', obj.type)
yield obj;
obj = yield;
console.log('# obj 2 = ', obj)
yield obj;
}
const generator = generatorFunction();
console.log(generator.next())
console.log(generator.next( { type: "A" }))
console.log(generator.next())
console.log(generator.next( { type: "B", title: "REACT" }))
(2) {value: undefined, done: false}
# obj 1 = A
(2) {value : {type: "A"}, done: false}
(2) {value: undefined, done: false}
# obj 2 =
(2) {type: "B", title: "REACT"}
(2) {value: {type: "B", title: "REACT"}, done: false}
기존에 thunk 함수로 구현했던 비동기 카운터를 리덕스 모듈 counter.js 에서 redux-saga 를 적용하는 포스팅이기에 redux-saga 를 설치해주자
$ yarn add redux-saga
1-1. Redux Saga의 counterSaga 제너레이터 함수
위의 코드는 Redux Saga에서 사용되는 제너레이터 함수인 counterSaga
이다. 이 함수는 특정 액션들에 대한 비동기 작업을 수행한다.
takeEvery
:들어오는 모든 액션에 대해 특정 작업을 처리
한다. 예를 들어, INCREASE_ASYNC 액션이 발생할 때마다 increaseSaga 함수를 호출한다.takeLatest
: 가장 마지막으로 실행된 작업만을 수행한다.즉, 동시에 여러 번 요청이 오더라도 마지막 요청에 대해서만 작업을 수행
합니다. 예를 들어, DECREASE_ASYNC 액션이 여러 번 발생할 경우, 마지막으로 발생한 액션에 대해서만 decreaseSaga 함수를 호출합니다.
takeEvery와 takeLatest 효과를 사용하여 특정 액션들에 대한 비동기 작업을 관리해서 서버로부터 데이터를 가져오거나 상태를 변경하는 등의 작업을 수행 할 수 있다.
/modules/counter.js
import { createAction, handleActions } from "redux-actions";
import { delay, put, takeEvery, takeLatest } from "redux-saga/effects";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
const INCREASE_ASYNC = "counter/INCREASE_ASYNC";
const DECREASE_ASYNC = "counter/DECREASE_ASYNC";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);
function* increaseSaga() {
yield delay(1000); //1초를 기다린다.
yield put(increase()); //특정 액션을 디스패치한다.
}
function* decreaseSaga() {
yield delay(1000); //1초를 기다린다.
yield put(decrease()); //특정 액션을 디스패치한다.
}
export function* counterSaga() {
//takeEvery 는 들어오는 모든 액션에 대해 특정 작업을 처리
yield takeEvery(INCREASE_ASYNC, increaseSaga);
//takeLatest 는 가장 마지막으로 실행된 작업만 수행
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
const initialState = 0;
const counter = handleActions(
{
[INCREASE]: (state) => state + 1,
[DECREASE]: (state) => state - 1,
},
initialState
);
export default counter;
아래와 같이 화면에서 counter 컴포넌트를 “+”를 빠르게 연속으로 클릭하면 takeEvery
이기에 비동기지만 클릭수 만큼 액션과 디스패치는 호출된다. “-“를 빠르게 연속으로 클릭하면 takeLatest
이기에 비동기로 액션이 중첩되게 되면 마지막 요청에 대해서만 디스패치는 호출된다.
1-2. Redux Saga의 rootSaga
rootSaga: Redux Saga에서 사용되는 여러 개의 사가를 합치는 역할을 한다. all 함수를 사용하여 여러 사가를 결합하여 하나의 루트 사가로 생성한다. 이 루트 사가는 애플리케이션의 비동기 작업 흐름을 관리하게 된다.
/modules/index.js
루트 리듀서를 만들었던 것처럼 루트 사가를 만들어 줘야 한다.
import { combineReducers } from "redux";
import { all } from "redux-saga/effects";
import counter, { counterSaga } from "./counter";
import sample from "./sample";
import loading from "./loading";
const rootReducer = combineReducers({
counter,
sample,
loading,
});
export function* rootSaga() {
//all 함수는 여러 사가를 합쳐주는 역할 한다.
yield all([counterSaga()]);
}
export default rootReducer;
1-3. Redux Saga의 createSagaMiddleware
sagaMiddleware는 Redux Saga를 사용하기 위해 추가된 미들웨어
이다. 이렇게 함으로써 Redux와 Redux Saga를 함께 사용하여 비동기 작업을 처리할 수 있다.
index.js
//...
import rootReducer, { rootSaga } from "./modules";
import createSagaMiddleware from "redux-saga";
//...
const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(logger, thunk, sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
//...
1-4. Redux Saga로 API요청
제너레이터 함수를 사용하는 이유는 Redux Saga에서 비동기 작업을 처리하기 위해서이다. Redux Saga에서는 비동기 작업을 수행하는 동안 애플리케이션의 상태를 블로킹하지 않으면서도 비동기 코드의 순서를 제어할 수 있어야 한다. 그래서 제너레이터 함수를 사용한다.
제너레이터 함수는 함수 실행을 중간에 멈추고 다시 시작할 수 있는 기능을 제공한다. 이를 통해 비동기 작업을 수행하는 동안 중간에 멈추고 다른 작업을 수행하거나, 작업이 완료될 때까지 기다릴 수 있다. 이러한 특징을 활용하여 Redux Saga에서는 yield 키워드를 사용하여 비동기 작업을 수행하고, 해당 작업이 완료될 때까지 기다리는 방식으로 코드를 작성한다.
Redux Saga 에서 put과 call 함수는
put: put 함수는 Redux 액션을 디스패치하는 데 사용된다. 즉, 액션을 스토어로 보낸다. 이 함수는 Redux의 dispatch와 유사하게 동작하지만, 제너레이터 함수 내에서 사용된다. 예를 들어, put({ type: ‘INCREMENT’ })와 같이 사용됩니다.
call: call 함수는 동기 함수나 Promise를 호출할 때 사용됩니다. Redux Saga는 이 함수를 사용하여 비동기 작업을 호출하고, 해당 작업이 완료될 때까지 제너레이터를 일시 중단한다. call 함수는 첫 번째 매개변수로 호출할 함수를 받고, 나머지 매개변수는 해당 함수에 전달된다.
아래 코드로 설명을 하면
call 함수는 이를 호출하고, Promise가 완료될 때까지 제너레이터를 일시 중단합니다. 그 후에는 call 요청이 완료되면 해당 결과를 반환한다.
- yield 키워드 : 호출된 함수의 결과가 반환될 때까지 제너레이터 함수를 일시 중단
- call 함수 : 비동기 작업이 완료되면 그 결과를 반환
const post = yield call(api.getPost, action.payload);
/modules/sample.js
import { createAction, handleActions } from "redux-actions";
import { call, put, takeLatest } from "redux-saga/effects";
import * as api from "../lib/api";
import { startLoading, finishLoading } from "./loading";
const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_POST_FAILURE = "sample/GET_POST_FAILURE";
const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
const GET_USERS_FAILURE = "sample/GET_USERS_FAILURE";
export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);
console.log("# sample.js :: ", getPost);
function* getPostSaga(action) {
yield put(startLoading(GET_POST)); //로딩 시작
try {
//call 함수는 이를 호출하고, Promise가 완료될 때까지 제너레이터를 일시 중단합니다. 그 후에는 call 요청이 완료되면 해당 결과를 반환한다.
//- yield 키워드는 호출된 함수의 결과가 반환될 때까지 제너레이터 함수를 일시 중단
//- call 함수는 비동기 작업이 완료되면 그 결과를 반환
const post = yield call(api.getPost, action.payload);
yield put({
type: GET_POST_SUCCESS,
payload: post.data,
});
} catch (e) {
yield put({
type: GET_POST_FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoading(GET_POST));
}
function* getUsersSaga() {
yield put(startLoading(GET_USERS));
try {
const users = yield call(api.getUsers);
yield put({
type: GET_USERS_SUCCESS,
payload: users.data,
});
} catch (e) {
yield put({
type: GET_USERS_FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoading(GET_USERS));
}
export function* sampleSaga() {
yield takeLatest(GET_POST, getPostSaga);
yield takeLatest(GET_USERS, getUsersSaga);
}
//초기 상태를 선언
//요청의 로딩 중 상태는 loading 객체에서 관리
const initialState = {
post: null,
users: null,
};
const sample = handleActions(
{
//[GET_POST_SUCCESS]: (state, action) => console.log("***", action.payload),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
},
initialState
);
export default sample;
비동기 작업을 처리할 경우 redux-thunk, redux-saga 등 미들웨어를 사용하는 것은 좋은 방법이다.