React/NodeBird(ZeroCho)

redux-thunk 이해하기

느리지만 꾸준하게 2021. 10. 28. 20:28

NodeBird 클론코딩을 진행하였는데 antd을 이용해서 sns 화면을 만들어주고 Redux도 연동하여서 더미데이터와 게시글 댓글 이미지도 구현하고 해시태그 링크도 만들어 주었다. (정리는 차후에 하자...)

 

이제는 Redux-saga를 연동해주면서 프로젝트를 진행해줄 것인데, redux-thunk 부터 이해를 하자.(redux-thunk말고 redux-saga는 나중에 알아보자.)

 

redux-thunk는 redux의 middleware로써 (여기1여기2를 보고 참고하자.) redux-thunk는 redux가 비동기 dispatch할 수 있도록 도와주는 역할을 한다. 

 

 

thunk는 프로그래밍 용어이다. 지연된 함수를 뜻한다. thunk 공식문서인 여기를 한 번 보자.

아래 코드를 보자. increment는 action creator이고 increment 밑에 부분인 return 부분이 action이고 즉 action creator함수인 것이다.

 

요것을 비동기로 쓸 수 있게 해준다. increment Async() 이거를 호출하기 전까지는 return 부분이 실행이 안된다.

글고 incrementAsync()는 다시 함수를 리턴하는데 그 함수도 dispatch가 바로 되는 것이 아니라 나중에 함수를 호출 할 때 dispatch가 된다. 즉 위에는 동기 action creator이고 아래는 비동기 action creator이다.

 

원래 redux에서는 아래 부분의 함수처럼 하는게 실행이 안되는데 redux-thunk를 쓰면 아래처럼 하는 것이 실행이 된다.

장점이 뭐냐 dispatch를 여러번 할 수 있다. 하나의 액션에서 dispatch를 여러번 하는 것이다. 즉 하나의 비동기 액션안에 여러개의 동기액션을 넣을 수 있는데 예를들어 axios 요청을 보낼 때 load post request 여기서 request action을 dispatch하고 성공했을 때 load post success 이 action을 dispatch하고 두 번 이상을 할 수 있다.

그리고 혹시나 실패했을 때는 load post failure action을 또 dispatch하면 된다.

// Motivation
// Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.

// An action creator that returns a function to perform asynchronous dispatch:

const INCREMENT_COUNTER = 'INCREMENT_COUNTER'

function increment() {
  return {
    type: INCREMENT_COUNTER
  }
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment())
    }, 1000)
  }
}

즉 비동기 액션 하나에 동기액션 여러개를 dispatch할 수 있기 때문에 원래 redux기능에서 기능이 확장이 된것이다.

redux-thunk를 받아보자. 그리고 직접 custom하게 middleware을 만드는 것도 알아보자. 

npm i redux-thunk

프로젝트 진행중인 폴더에서 진행해주자.

// 아래와 같이 thunkMiddleware를 넣어주자.

import thunkMiddleware from 'redux-thunk';

import reducer from "../reducers";

const configureStore = () => {
  const middlewares = [thunkMiddleware];

개발모드나 배포모드나 thunkmiddleware가 장착이 된다. applymiddleware인자로 들어가준다.

// store의 configure.js 전체코드

import { createWrapper } from "next-redux-wrapper";
import { applyMiddleware, compose, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunkMiddleware from 'redux-thunk';

import reducer from "../reducers";

const configureStore = () => {
  const middlewares = [thunkMiddleware];
  const enhancer =
    process.env.NODE_ENV === "production"
      ? compose(applyMiddleware(...middlewares))
      : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  return store;
};

const wrapper = createWrapper(configureStore, {
  debug: process.env.NODE_ENV === "development",
});

export default wrapper;

loggermiddleware라고해서 action이 dispatch되는 것들을 로깅하는 미들웨어가 있다고 쳐보자.

middleware는 항상 두번째 아래와 같이 화살표 3개를 가지면 된다. 3단 고차함수라고 한다.

 

thunk는 첫번째 코드 처럼 action이 function인 경우(action은 원래 객체) thunk에서는 action을 function으로 둘 수 있다.

action이 function인 경우에는 지연함수이기 때문에 그 action을 나중에 실행해 줄수가 있는 것이다.

const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }
  return next(action);
}


