관리 메뉴

Jerry

til #23 본문

자기 성찰/TIL

til #23

juicyjerry 2021. 8. 7. 03:57
반응형

Action  Reducer  Store  Dispatch

 

 

오늘도 어김없이 [인프런] Redux vs MobX (둘 다 배우자!), 조현영 강좌를 수강해 공부를 하였다.

이번에 배운 내용은 리덕스의 미들웨어, thunk, devtools와 immer library, 클래스 컴포넌트를 사용하는 유저를 위한 connect 기능을 알아보았다.

 

먼저, 미들웨어에 대해서 적어보겠다. 

리덕스에서 Action은 기본적으로 객체 형태로 동기적으로 동작한다. 

Dispatch라는 함수는 해당 객체를 받아 전달하는 역할이다.

이 둘 사이(Action과 Dispatch)의 들어가 위치해 동작하는 것이 미들웨어다. 

미들웨어로 thunk와 saga가 많이 쓰인다고 한다.

(근데 toolkit이 나온뒤로 이걸로 thunk와 saga를 대체할 수 있다고 하는데 내일 toolkit 공부할 차례니 무엇이 맞는지 확인해봐야겠다!)

 

 

 

https://www.npmtrends.com/redux-saga-vs-redux-thunk-vs-@reduxjs/toolkit

 

 

 

thunk와 saga 둘 다 비동기인데 thunk와 saga를 각각 어느 때에 사용해야하는지 구별된 것이 있을까? 차이가 무엇일까?

 saga가 더 배우기 어렵지만 thunk보다 기능이 많다고 한다. thunk는 실제 요청/응답과 같은 간단한 비동기만 있을 때 사용하고 saga는 이전 요청 취소, 딜레이, 쓰로틀링 등 다양한 비동기 작업이 필요할 때 사용한다고 한다.

 

 

비동기 Action은 리덕스에 없는 개념이다. 리덕스에서 비동기 Action은 존재하지 않기에 결국, 동기 Action들을 시간 순서를 조정해서 눈속임을 한다. 이런 비동기 Action은 thunk에서 아래와 같은 형태에서 미들웨어(middleware)로 사용하기로 약속한 것이다.

 

 

아래 code snippet을 보자.

변수 firstMiddleware와 thunkMiddleware, 이 두 변수는 미들웨어다. 

생소하게 생겼다. 3차 고차함수 형태를 띄고 있는 이 것은 도입부에서 잠깐 언급했듯이 커리(currying)이라는 방법이다. 

 

 

커링은 함수와 함께 사용할 수 있는 고급기술이다. f(a, b, c)처럼 단일 호출로 처리하는 함수를 f(a) (b) (c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것입니다.

출처: javascript.info

 

 

이런 커링 방식은 확장성이 뛰어난 방식으로 중간중간에 끼워 넣을 수 있다. 미들웨어가 이런 형태를 띈 것은 리덕스가 함수형 프로그래밍의 영향을 많이 받아서 그렇다.

 

 

// 3단 고차함수, 아래 코드와 동일하다
const firstMiddleware = (store) => (dispatch) => (action) => {
  console.log("로깅: ", action); // 기능 추가

  dispatch(action); // 기본 기능, 기능 추가할 수 있는 공간이 dispatch 전후로 생김

  // console.log("액션 끝");
};

// 실행되는 시점에 따른 차이
function firstMiddleware(store) {
  return function (dispatch) {
    return function (action) {
      // ...
    };
  };
}

// 액션은 객체고 동기적인 기능밖에 못하는데, 로그인이나 로그아웃, 회원가입 같은 서버를 거치는 작업을 하는데
// 액션을 동기 작업은 액션을 객체로 디스패치하겠고 비동기 작업을 함수로 디스패치 해줄게 그럼 썽크너가 실행해죠 (약속함)
const thunkMiddleware = (store) => (dispatch) => (action) => {
  if (typeof action === "function") {
    // 비동기를 구별해주기 위해, 비동기인 경우에는 액션을 함수로 만들어주겠다 라는 썽크에서의 약속
    // 객체가 현재 동기적으로 동작하고 있고 객체로도 비동기처리를 할수 있긴하다고 한다.

    return action(store.dispatch, store.getState); // 얘네들은 어디에 전달되냐? -> action으로!
  }
  return dispatch(action);
};

 

 

 

firstMiddleware의 커링 형태와 우리가 익숙한 형태의 firstMiddleware 함수를 볼 수 있는데, 이 둘은 같다!

그 다음에 있는 thunkMiddleware도 커링 형태인 미들웨어다. thunk 다운 받지 않고 직접 코드로 구현한 것이다.

 

 

Action은 객체 형태로 동기적인 기능밖에 못한다고 했다. 

그렇지만 로그인이나 로그아웃, 회원가입 같은 서버를 거치는 비동기 작업을 해야 될 경우에는 동기적으로 (객체 형태로) Dispatch말고, 함수로 Dispatch로 비동기 처리하는 역할을 thunk가 하는 것을 의미한다.

참고로, thunk 코드를 까(?)보면 몇 줄 되지 않는다고 한다. (아래를 보거나 출처 링크로 들어가 확인해보시길 바란다)

 

 

 

https://github.com/reduxjs/redux-thunk/blob/master/src/index.js

 

 

 

 

+) 추가로, 비동기에 관한 내용이 나와서 비동기 액션 커스텀에 대해서도 이야기해본다.

