[React 완벽가이드] Section 20: 고급 리덕스

gonn-i·2024년 7월 12일
0

React 완벽 가이드

목록 보기
15/18
post-thumbnail

본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.

목차 🌳
1️⃣ redux로 비동기 작업
2️⃣ 🛒 컴포넌트 안에서 비동기 다루기
3️⃣ 🛒 action creators(액션 생성자) 안 에서 비동기 다루기

redux로 비동기 작업

이전 강의에서 배운 리덕스에서 리듀서함수는 무조건 순수 함수여야만 하며, side-effect 을 다룰 수 없고, 동기적으로 작동해야 한다고 배웠다. 이에 따라, 동일한 input을 넣었을때 항상 같은 output을 내는 함수를 만들었다. 그치만 여기에서 드는 궁금증!

리덕스로 작업할때, Http 요청은 어떻게 하나? / 비동기 코드는 어디에 넣어야 할까? 🤔

방법은 두가지가 있어요! ✌️
1️⃣ 컴포넌트 안에서 다루기 (useEffect 로 제어)
2️⃣ action creators(액션 생성자) 안 에서 다루기

firebase를 통해, 데베에 장바구니 정보를 저장하고 꺼내쓸 수 있도록 해보자!
-> 1️⃣ 장바구니 수량 추가 및 삭제시, put 요청 보내기
-> 2️⃣ 디비에 저장된 장바구니 정보 get 요청 보내기

1️⃣ 장바구니 수량 추가 및 삭제시, put 요청 보내기

📁 컴포넌트 안에서 (useEffect 로 제어)

단순 cart 값 PUT Req 보내기

function App() {
  // 카트 상태를 store에서 가져오기
  const cart = useSelector((state) => state.cart.items);

  // useEffect를 통해, cart 상태 변경시, store에서 cart를 꺼내와 fetch 함수를 통해 데베에 덮어쓰기
  useEffect(() => {
    fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json', {
      method: 'PUT', // PUT method를 통해 데이터를 덮어씀
      body: JSON.stringify(cart),
    });
  }, [cart]);
  return (
    <Layout>
      <Cart />
      <Products />
    </Layout>
  );
}

근데 이렇게 useEffect를 쓰는데 문제점!🚨

앱이 시작될때, 실행이 된다는 점!!
(이게 왜 문젠데요?) -> 왜냐면, 비어있는 초기 카드를 백엔드에 보내고, 거기에 저장된 모든 데이터를 덮어쓰기 때문!!!! 우리는 카트를 클릭을 했을때만 작동하길 원하지만, 마운트시 useEffect가 작동되기 때문이다.

useEffect 에서 마운트시 원치 않는 작동을 방지하는 법 🔍

boolean 을 담은 변수를 하나 선언해서, 최초 랜더링인지를 판별하여 실행

const isInitial = true // 전역으로 선언해줌

function App () {
  useEffect(() => {
    const sendCartData = async () => {
      // ... 생략
    };

    // 초기 랜더링일 경우, isInitial를 뒤집고 return 
    if (isInitial) { 
      isInitial = false;
      return;
    }

    sendCartData().catch((error) => {
	// ... 생략
    });
  }, [cart, dispatch]);


}

useEffect에 side Effect을 다루다보니 상당히 component가 길어진 경향이 있다...
2번째 방법을 통해 다른 방법도 찾아보자!


📁 action creators(액션 생성자)안 에서 다루기

redux Toolkit에서 우린 createSlice를 통해 자동으로 생성된 액션 생성자를 사용해왔다.

// 액션 생성자 자동 생성
export const cartActions = cartSlice.actions;

위와 같이 .acions 를 이용했었지만, 사실은 개발자가 직접 action을 만들 수도 있다!
우린 action에 비동기 작업을 담기 위해, action creatorthunk를 이용하여 만들어보려고 한다.

thunk 란 ? 🥸
다른 작업이 완료될때까지 작업을 지연시키는 단순한 함수! (나중에 실행할 코드 조각이란 뜻)
-> 작업 객체를 즉시 반환하지 않는 action creators를 작성할 수 있음
action 에 대한 return 값으로 함수를 넘겨주고, 그 함수 안에 side Effect을 넣는다.
(이로써 지연을 통해, action을 보내기 전에 비동기적으로 작업을 수행할 수 있다)

slice 파일 안에서, thunk 함수 정의

const cartSlice = createSlice({
  // .. 생략
});

// 슬라이스 외부에  Thunk 함수를 만듦
export const sendCartData = (cart) => {
  return async (dispatch) => {
    // 첫 번째 액션: 요청을 시작했다는 알림을 디스패치
    dispatch(
      modalActions.showNotification({
        status: 'pending',
        title: 'sending...',
        message: 'sending cart data!',
      })
    );

    const sendReq = async () => {
      const response = await fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json', {
        method: 'PUT',
        body: JSON.stringify(cart),
      });

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

    try {
      // 실제 비동기 Http Req
      await sendReq();
      // 두 번째 액션: 요청이 성공했음을 알리는 알림을 디스패치
      dispatch(
        modalActions.showNotification({
          status: 'success',
          title: 'Success!',
          message: 'send cart data successfully',
        })
      );
    } catch (error) {
      // 세 번째 액션: 요청이 실패했음을 알리는 알림을 디스패치
      dispatch(
        modalActions.showNotification({
          status: 'error',
          title: 'Failed!',
          message: 'fail to send cart data :(',
        })
      );
    }
  };
};

사용할땐 useEffect 훅 안에서 dispatch의 인자로thunk 함수를 호출하면 된다.
(이때 자동으로 dispatchthunk 함수에 대한 인자로 전달됨)


function App() {
  // ...생략
  
  useEffect(() => {
    // 마운트시 실행 방지
    if (isInitial) {
      isInitial = false;
      return;
    }

    dispatch(sendCartData(cart));
  }, [cart, dispatch]);

  return (
    <>
    	// ...생략
    </>
  );
}

export default App;

2️⃣ 인제 디비에 저장된 장바구니 정보 fetch 하기

fetch 를 담은 thunk 함수 정의

export const fetchCartData = () => {
  return async (dispatch) => {
    const fetchData = async () => {
      const res = await fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json');

      if (!res.ok) {
        throw new Error('could not fetch data');
      }

      const data = await res.json();
      return data;
    };

    try {
      const cartData = await fetchData();
      console.log(cartData);
      // 장바구니에 물건이 없을 경우를 대비하여, [] 설정
      dispatch(cartActions.replaceCart({ items: cartData.items || [], totalQuantity: cartData.totalQuantity }));
    } catch (err) {
      console.log(err);
      dispatch(
        modalActions.showNotification({
          status: 'error',
          title: 'Failed!',
          message: 'fail to get cart data :(',
        })
      );
    }
  };
};

component 안에서 useEffect로 실행

	// 카트 정보 fetch 
  useEffect(() => {
    dispatch(fetchCartData());
  }, [dispatch]);

	// 카드 정보 put 
  useEffect(() => {
    if (isInitial) {
      isInitial = false;
      return;
    }
    
	// 장바구니에 변경 사항이 있을때만 작동되도록
    if (cart.changed) {
      dispatch(sendCartData(cart.items, cart.totalQuantity));
    }
  }, [cart, dispatch]);

Redux Devtools

리덕스 slice가 복잡해질때, 전체 리덕스 스토어의 현재 상태를 살펴볼때 사용!

어떤 action 이 불려,
상태에 어떠한 변화가 이루어졌는지 보기 쉬움

0개의 댓글