본문 바로가기
WEB/기초

[WEB] React, Redux - 2. combineReducer, middleware, useDispatch, thunk...

by IT황구 2021. 7. 31.
728x90
반응형

 

반갑습니다.

이제 강의도 벌써 4주차 강의를 듣고있습니다.

마지막주라서 그런지.. 내용도 많아지고 복습하고, redux 문서를 다시 다 읽어야하기에 포스팅 시간이 오래 걸렸습니다.

 

그래도 여기에 쓰면서 생각이 정리되는것도 있어서 굉장히 좋습니다^^

 

combineReducer
여러개의 리듀서를 하나의 스토어에서 다 실행합시다.

 

지난번에는 Reducer가 한개밖에 없었습니다. 그래서 createStore(reducer) 을 하면 그냥 됐었죠.

하지만, Reducer 하나에 모든 state를 끼워넣고 매번 불필요한 복사, 여러 조건이 들어간 수정. 이런식으로 해야할까요?

 

아닙니다. reducer도 관심사에 맞게 여러개의 reducer로 분리할 수 있습니다.

여러개로 분리를 했다고 합시다.

그러면 이제 store를 만들죠? 어떻게 하면 될까요?

아주 간단합니다. 아래 코드를 봅시다.

 

const store1 = createStore(reducer1)
const store2 = createStore(reducer2)
const store3 = createStore(reducer3)

 

바로 이렇게 하시면 탈락입니다.

아!! 이게 아니면 뭐 for문으로 돌리라 이런 말인가? 하실 수 있습니다.

 

그냥 저런 접근 자체가 틀렸습니다.

why?? redux의 디자인 컨셉을 저번에 썼었는데, store는 1개만 있어야 합니다. a single source of truth 아시죠? 네~

따라서 store가 여러개니까 디자인 컨셉에 위배됩니다.

 

그래서 나온것이 바로 combineReducer!!

한개의 스토어의 모든 리듀서를 넣습니다!!

 

 

export const configureStore = () =>{
    const store = combineReducer({
      reducer1, reducer2, reducer3
    });
    return store;
}

 

이렇게 하시면, 여러개의 reducer를 하나의 리듀서에서 사용할 수 있습니다.

 

작동은 어떻게 되는가?

combineReducer안의 reducer들을 하나씩 모두 호출하고, 동일한 형태의 state object를 생성합니다.

내가 reducer마다 지정해놓은 모든 return state를 하나의 object에 담습니다. 예시를 보면 이해가 갈겁니다.

 

import { createStore, combineReducers } from "redux";
const data = {
  id: 1,
  name: "test",
};

const copy_data = {
  id: 2,
  name: "test2",
};

export const configureStore = () => {
  const store = createStore(
    combineReducers({
      data: reducer,
      data2: copy_reducer,
    })
  );
  return store;
};

const reducer = (state = data, action) => {
  switch (action.type) {
    default:
      return { ...state, name: "john" };
  }
};

const copy_reducer = (state = copy_data, action) => {
  switch (action.type) {
    default:
      return { ...state, name: "cucu" };
  }
};

 

이제

const values = useSelector((val) => val);

console.log(values);

이걸 이용해서 values에 뭐가 찍히는지 보겠습니다.

각각의 reducer를 돌면서 반환하는 값들을 object로 만든다고 했죠?

바로 이렇게 나온답니다.

그러면 {values.data.id} 또는 {values.data2.id} 이런식으로 접근할 수 있답니다.

 

middleware
로깅, 비동기처리 등 reducer에 가기 전에 도와주는것

 

지금까지의 redux 의 data flow를 보면 계속 동기적인 작업만 보냈습니다. 중간에 값이 바뀌거나 할 수 없었습니다.

하지만 현실세계의 프로그램은 중간에 서버에서 값도 받아오고 해야합니다.

 

지난번에 Redux를 하면서 reducer는 "side effect"를 발생시키면 안된다고 했습니다.

그때 이게 뭔말인가 했는데, docs에 다 있었습니다.

 

1. 값을 console에 찍어보는 행위

2. 파일을 저장하는것

3. 비동기 타이머를 세팅하는 행위

4. AJAX 요청

5. 함수 밖에 존재하는 state를 변화시키거나, parameter의 state를 마음대로 바꾸는 행위

6. 랜덤값을 생성하는 행위

이런 행위들은 reducer의 pure function을 해치는 행위이기도 합니다.

 

위의것들을 안하고 어떻게 프로그램을 만드나요.. 너무 제한적입니다.

 

이러한 과정들을 도와주는것을 middleware라고 합니다.

redux에서의 미들웨어는, dispatch(action)을 통해서 action이 보내질 때, reducer에 도착하기 전에 middleware가 값을 가로챈 후에 여러 코드를 실행시키고나서 reducer에 보내는 것 입니다. 이해가 잘 안가시죠? 아래 영상을 봅시다.

https://redux.js.org/tutorials/essentials/part-5-async-logic

 

 

미들웨어는

1. action 수정하기

2. action delay하기

3. async call 등 여러가지를 할 수 있습니다.

 

참고로 Eventhandler에서 dispatch를 할때 action을 직접 넣는것 대신에, action을 반환하는 함수를 넣을 수 있습니다.

 