index2.js에서 로그인에 관한 더미데이터를 dispatch를 한다. 

user.js에서 logIn이란 비동기 액션 크리에이터가 dispatch한 내용을 처리한다. 이것은 여기서 서버 기능을 하고 있는데 비동기 처리를 한다. 

 

 

하지만, 이 비동기 액션도 눈속임에 불과하다. 비동기 액션 크리에이터라는 기능을 미들웨어를 통해 추가하고 동기 한 번 부르고 비동기 작업안에서 끝날쯤에 동기 한번 더 부르고하는 형태를 띄우는 것을 확인할 수 있다. 이건 동기 액션들간에 실행 순서를 조작하는 정도밖에 되지 않는다.

 

 

 

 

 

 

 

 


 

 

 

 

 

다음에는 redux-devtools와 immer library, connect에 대해서 알아보겠다.

 

devtools를 추가는 아래와 같다. 

삼항연산자를 통해 배포 환경과 개발 환경을 분리하였다. 그 이유는 devtools이 고객들이 접속하는 (배포)환경에서 사용하면 리덕스에 몰려이는 데이터들이 유출될 수가 있기 때문이다.

 

 

// const enhancer = compose(applyMiddleware());
// 함수 합성 함수 : compose (combineReducer랑 비슷?)
const enhancer =
  process.env.NODE_ENV === "production"
    ? compose(applyMiddleware(firstMiddleware, thunkMiddleware))
    : composeWithDevTools(applyMiddleware(firstMiddleware, thunkMiddleware));

const store = createStore(reducer, initialState, enhancer);

module.exports = store;

 

 

 

immer library는 불변성을 지키는 것에 적용해봄으로써 immer의 순기능을 알 수있다.

아래의 code snippet은 immer의 기본꼴로, produce 함수는 이전 state를 바탕으로 action을 적용해 다음 state를 만든다.

 

 

const { produce } = require("immer");

nextState = produce(prevState, (draft) => {});

 

 

 

#1.
	  state.data = null;
      state.deep.deeper.deepest.a = 'b'; 이걸 쓰려고 하는데 불변성을 지키려고 #2 같이 한다.
      


#2.
      {
        ...prevState,
        data: null,
        deep: {
          ...prevState.deep,
          deeper: {
            ...prevState.deeper,
            deepest: {
              ...prevState.deepest,
              a: 'b'
            }
          }
        }
      }
      
      
=> 이걸 편하게 해주려고 immer 라이브러리를 사용한다.

 

 

immer 전과 후를 코드로 비교 해보자!

 

 

 

#1. immer 미적용
const userReducer = (prevState = initialState, action) => {
  switch (action.type) {
    case LOG_IN_REQUEST:
      return {
        ...prevState,
        isLoggingIn: true,
      };
    case LOG_IN_SUCCESS:
      return {
        ...prevState,
        data: action.data,
        isLoggingIn: false,
      };
    case LOG_IN_FAILURE:
      return {
        ...prevState,
        data: null,
        isLoggingIn: true,
      };
    case LOG_OUT:
      return {
        ...prevState,
        data: null,
        isLoggingIn: false,
      };
    default:
      return prevState;
  }
};

 

 


 

 