const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  return next(action);
}

 

인자로 받은 next와 action을 이용해서 조작을 할 수가 있는데 예를 들면 아래와 같이 간단한 미들웨어이다. action을 실행하기 전에 console을 찍어주는 middleware..

그러한 것을 넣어준다.

const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  // if (typeof action === 'function') {
  //   return action(dispatch, getState);
  // }

  console.log(action);
  return next(action);
}

const configureStore = () => {
  const middlewares = [thunkMiddleware, loggerMiddleware];
  const enhancer =
// store에 configureStore.js 전체코드

import { createWrapper } from "next-redux-wrapper";
import { applyMiddleware, compose, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunkMiddleware from "redux-thunk";

import reducer from "../reducers";

const loggerMiddleware =
  ({ dispatch, getState }) =>
  (next) =>
  (action) => {
    // if (typeof action === 'function') {
    //   return action(dispatch, getState);
    // }

    console.log(action);
    return next(action);
  };

const configureStore = () => {
  const middlewares = [thunkMiddleware, loggerMiddleware];
  const enhancer =
    process.env.NODE_ENV === "production"
      ? compose(applyMiddleware(...middlewares))
      : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  return store;
};

const wrapper = createWrapper(configureStore, {
  debug: process.env.NODE_ENV === "development",
});

export default wrapper;

 

 

로그인과 로그아웃을 하면 콘솔에다가 로깅을 해주게 된다. 

 

리덕스 데브툴즈에서 로깅해주는 거처럼...

 

composeWithDevTools라고 되어있는 곳은 데브툴 미들웨어가 있는데 그것을 같이 넣어준것이라고 보면 된다. 리덕스 데브툴이 동작하는 것도 devtool middleware가 있어서 동작하는 것이다. 즉 리덕스의 기능을 middleware로 계속 확장을 해 줄수가 있다.

const enhancer =
    process.env.NODE_ENV === "production"
      ? compose(applyMiddleware(...middlewares))
      : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  return store;

 

thunk같은게 필요한 이유는 진행중인 프로젝트인 reucers 폴더안에 user.js파일 안에서 login action  / logout action 이렇게 두 개를 만들었는데 실제로 우리가 로그인을 원하고 로그아웃을 원한다고 해서 바로 로그인이 되고 바로 로그아웃이 되고 그렇진 않다. 

project folder

// reducers안에 user.js 파일

export const initialState = {
  isLoggedIn: false,
  me: null,
  signUpData: {},
  loginData: {},
};

export const loginAction = (data) => {
  return {
    type: "LOG_IN",
    data,
  };
};

export const logoutAction = () => {
  return {
    type: "LOG_OUT",
  };
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        isLoggedIn: true,
        me: action.data,
      };
    case "LOG_OUT":
      return {
        ...state,
        isLoggedIn: false,
        me: null,
      };
    default:
      return state;
  }
};

export default reducer;

 

로그인 로그아웃 모두 백엔드 서버에다가 요청을 한번 보내고 응답을 받아와야 하기 때문에 실제로는 아래와 같이 코드가 되어야 한다. Request를 빼고싶다고 해서 안쓸수 있는 것이 아니다.

export const loginRequestAction = (data) => {
  return {
    type: "LOG_IN_REQUEST",
    data,
  };
};

export const logoutRequestAction = () => {
  return {
    type: "LOG_OUT_REQUEST",
  };
};

 

 

그리고 아래와 같이 3개가 거의 그룹으로 나타나는데 비동기 요청은 어쩔 수가 없다.