이 함수를 ActionCreator 라고 합니다.

이건 왜 쓸까요?

매번 action을 직접적으로 입력할 필요 없이, 반복적인 과정을 함수로 처리할 수 있는거죠.

정보 은닉도 할 수 있고요. reusable한 aciton이 되는겁니다.

 

과정을 다시 보면, 새로 추가된 Middleware의 역할은 다음과 같습니다.

dispatch(ActionCreator)을 해서 reducer로 가기 전에 Middleware에 한번 들릅니다.

만약 그냥 평범한 plain js object면 그냥 reducer로 가라고 합니다.

하지만, 서버에서 data를 불러와서 그걸 object에 담아야 한다면 ?

middleware에서 그것을 처리해주고, return을 받아서 action object에 다시 넣고 dispatch를 합니다.

 

저는 이걸보고 좀 더 와닿았는데.. 다른분들도 와닿았으면 좋겠습니다

 

그렇다면 dispatch(ActionCreator)를 아무때나 쓸 수 있을까요?

ActionCreator가 동기적 함수면 그냥 넣어도 됩니다.

 

export const changeName = () => ({

type: "pass",

payload: "joon",

});

이런 actionCreator면 그냥 dispatch(changeName()) 으로 해도 잘 돌아갑니다.

하지만, 문제는 내부에서 setTimeout같은 비동기처리 함수가 존재하면 에러가 납니다.

 

이런 경우에는 thunk를 applyMiddleware로 등록해줘야 합니다.

 

thunk는 비동기처리를 위한 미들웨어를 말합니다.

 

thunk middleware는 dispatch에 함수를 보낼 수 있게 해줍니다.

이건 dispatch와 getState를 인자로 받아서, 처리한 function의 return값을 action에 담아 dispatch도 할 수 있고, getState로 state도 가져올 수 있답니다.

 

 

이렇게 createStore의 두번째 인자로 대신 해 줍니다.

참고로 2번째 인자는 preloadState도 맞지만, storeEnhancer를 넣어줄 수도 있습니다.

 

자 그러면 이제 해봅시다.

 

처음에 화면이 나오고나서, dispatch(ActionCreator)를 통해서 함수를 전달시켜 봅시다.

 

 

ActionCreator는 내부에서, 2초후에 changeName을 실행시켜서 다시 dispatch 해주는 함수입니다.

 

따라서 2초 후에 action이 reducer로 전달되고, 그에 맞게 이름이 바뀌게 될 것입니다.

 

reducer에서 type이 pass면 이름을 joon으로 바꾸게 했습니다.

 

 

처음에 이렇게 john이 었던것이, 2초가 지면 어떻게 될까요?

 

이렇게 store update가 끝나고 joon으로 바뀌게 됩니다.

 

값을 봐도 이렇게 변했답니다.

 

import "./App.css";
import { useDispatch, useSelector } from "react-redux";
import { actionCreator } from "./reducer/configureStore";
import { useEffect } from "react";

function App() {
  const values = useSelector((val) => val);

  const dispatch = useDispatch();
  console.log("hi");

  console.log(values);

  useEffect(() => {
    dispatch(actionCreator());
    return () => {
      console.log("clean up!");
    };
  }, [dispatch]);

  return (
    <div>
      <div className="App">{values.data.id}</div>
      <div className="App">{values.data.name}</div>
    </div>
  );
}

export default App;

 

여기서 제가 마지막으로 말하고 싶은것은, useEffect에서 deps 에 dispatch를 주지 않으면 warning이 자꾸 거슬리게 나옵니다.

이게 왜 그런지 알아보겠습니다.

 

dispatch는 hook에 의해서 값이 변할 수 도 있는 변수로 선언되어 있습니다.

하지만, dispatch function이 참조하는 store ( <Provider store={store}/>) 가 변하지 않으면, 항상 pure하게 작동합니다.

redux concept이 store은 하나라서 store가 바뀔일은 없는데, 이걸 React hook인 effect는 잘 모릅니다. (바뀔수도 있다고 생각)

그래서 [dispatch]라고 넣어줘야 에러를 뱉지 않습니다.

 

처음에 이유를 몰라서 막 검색해봤는데, docs에 다 있었습니다.

https://react-redux.js.org/api/hooks#usedispatch

 

Hooks | React Redux

API > Hooks: the `useSelector` and `useDispatch` hooks`

react-redux.js.org

 


 

비동기처리, 로깅... 에 사용될 수 있는 미들웨어의 역할에 대해서 알아보았습니다.

여러가지 리듀서를 합칠수 있는 combineReducer도 알아보았고, 아마 이 포스팅이 올라가기 전에 useEffect도 하나 올라갈 것입니다.

 

비동기 처리를 위해서 thunk라는 미들웨어를 사용했고, 이걸 applyMiddleware에 넣어줬습니다.

거기서 return된 storeEnhancer는 createStore의 2번째 param으로 사용됐습니다.

 

dispatch로 보내서 state를 업데이트 하면 useSelector로 값을 가져와서 사용하면 됩니다.

 

useDispatch가 굉장히 편합니다

그게 아니면

 

이렇게 직접 state를 prop에 map 해서 connect를 해줘야 합니다.

 

 

내용이 많았네요

 

감사합니다

 

 

 

728x90
반응형