#2. immer 적용 
const { produce } = require("immer");

const userReducer = (prevState = initialState, action) => {
  return produce(prevState, (draft) => {
    switch (action.type) {
      case LOG_IN_REQUEST:
        (draft.data = null), (draft.isLoggingIn = true);
        break;
      case LOG_IN_SUCCESS:
        (draft.data = action.data), (draft.isLoggingIn = false);
        break;
      case LOG_IN_FAILURE:
        (draft.data = null), (draft.isLoggingIn = false);
        break;
      case LOG_OUT:
        (draft.data = null), (draft.isLoggingIn = false);
        break;
      default:
        break;
    }
  });
};

 

 

 

 

마지막 connect 함수는 기존 클래스 컴포넌트를 사용하는 유저가 리덕스를 사용할 수 있기 위해서 컨테이너 컴포넌트를 만드는 다른 방법이다.이 방법은 hooks에서 useSelector,  useDispatch로 편하게 사용하기 때문에 별로 다룰 일이 많이 없을 거라고 한다.

hooks로 하는 방법은 분리되어 있어서 바꾸기 편하지만, 클래스 컴포넌트 방식은 하나로 묶여 있어서 다같이 바꿔줘야한다.

 

 

 

// 이 방식을 사용한다면, 해당 함수가 실행할 때마 리렌더링되어서 성능상 문제가 생길 수 있어 reselect를 사용한다. (훅스의 경우에는 resselect를 사용할 필요가 없다)
// 리덕스 state를 component props로 바꾼다
const mapStateToProps = (state) => ({
  user: state.user,
  posts: state.posts,
}); // reselect

// 리덕스 dispatch를 component props로 바꾼다
const mapDispatchToProps = (dispatch) => ({
  // action login, action logout과는 다르다. 헷갈리면 앞에 dispatch를 붙여준다.
  // hooks랑 비교해보면, 이건 하나로 묶여있어서 바뀔 때마다 다같이 바꿔줘야되지만 hooks는 분리되어 있어서 바꾹 편하다
  dispatchLogIn: (data) => dispatch(logIn(data)),
  dispatchLogOut: () => dispatch(logOut()),
});

// connect라는 high-order component,App compoennt를 확장하는 connect hight order component
export default connect(mapStateToProps, mapDispatchToProps)(App);

 

 

 

connect는 HOC라고 리액트 컴포넌트를 개발하는 하나의 패턴으로써, 컴포넌트의 로직을 재활용할 때 유용한 패턴이다. 이 또한 hooks 도입 후 대체가능해져서 HOC를 만들 이유가 떨어졌기 때문에 "컴포넌트를 특정 함수로 감싸서 특정 값 또는 함수를 props로 받아와서 사용할 수 있게 해주는 패턴"이라는 것 정도만 알아두면 된다고 한다.

 

 

 

connect 함수는 리덕스 스토어 안에 있는 상태를 props로 넣어줄 수도 있고, 액션을 디스패치하는 함수를 props로 넣어줄수도 있다.

참고 | https://react.vlpt.us/redux/09-connect.html

 

 

 

  const user = useSelector((state) => state.user);
  const dispatch = useDispatch(); // dispatch 함수를 가져옴

 

 

 

 

cf) import와 require

흔히들, import와 require 쉽게 바꿀 수 있다고 생각하는데 비슷한 기능도 있지만 서로는 다른 기능이다. 내부적인 동작이 다르다. 대표적으로 디폴트 기능이 다르다. 95% 정도 같은 경우가 있지만, immer처럼 다른 경우도 있다.

 

 

 

 

 

 

 

반응형

'자기 성찰 > TIL' 카테고리의 다른 글

til #26  (0) 2021.08.17
til #24  (0) 2021.08.10
til #22  (0) 2021.08.05
til #21  (0) 2021.07.31
til #20  (0) 2021.07.31