React Redux reducer는 왜 반드시 순수함수여야 하는가?? feat. 그럼 비동기는?

·2022년 7월 4일
0

react

목록 보기
17/24
post-thumbnail

reducer는 왜 반드시 순수함수여야 할까??

요약해서 설명하자면, 기존 state를 직접 수정하는 경우, redux의 변경 감지 알고리즘이 state 객체의 주소값을 비교하기 때문입니다.

객체는 참조형 데이터이며, 참조형 데이터의 특징은 주소값을 가진다는 것 입니다. reducer를 순수함수 형태로 반환한다면, 새로운 주소값이 생성되고 이를 통해 state가 변경되었음을 감지하여, 새로운 변화를 적용시킵니다. 만약 기존의 state 값을 직접 수정하게 된다면 원래 있는 주소값은 그대로 보존된 체, 해당 값만 바뀌게 되며, redux가 인지하지 못하는 상황이 발생하게 됩니다.

왜 이렇게 코드를 짰을까??

shllow copy 와 deep copy 성능 차이 때문일겁니다. 실제 같은 주소를 지닌 두 객체를 비교하기 위해서는 객체의 모든 속성을 살펴보며, 변화를 감지해야 합니다. 만약 shallow copy 형태로 변화를 감지했다면, 개발자들은 무척 편하겠지만, 그만큼 너무나 복잡하고 무거운 알고리즘을 필요로 하게 되어, 지금보다 느리고 비효율적인 어플리케이션이 만들어지게 될 겁니다.

그럼 비동기는??

비동기는 절대 순수함수가 될 수 없습니다. 왜냐구요?? 자바스크립트 비동기 함수는 Promise 객체를 반환하기 때문입니다. 또한 상황에 따라 error가 날 수도 있으니 동일한 인자를 넣는다 하더라도 값이 다르게 나타나기도 합니다.

근데 우리가 웹 앱을 개발하며, 비동기를 안 쓸 수 있을까요?? 웹이 이정도로 발달하는데에는 비동기를 제외하고는 설명읋 할 수 없을 정도로, 비동기는 이미 웹 역사에 많은 부분을 기여했습니다. 순수함수를 사용해야한다는 규칙성과 순수함수가 될 수 없는 비동기를 같이 사용하는 방법은 없을까요??

1. useEffect()를 통해 컴포넌트 측에서 처리한다.

useEffect()는 부수 효과가 일어날 수 있는 함수를 처리해주는 도구입니다. 간단히 설명하자면, 리액트 측에서는 컴포넌트 렌더링을 마친 후에 부수 효과를 실행시키 문제를 해결하였습니다.

리액트 컴포넌트가 렌더링된 이후 side-effect를 실행시켜 수행되는 시점에는 이미 DOM이 업데이트를 되었음을 보장합니다. side-effect가 컴포넌트 렌더링에 영향을 주지 않는 것이죠.

문제는 useEffect() 는 리턴값으로 의존성 인자와 의존성 데이터가 변경될 경우 실행할 함수를 나타내야 하는데, 비동기 함수는 promise를 객체를 반환하기 때문에 해당 비동기 함수를 제어해줄 래퍼 함수가 하나 필요합니다.

// 기본 useEffect() 에서 비동기 처리하기
function Profile({userId}){
  const [user,setUser]= useState();
  async function fetchAndSetUser(needDetail){
    const data = await fetchUser(userId, nedDetail);
    setUser(data);
}
...
... 
useEffect(() => {
  fetchAndSetUser(false);
}, [fetchAndSetUser]); //1 
// redux + useEffect()
function App() {
  const dispatch = useDispatch();
  const showCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);
  const notification = useSelector((state) => state.ui.notification);

  useEffect(() => {
    const sendCartData = async () => {
      dispatch(
        uiActions.showNotification({
          status: "pending",
          title: "Sending...",
          message: "Sending cart data!",
        })
      );
      const response = await fetch(
        "https://react-http-2a23a-default-rtdb.firebaseio.com/cart.json",
        {
          method: "PUT",
          body: JSON.stringify(cart),
        }
      );

      if (!response.ok) {
        throw new Error("Sending cart data failed");
      }

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success!",
          message: "Sent cart data successfully!",
        })
      );
    };

    sendCartData().catch((error) => {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "Sending cart data failed",
        })
      );
    });
  }, [cart, dispatch]);

  return (
    <>
      {notification && <Notification status={notification.status} title={notification.title} message={notification.me} />}
      <Layout>
        {showCart && <Cart />}
        <Products />
      </Layout>
    </>
  );
}

export default App;

2. Action-creator를 통해 처리하기

비동기 통신을 통해 리덕스의 전역 데이터에 적용은 시켜야 하는데, 리덕스에서 저장소에 데이터를 저장하는 방법 action이며 해당 영역은 순수함수로 사용할 것을 공식문서에서 말합니다.
Action creators 단지 액션 함수를 반환하는 함수입니다. useEffect() 에서 처리하는 래퍼함수의 역할을 Action creator에게 전가하여 처리하면 redux 내부에서도 처리가 가능합니다.

export const fetchCartData = () => {
  return async (dispatch) => {
    const fetchData = async () => {
      const response = await fetch(
        "https://react-http-2a23a-default-rtdb.firebaseio.com/cart.json"
      );

      if (!response.ok) {
        throw new Error("Could not fetch cart data!");
      }

      const data = await response.json();

      return data;
    };

    try {
      const cartData = await fetchData();
      dispatch(
        cartActions.replaceCart({
          items: cartData.items || [],
          totalQuantity: cartData.totalQuantity,
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "Fetching cart data failed",
        })
      );
    }
  };
};

3. middleware 사용하기

앞의 2. 의 과정을 간편화 한 것이 middleware 입니다.
미들웨어는 디스패치 함수를 결합해서 새 디스패치 함수를 반환하는 High-Order-Function 입니다. 미들웨어는 함수 결합을 통해 서로 결합하는 것이 가능하며, 액션을 로깅하거나, 부수효과를 일으키거나, 비동기 API 호출을 일련의 동기 액션으로 바꾸는 데 유용합니다.

@reduxjs/toolkit createAsycnThunk 라는 react-thunk 기능을 내장하고 있습니다. 해당 기능은 더 공부한 후 정리하도록 하겠습니다.

profile
새로운 것에 관심이 많고, 프로젝트 설계 및 최적화를 좋아합니다.

0개의 댓글