그래서 대부분의 요청들이 다 비동기인데 게시글을 작성하든 코멘트를 작성하든 팔로잉을 하든 좋아요를 누르든 모든 기록들이 서버로 한번 갔다가 제대로 처리되면 성공 SUCCESS 실패했으면 FAILURE로 가서 어떠한 요청이든 3가지로 구성이 된다.

물론 성공했다고 무조건 login success action을 dispatch 할 필요는 없다. 아무일도 안해도 되긴 한다.

실패했다고 해서 loginRequestFailure을 dispatch 해야되는 것은 아니다. 실패했는데 실패했다는 에러메세지를 보여주고 싶지 않으면 아래 loginRequestFailure을 dispatch 안해주면 되는 것이다. 원칙적으로는 request success failure 세 단계가 있다고 기억을 하자.

export const loginRequestAction = (data) => {
  return {
    type: "LOG_IN_REQUEST",
    data,
  };
};
export const loginSuccessAction = (data) => {
  return {
    type: "LOG_IN_SUCCESS",
    data,
  };
};
export const loginFailureAction = (data) => {
  return {
    type: "LOG_IN_FAILURE",
    data,
  };
};


export const logoutRequestAction = () => {
  return {
    type: "LOG_OUT_REQUEST",
  };
};

export const logoutSuccessAction = () => {
  return {
    type: "LOG_OUT_SUCCESS",
  };
};

export const logoutFailureAction = () => {
  return {
    type: "LOG_OUT_FAILURE",
  };
};

 

 

그리고 thunk를 쓸 때는 아래코드를 request success failure 위에다가 적어준다. 요청할 때는 axios를 많이 쓰는데 요청하기 직전에 loginRequestAction()을 dispatch하고 그 다음에 어떤 주소로 요청을 보낸다. 그리고 성공했을 때와 실패했을 때를 나눠준다. 즉 redux-thunk를 쓰면 비동기 action creator 하나가 추가된 것을 볼 수 있다.

export const loginAction = (data) => {
  return (dispatch) => {
    dispatch(loginRequestAction());
    axios.post('/api/login')
    .then((res) => {
      dispatch(loginSuccessAction(res.data));
    })
    .catch((err) => {
      dispatch(loginFailureAction(err));
    })
  }
}

 

그리고 getState를 실행을 하면 initialState 즉 reducers폴더의 index.js에 있는 코드가 실행이 될 것이다.

// reducers폴더의 users.js 파일

export const loginAction = (data) => {
  return (dispatch, getState) => {
    const state = getState();
    dispatch(loginRequestAction());
    axios
      .post("/api/login")
      .then((res) => {
        dispatch(loginSuccessAction(res.data));
      })
      .catch((err) => {
        dispatch(loginFailureAction(err));
      });
  };
}
// reducers폴더의 index.js 코드

// (이전상태, 액션) => 다음상태
const rootReducer = combineReducers({
  index: (state = {}, action) => {
    switch (action.type) {
      case HYDRATE:
        console.log("HYDRATE", action);
        return { ...state, ...action.payload };
      default:
        return state;
    }
  },
  user,
  post,
});
// reducers폴더의 user.js 모든 코드



export const initialState = {
  isLoggedIn: false,
  me: null,
  signUpData: {},
  loginData: {},
};

export const loginAction = (data) => {
  return (dispatch, getState) => {
    const state = getState();
    dispatch(loginRequestAction());
    axios
      .post("/api/login")
      .then((res) => {
        dispatch(loginSuccessAction(res.data));
      })
      .catch((err) => {
        dispatch(loginFailureAction(err));
      });
  };
}

export const loginRequestAction = (data) => {
  return {
    type: "LOG_IN_REQUEST",
    data,
  };
};

export const loginSuccessAction = (data) => {
  return {
    type: "LOG_IN_SUCCESS",
    data,
  };
};

export const loginFailureAction = (data) => {
  return {
    type: "LOG_IN_FAILURE",
    data,
  };
};

