티스토리 뷰
"axios": "^0.21.1",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다. 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.
클로저 개념 모르면 이해못해. 기억안나면 클로저 공부
ex)
function hi(a,b){
console.log(a,b)
return (pa)=>console.log("return",pa);
}
post=hi(1,2)
1 2
post("a")
return a
제너레이터
ex1
function* generatorFunction(){
console.log('안녕하세요');
yield 1;
console.log('제너레이터 함수');
yield 2;
console.log('function');
yield 3;
return 4;
}
const generator=generatorFunction();
generator.next()
VM471:2 안녕하세요
{value: 1, done: false}
generator.next()
VM471:4 제너레이터 함수
{value: 2, done: false}
generator.next()
VM471:6 function
{value: 3, done: false}
generator.next()
{value: 4, done: true}
generator.next()
{value: undefined, done: true}
ex2
function* sumGenerator(){
console.log('sumGenerator가 만들어졌습니다.');
let a=yield;
let b=yield;
yield a+b;
}
const sum=sumGenerator();
sum.next();
VM213:2 sumGenerator가 만들어졌습니다.
{value: undefined, done: false}
sum.next(1);
{value: undefined, done: false}
sum.next(2);
{value: 3, done: false}
sum.next();
{value: undefined, done: true}
ex3
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();
watch.next();
VM2434:2 모니터링 중...
{value: undefined, done: false}
watch.next({type:'TEST'});
VM2434:6 이전 액션: null
{value: undefined, done: false}
watch.next({type:'HELLO'});
VM2434:6 이전 액션: {type: "TEST"}
VM2434:9 안녕하세요!
{value: undefined, done: false}
이렇게 만들수있어
미들웨어 속성을 사용하여 네트워크 요청과 같은 비동기 작업을 관리하면 매우 유용
const loggerMiddleware = (store) => (next) => (action) => {
//미들웨어 기본 구조
console.group(action && action.type); // 액션 타입으로 log를 그룹화함
console.log("이전 상태", store.getState());
console.log("액션", action);
next(action); // 다음 미들웨어 혹은 리듀서에게 전달
console.log("다음 상태", store.getState()); // 업데이트된 상태
console.groupEnd(); // 그룹 끝
};
export default loggerMiddleware;
yarn add redux-logger
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
이걸로 하면 돼
비동기 작업을 처리할 때 도움을 주는 미들웨어
redux-thunk : 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어입니다. 객체가 아닌 함수 형태의 액션을 디스패치할 수 잇게 해 줍니다.
redux-sage : redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리입니다. 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성하여 비동기 작업을 처리할 수 있게 해줍니다.
redux-thunk
yarn-add redux-thunk
thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다.
ex)
const sampleThunk = () => (dispatch, getState) => {
//현재 상태를 참조할 수 있고,
//새 액션을 디스패치할 수도 있습니다.
};
처음 디스패치되는 액션은 함수형태이고, 두번째 액션은 객체 형태이다.
counter.js
import { createAction, handleActions } from "redux-actions";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
//1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const initialState = 0; // 상태는 꼭 객체일 필요가 없습니다. 숫자도 작동해요.
const counter = handleActions(
{
[INCREASE]: (state) => state + 1,
[DECREASE]: (state) => state - 1,
},
initialState
);
export default counter;
sample.js
import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";
// 액션 타입을 선언합니다.
// 한 요청당 세 개를 만들어야 합니다.
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";
// thunk 함수를 생성합니다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치합니다.
export const getPost = (id) => async (dispatch) => {
dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
try {
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data,
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true,
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
export const getUsers = () => async (dispatch) => {
dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
try {
const response = await api.getUsers();
dispatch({
type: GET_USERS_SUCCESS,
payload: response.data,
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true,
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
}
};
// 초기 상태를 선언합니다.
// 요청의 로딩중 상태는 loading 이라는 객체에서 관리합니다.
const initialState = {
loading: {
GET_POST: false,
GET_USERS: false,
},
post: null,
users: null,
};
const sample = handleActions(
{
[GET_POST]: (state) => ({
...state,
loading: {
...state.loading,
GET_POST: true, // 요청 시작
},
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false, // 요청 완료
},
post: action.payload,
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false, // 요청 완료
},
}),
[GET_USERS]: (state) => ({
...state,
loading: {
...state.loading,
GET_USERS: true, // 요청 시작
},
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false, // 요청 완료
},
users: action.payload,
}),
[GET_USERS_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false, // 요청 완료
},
}),
},
initialState
);
export default sample;
리팩토링
API를 요청해야 할 때마다 긴 think 함수를 작성하는 것과 로딩 상태를 리듀서에서 관리하는 작업은 귀찮을 뿐 아니라 코드도 길어지게 만든다. 그러므로 반복되는 로직을 따로 분리하여 코드의 양을 줄이자
아래코드에서 dispatch({type})은 저자가 실수로 안지운듯?? 실수가 아니지 생각을 다시해봐
createRequestThunk.js
import { startLoading, finishLoading } from "../modules/loading";
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의합니다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = ` ${type}_FAILURE`;
return (params) => async (dispatch) => {
dispatch({ type }); // 시작됨
dispatch(startLoading(type));
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data,
}); // 성공
dispatch(finishLoading(type));
} catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true,
}); // 에러 발생
dispatch(startLoading(type));
throw e;
}
};
}
// 사용법: createRequestThunk('GET_USERS', api.getUsers);
loading.js
import { createAction, handleActions } from "redux-actions";
const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";
// 요청을 위한 액션 타입을 PAYLOAD로 설정합니다(예: "SAMPLE/get_post")
export const startLoading = createAction(
START_LOADING,
(requestType) => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
(requestType) => requestType
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true,
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false,
}),
},
initialState
);
export default loading;
이러면 container에 useEffect async로
useEffect(() => {
// useEffect에 피라미터로 넣는 함수는 async로 할 수 없기 때문에
// 그 내부에서 async 함수를 선언하고 호출해 줍니다.
const fn = async () => {
try {
await getPost(1);
await getUsers(1);
} catch (e) {
console.log(e); // 에러 조회
}
};
fn();
// getPost(1);
// getUsers(1);
}, [getPost, getUsers]);
sample.js
import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";
// 액션 타입을 선언합니다.
// 한 요청당 세 개를 만들어야 합니다.
const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
// thunk 함수를 생성합니다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치합니다.
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
// 초기 상태를 선언합니다.
// 요청의 로딩중 상태는 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;
redux-saga
counter.js
/ 마우스 클릭 이벤트가 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);
}
modules/index.js
export function* rootSaga() {
// all 함수는 여러 사가를 합쳐 주는 역할을 합니다.
yield all([counterSaga()]);
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { applyMiddleware, createStore } from "redux";
import rootReducer, { rootSaga } from "./modules/index";
import { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";
import createSagaMiddleware from "redux-saga";
import { composeWithDevTools } from "../node_modules/redux-devtools-extension/index";
const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(logger, ReduxThunk, sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
API를 호출해야 하는 경우에는 사가 내부에서 직접 호출하지 않고 call 함수를 사용한다. call은 첫번째 인수는 호출하고 싶은 함수이고 그 뒤에 오는 인수들은 해당 함수에 넣어주고 싶은 인수이다.
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)); // 로딩 끝
};
}
sample.js
import { createAction, handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";
import { call, put, takeLatest } from "redux-saga/effects";
import { startLoading, finishLoading } from "./loading";
import createRequestSaga from "../lib/createRequestSaga";
// 액션 타입을 선언합니다.
// 한 요청당 세 개를 만들어야 합니다.
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);
const getPostSaga = createRequestSaga(GET_POST, api.getPost);
// function* getPostSaga(action) {
// yield put(startLoading(GET_POST)); // 로딩 시작
// // 피라미터로 actionn을 받아 오면 액션의 정보를 조회할 수 있습니다.
// try {
// // call을 사용하면 Promise를 반환하는 함수를 호출하고, 기다릴 수 있습니다.
// // 첫 번째 피라미터는 함수, 나머지 피라미터는 해당 함수에 넣을 인수입니다.
// const post = yield call(api.getPost, action.payload); // api.getPost(action.payload를 의미)
// yield put({
// type: GET_POST_SUCCESS,
// payload: post.data,
// });
// } catch (e) {
// // try/catch 문을 사용하여 에러도 잡을 수 있습니다.
// yield put({
// type: GET_POST_FAILURE,
// payload: e,
// error: true,
// });
// }
// yield put(finishLoading(GET_POST)); // 로딩 완료
// }
const getUsersSaga = createRequestSaga(GET_USERS, api.getUsers);
// 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 = {
// loading: {
// GET_POST: false,
// GET_USERS: false,
// },
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;
알아두면 유용한 기능들
만약 사가 내부에서 현재 상태를 참조해야 하는 상황이 생기면 select를 사용하면 된다.
function* increaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(increase()); // 특정 액션을 디스패치합니다.
const number = yield select((state) => state.counter); // state는 스토어 상태를 의미함
console.log(`현재 값은 ${number}입니다.`);
}
사가가 실행되는 주기를 제한하는 방법. takeEvery 대신 throttle이라는 함수를 사용하면 사가가 n초에 단 한번만 호출되도록 설정할 수 있다.
export function* counterSaga() {
// takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리해 줍니다.
// yield takeEvery(INCREASE_ASYNC, increaseSaga);
//첫번째 피라미터: n초 * 1000
yield throttle(3000, INCREASE_ASYNC, increaseSaga);
// takeLatest는 기존에 진행 중이던 작업이 있다면 취소 처리하고
// 가장 마지막으로 실행된 작업만 수행합니다.
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
다른 여러 기능
'책 > 리다기' 카테고리의 다른 글
리다기 정리11(서버 사이드 렌더링) (0) | 2021.01.29 |
---|---|
리다기 정리10(코드 스플리팅) (0) | 2021.01.28 |
리다기 정리8(리덕스) (0) | 2021.01.25 |
리다기 정리7(context) (0) | 2021.01.24 |
리다기 정리6(뉴스뷰어 만들기) (0) | 2021.01.22 |