신입 개발자에서 시니어 개발자가 되기까지

[Redux] dispatch는 action을 reducer 어떻게 전달하는가? 본문

javascript/React Js

[Redux] dispatch는 action을 reducer 어떻게 전달하는가?

Jin.K 2022. 11. 3. 14:47

어제 과제를 하면서 생긴 의문점 하나 때문에 하루종일을 매달렸다. 의문의 시발점은 다음과 같다.
"reducer가 여러 개인 경우 dispatch함수의 인자는 action만 존재하고, 어느 reducer에 전달할지는
지정해주지 않았는데 왜 combineReducer 안에 있는 reducer을 딱 찾아서 전달 할 수 있었을까?"

의문점이 생긴 코드

 const state = useSelector((state) => state);
  console.log(state);
  const { cartItems, items } = state.itemReducer;
  const dispatch = useDispatch();
  console.log(dispatch);
  const [checkedItems, setCheckedItems] = useState(
    cartItems.map((el) => el.itemId)
  );

  const handleQuantityChange = (quantity, itemId) => {
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
    dispatch(setQuantity(itemId, quantity));
  };

  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId));
    dispatch(removeFromCart(itemId));
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
  };

dispatch의 인자로 전달된 action은 itemReducer로 전달되어야 한다. 그런데 itemReducer는
state를 받아올 때 사용한 것 외에는 사용되지 않았다.

결론부터 먼저 말하면, dispatch는 action을 store에 전달하고, store는 모든 reducer를 호출한다.

솔직히 코드스테이츠 유어클래스에서 dispatch가 reducer에 action을 전달한다고 했고, 데이터 흐름이
Action → Dispatch → Reducer → Store 라고 해서 더 헤맸다. 그냥 중간과정을 다 생략해버린 설명이다.
여기서 말하는 '데이터'는 action객체를 의미하는 것 같은데, reducer가 store보다 먼저 나왔다.
하지만 찾아본 공식문서나 이걸 정리해둔 문서들에서는 action이 store을 거쳐서 reducer에 전달된다고 말한다.

근거 문서

https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
Redux Application Data Flow 부분에서
The app code dispatches an action to the Redux store(app 코드는 action객체를 store에 dispatch한다.)
The store runs the reducer function again with the previous state and the current action, and saves the return value as the new state(그러면 store는 reducer 함수를 호출한다)

데이터 흐름(Data Flow)

1.dispatch함수를 호출하면 action객체를 store로 전달한다.

2.store은 지정 해놓은 모든 reducer 함수를 호출한다.

-모든 reducer 함수를 forEach로 순회하며 호출한다. action객체가 전달되는 reducer함수만 호출되는 것이 아니다.

3.root reducer는 각 reducer의 반환값을 합쳐서 state tree를 만든다

    const rootReducer = combineReducers({
      itemReducer,
      notificationReducer
    });

이렇게 정의해놨는데

    { itemReducer : { items : […], cartItems : […] }, notificationReducer : { … } }

이런 식으로 각 reducer가 반환하는 값들이 하나의 state tree로 저장된다.

4.redux 스토어가 루트 리듀서에 의해 반환된 상태 트리를 저장한다.

-위의 state tree를 새로운 state로 업데이트 한다. 만약 state를 참조하고 있는 컴포넌트가 있다면
rerendering이 되는 단계다.

그렇다면, 2단계에서 store가 모든 reducer함수를 호출한다는 것을 어떻게 알 수 있는가?
모든 reducer를 객체로 저장해서 반환하는 combineReducer의 소스코드를 살펴보면 된다.

combineReducer

1.개념

reducer를 관련있는 state에 따라 쪼개고 난 뒤, 여러 reduce를 객체로 관리하는 함수다.
각 reducer들을 key에 저장하고, 호출해서 호출 결과들을 하나의 객체로 다시 합쳐준다.
(이 결과들은 각각의 reducer들의 key에 저장된다)
a. 예시
- combineReducers는 인자로 전달해준 키와 value를 가지는데 여기서 value는 각reducer function이다.

constrootReducer = combineReducers({
  floodCount: floodCountReducer,
  furniture: furnitureReducer
})

이렇게 전달하고 store.getState()를 호출하면

{
  floodCount: 0,
  furniture: {
       hasFurniture: true
    }
}

각 키 값에 대응하는 function의 return 값이 나온다.

2.combineReducer의 소스코드

function combineReducers(reducers) {
  // grab all the keys from the object passed in
  const reducerKeys = Object.keys(reducers);
  // return a function that has the same signature as all reducers do
  // (state, action) => state
  return function combination(state = {}, action) {
    // a flag to track if the action has caused any changes at all
    let hasChanged = false;
    const nextState = {};
    // loop through each reducer that was passed in
    reducerKeys.forEach((key) => {
      const reducer = reducers[key];
      // grab the slice of the whole state that is
      // relevant for this reducer, again based on its key
      const previousStateForKey = state[key];
      // actually run that slice through the reducer for this key
      const nextStateForKey = reducer(previousStateForKey, action);
      // tack it onto the object we'll ultimately return
      nextState[key] = nextStateForKey;
      // keep track of whether or not anything actually changed
      // but only keeps this check going if we don't already know
      // something has changed.
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    });
    // return the unchanged or the new state
    return hasChanged ? nextState : state;
  };
}

출처 : https://read.reduxbook.com/markdown/part1/03-updating-state.html

이 코드를 보면 인자로 전달받은 모든 reducers들의 key를 배열로 만들고, combination 함수
내에서 forEach로 순회하는 것을 볼 수 있다. 모든 key를 순회하고 마지막에 변경사항이 있으면
변경된 nextState를 반환하고, 아니면 기존의 state를 반환한다. (여기서 state는 모든 state가
하나의 객체로 결합된 형태일 것이다.)

결론

컴포넌트에서 state를 업데이트 하기 위해 dispatch(action)을 호출하면, 이 action 객체는 app에 존재하는
단 하나의 store에 전달된다. store은 action을 전달 받으면 state와 action을 store에 연결된 "모든" reducer에
인수로 전달하여 호출한다.(=combineReducer를 호출한다)
그리고 combinReducer는 모든 reducer를 순회하면서 action값을 전달하여 업데이트된 state를 반환한다. state가
업데이트 되지 않았다면 기존의 state를 반환한다.

그 외 참고자료

https://dobbit.github.io/redux/basics/DataFlow.html