export const logoutRequestAction = () => {
  return {
    type: "LOG_OUT_REQUEST",
  };
};

export const logoutSuccessAction = () => {
  return {
    type: "LOG_OUT_SUCCESS",
  };
};

export const logoutFailureAction = () => {
  return {
    type: "LOG_OUT_FAILURE",
  };
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        isLoggedIn: true,
        me: action.data,
      };
    case "LOG_OUT":
      return {
        ...state,
        isLoggedIn: false,
        me: null,
      };
    default:
      return state;
  }
};

export default reducer;

 

thunk를 안쓰고 saga를 쓰는 이유가 thunk를 한번에 dispatch를 여러번 할 수 있게 해준다 이게 끝이다. 더 이상의 기능이 없다. 그래서 나머지 것들은 자기가 다 구현을 해야한다. 

하지만 saga를 쓰면 delay 즉 몇초뒤에 action이 실행되게 하는거를 구현할 수 있다.

 

thunk였다면 아래같이 직접구현해야 할 것이다. 이렇게 delay 같은거를 javascript를 통해서 직접구현을 해줘야 한다.

export const loginAction = (data) => {
  return (dispatch, getState) => {
    const state = getState();
    // 이부분
    setTimeout(() => {
      dispatch(loginRequestAction());
    }, 2000)
    
    
    axios
      .post("/api/login")
      .then((res) => {
        dispatch(loginSuccessAction(res.data));
      })
      .catch((err) => {
        dispatch(loginFailureAction(err));
      });
  };
}

saga였으면 미리 만들어서 제공을 해준다. 그리고 복잡한 것들 실수로 클릭을 두번한 경우 로그인을 누르는데 실수로 클릭을 두번했다. 그러면 thunk에서는 클릭 두번한것이 요청이 다간다. 

하지만 saga에서는 takeLatest라는 것이 있어서 두번이 동시에 들어왔으면 가장 마지막꺼만 요청을 보내고 첫번째꺼는 무시해버린다.

그리고 throttle이라는 것이 있는 scroll 같은거 내릴 때 scrollEvent는 1초에 수백번씩 발생하기 때문에 그러한 eventListener안에다가 비동기요청 하는 것을 넣어놓으면 scroll 한 번 쫙 내렸는데 서버에 요청이 수백개가 날아간다.

이게바로 DoS(Denial of Service) 공격이다. 즉 프론트를 잘못만들면 서버에다가 DoS 공격을 때릴 수가 있다.

이걸 이제 배포까지 해버리면 DDoS 공격이 되는 것이다.

(자기서버에다가 셀프 DDoS 공격을 하는 프론트엔드 개발자는 과연...)

 

리덕스같은거 진행하다가 셀프디도스 하는 경우가 있다고 한다.. 이러한 셀프 DDoS 공격을 막으려면 1초에 몇번까지 허용해준다.

즉 1초에 3번이상 action이 발생하면 그러한 것들은 다 차단해버린다 이런것들이 가능하다.

throttle이나 debounce 이 두개로.... 

이러한 것들을 saga가 미리 구현을 해놓았다.

실무에서 많이 쓰이니 알아놓자. 

 

 

 

 

 

 

 

 

 

 

 

 

<출처 조현영: [리뉴얼] React로 NodeBird SNS 만들기>

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/dashboard

 

[리뉴얼] React로 NodeBird SNS 만들기 - 인프런 | 강의

리액트 & 넥스트 & 리덕스 & 리덕스사가 & 익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다., 새로 만나는 제로초의 리액트 노드버

www.inflearn.com

 

 

 

'React > NodeBird(ZeroCho)' 카테고리의 다른 글

saga 이펙트 알아보기  (0) 2021.10.29
saga 설치 & generator 이해하기  (0) 2021.10.28
정규표현식 맛보기  (0) 2021.10.28
리덕스의 원리와 불변성  (0) 2021.10.26
redux설치  (0) 2021.10.26