JavaScript 제너레이터 함수
제너레이터 함수를 사용하면 함수에서 값을 순차적으로 반환할 수 있습니다. 심지어 함수의 흐름을 도중에 멈춰 놓았다가 다시 이어서 진행시킬 수도 있습니다.
function* generatorFunction() {
console.log("hello");
yield 1;
console.log("generator function");
yield 2;
console.log("function*");
yield 3;
yield 4;
}
const generator = generatorFunction();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
제너레이터가 처음 만들어지면 함수의 흐름은 멈춰 있는 상태입니다. next()가 호출되면 다음 yield가 있는 곳까지 호출하고 다시 함수가 멈춥니다. 제너레이터 함수를 사용하면 함수를 도중에 멈출 수도있고, 순차적으로 여러 값을 반환시킬 수도 있습니다. next 함수에 파라미터를 넣으면 제너레이터 함수에서 yield를 사용하여 해당 값을 조회할 수도 있습니다.
function* sumGenerator() {
console.log("sumGenerator가 만들어졌습니다.");
let a = yield;
let b = yield;
yield a + b;
}
const sum = sumGenerator();
console.log(sum.next());
console.log(sum.next(1));
console.log(sum.next(2));
console.log(sum.next());
또한 redux-saga도 이처럼 동작합니다. 제너레이터 함수 문법을 기반으로 비동기 작업을 관리해줍니다. 좀 더 이해하기 쉽게 설명하자면, redux-saga는 우리가 디스패티하는 액션을 모니터링해서 그에 따라 필요한 작업을 따로 수행할 수 있는 미들웨어입니다.
다음을 봅시다.
function* watchGenerator() {
console.log("모니터링 중");
let prevAction = null;
while (true) {
const action = yield;
console.log("이전 버전 : ", prevAction);
prevAction = action;
if (action.type === "HELLO") {
console.log("안녕하세요");
}
}
}
const watch = watchGenerator();
console.log(watch.next());
console.log(watch.next({ type: "TEST" }));
console.log(watch.next({ type: "HELLO" }));
Redux-saga 미들웨어 사용
< Counter >
// src/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);
// 마우스 클릭 이벤트가 payload안에 들어가지 않도록
// () => undefined를 두 번쨰 파라미터로 넣어 줍니다.
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 = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({
number: state.number + 1,
}),
[DECREASE]: (state, action) => ({
number: state.number - 1,
}),
},
initialState
);
export default counter;
// src/containers/CounterContainer.js
import React from "react";
import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increaseAsync, decreaseAsync } from "../modules/counter";
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<div>
<Counter
number={number}
onIncrease={increaseAsync}
onDecrease={decreaseAsync}
/>
</div>
);
};
export default connect(({ counter }) => ({ number: counter.number }), {
increaseAsync,
decreaseAsync,
})(CounterContainer);
// src/modules/index.js
import { combineReducers } from "redux";
import counter, { counterSaga } from "./counter";
import sample, { sampleSaga } from "./sample";
import loading from "./loading";
import { all } from "redux-saga/effects";
const rootReducer = combineReducers({
counter,
sample,
loading,
});
export function* rootSaga() {
// all 함수는 여러 사가를 합쳐 주는 역할을 합니다.
yield all([counterSaga(), sampleSaga()]);
}
export default rootReducer;
우선 modules의 counter.js에서는 INCREMENT_ASYNC, DECREMENT_ASYNC라는 액션 타입을 선언했습니다. 해당 액션에 대한 액션 생성 함수도 만들고, 이어서 제너레이터 함수를 만듭니다. 이 increaseSaga, decreaseSaga를 우리는 사가(Saga)라고 부릅니다.
그리고 우리는 CounterContainer에서 액션 생성 함수를 불러와서 이를 사용해 주어야 합니다.
마지막으로 우리가 루트 리듀서를 만들었던 것처럼 루트 사가를 만들어 주어야 합니다. 추후 다른 리듀서에서도 사가를 만들어 등록할 것이기 때문입니다. ( sampleSaga는 뒤에서 볼 겁니다. )
< Sample >
대충 정리해 본 흐름도인데 그냥 참고만 하자
// src/modules/sample.js
import { createAction, handleActions } from "redux-actions";
import { call, put, takeLatest } from "redux-saga/effects";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";
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";
/**
* redux-saga Version
*/
export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);
function* getPostSaga(action) {
yield put(startLoading(GET_POST));
// 파라미터로 action을 받아오면 액션의 정보를 받아올 수 있습니다.
try {
// call을 사용하면 Promise를 반환하는 함수를 호출하고, 기다릴 수 있습니다.
// 첫 번쨰 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣을 인수입니다.
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* getUserSaga() {
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, getUserSaga);
}
// 초기상태 선언
// 요청 로딩 중 상태는 loading이라는 객체에서 관리
const initialState = {
post: null,
users: null,
};
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
},
initialState
);
export default sample;
// src/containers/SampleContainer.js
import React, { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample";
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers,
}) => {
useEffect(() => {
const fn = async () => {
await Promise.all([getPost(1), getUsers()]);
};
fn();
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
export default connect(
({ sample, loading }) => ({
post: sample.post,
users: sample.users,
loadingPost: loading["sample/GET_POST"],
loadingUsers: loading["sample/GET_USERS"],
}),
{ getPost, getUsers }
)(SampleContainer);
// src/modules/loading.js
import { createAction, handleActions } from "redux-actions";
const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";
/**
* 액션을 위한 액션 타입을 payload로 설정합니다.
*/
export const startLoading = createAction(
START_LOADING,
(requestType) => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
(requestType) => requestType
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, { payload }) => ({
...state,
[payload]: true,
}),
[FINISH_LOADING]: (state, { payload }) => ({
...state,
[payload]: false,
}),
},
initialState
);
export default loading;
/*
{
type: 'loading/START_LOADING',
payload: 'sample/GET_POST'
}
*/
// 만약 위 액션이 디스패치되면 loading 리듀서가 관리하고 있는 상태에서
// 'sample/GET_POST'의 값을 true로 만들어 줍니다.
// 만약 기존 상태에서 'sample/GET_POST'가 없다면 새로 값을 설정해 줍니다.
/*
{
type: 'loading/FINISH_LOADING',
payload: 'sample/GET_POST'
}
*/
// 기존에 true로 했던 값을 다시 false로 설정해줍니다.
여기서 GET_POST액션의 경우에는 API요청을 할 때 어떤 id로 조회할지 정해주어야 합니다. redux-saga를 사용할 때는 id처럼 요청에 필요한 값을 액션의 payload로 넣어 주어야 합니다.예를 들어 지금 상황이라면 다음과 같은 액션이 디스패치 됩니다.
{
type: 'sample/GET_POST',
payload: 1
}
그러면 이 액션을 처리하기 위한 사가를 작성할 때 payload값을 API를 호출하는 함수의 인자로 넣어 주어야 합니다.
API를 호출해야 하는 상황에는 사가 내부에서 직접 호출하지 않고 call함수를 사용합니다. call함수의 경우, 첫 번째 인수는 호출하고 싶은 함수이고, 그 뒤에 오는 인수들은 해당 함수에 넣어주고 싶은 인수입니다. 지금 getPostSaga의 경우 id를 의미하는 action.payload가 인수가 됩니다.
사가를 작성하여 구현하는 과정에서 처음 redux-thunk를 사용했을 때처럼 반복되는 코드가 나왔습니다. 충분히 간소화할 수 있는 코드들입니다. 지금은 일단 기능을 구현하고, 나중에 리팩토링하겠습니다.
리팩토링
// src/lib/createRequestSaga.js
import { call, put } from "redux-saga/effects";
import { startLoading, finishLoading } from "../modules/loading";
export default function createRequestSaga(type, request) {
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return function* (action) {
yield put(startLoading(type));
try {
const response = yield call(request, action.payload);
yield put({
type: SUCCESS,
payload: response.data,
});
} catch (e) {
yield put({
type: FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoading(type));
};
}
// src/modules/sample.js
import { createAction, handleActions } from "redux-actions";
import { call, put, takeLatest } from "redux-saga/effects";
import * as api from "../lib/api";
// import createRequestThunk from "../lib/createRequestThunk";
import createRequestSaga from "../lib/createRequestSaga";
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";
/**
* redux-saga Version
*/
export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);
const getPostSaga = createRequestSaga(GET_POST, api.getPost);
const getUserSaga = createRequestSaga(GET_USERS, api.getUsers);
export function* sampleSaga() {
yield takeLatest(GET_POST, getPostSaga);
yield takeLatest(GET_USERS, getUserSaga);
}
// 초기상태 선언
// 요청 로딩 중 상태는 loading이라는 객체에서 관리
const initialState = {
post: null,
users: null,
};
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
},
initialState
);
export default sample;
기존의 코드를 이와같이 짧게 리팩토링 할 수 있습니다.
알아 두면 유용한 기능들
// src/modules/counter.js
import { createAction, handleActions } from "redux-actions";
import { delay, put, takeEvery, takeLatest, select } 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);
// 마우스 클릭 이벤트가 payload안에 들어가지 않도록
// () => undefined를 두 번쨰 파라미터로 넣어 줍니다.
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);
function* increaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(increase()); // 특정 액션을 디스패치 합니다.
const number = yield select(({ counter }) => counter.number);
console.log(`현재 값은 ${number}입니다.`);
}
function* decreaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(decrease()); // 특정 액션을 디스패치 합니다.
}
export function* counterSaga() {
// takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리해 줍니다.
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// takeLatest는 기존 진행 중이던 작업이 있다면 취소 처리하고
// 가장 마지막으로 실행된 작업만 수행합니다.
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
// export const increaseAsync = () => (dispatch) => {
// setTimeout(() => {
// dispatch(increase());
// }, 1000);
// };
// export const decreaseAsync = () => (dispatch) => {
// setTimeout(() => {
// dispatch(decrease());
// }, 1000);
// };
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({
number: state.number + 1,
}),
[DECREASE]: (state, action) => ({
number: state.number - 1,
}),
},
initialState
);
export default counter;
위와같이 redux-saga는 사가 내부에서 현재 상태를 조회하는 select입니다.
다음으로 사가가 실행되는 주기를 제한하는 방법입니다. takeEvery대신 throttle이라는 함수를 사용하면 사가가 n초에 단 한번만 호출되도록 설정할 수 있습니다.
예를 들어 counterSaga를 다음과 같이 수정하면 increaseSaga는 3초에 단 한번만 호출됩니다.
// src/modules/counter.js
import { createAction, handleActions } from "redux-actions";
import {
delay,
put,
takeEvery,
takeLatest,
select,
throttle,
} 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);
// 마우스 클릭 이벤트가 payload안에 들어가지 않도록
// () => undefined를 두 번쨰 파라미터로 넣어 줍니다.
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);
function* increaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(increase()); // 특정 액션을 디스패치 합니다.
const number = yield select(({ counter }) => counter.number);
console.log(`현재 값은 ${number}입니다.`);
}
function* decreaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(decrease()); // 특정 액션을 디스패치 합니다.
}
export function* counterSaga() {
// takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리해 줍니다.
yield throttle(3000, INCREASE_ASYNC, increaseSaga);
// takeLatest는 기존 진행 중이던 작업이 있다면 취소 처리하고
// 가장 마지막으로 실행된 작업만 수행합니다.
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
// export const increaseAsync = () => (dispatch) => {
// setTimeout(() => {
// dispatch(increase());
// }, 1000);
// };
// export const decreaseAsync = () => (dispatch) => {
// setTimeout(() => {
// dispatch(decrease());
// }, 1000);
// };
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({
number: state.number + 1,
}),
[DECREASE]: (state, action) => ({
number: state.number - 1,
}),
},
initialState
);
export default counter;
'React > ReactJs' 카테고리의 다른 글
React - ServerSideRendring (0) | 2022.02.22 |
---|---|
React - React.lazy & Suspense & Loadable Components (0) | 2022.02.22 |
React - Redux MiddleWare >> default + redux-thunk (0) | 2022.02.22 |
React - redux ( util_Version ) (0) | 2022.02.21 |
React - redux (0) | 2022.02.21 |