프로젝트 이모저모/HoduMarket 프로젝트

[오픈마켓 버그] 장바구니 무한렌더링 이슈

Ella Seon 2023. 5. 16. 11:14

0. 문제 개요 

장바구니에서  상품 정보를 렌더링 하려고 하는데 console.log(cartItems) 가 무한으로 출력되고 있었다.

function CartPage() {
  const dispatch = useAppDispatch();
  const TOKEN = useAppSelector((state: RootState) => state.login.token) || "";
  const cartItems = useAppSelector(
    (state: RootState) => state.cartList.cartItems
  );
  console.log(cartItems);
  const cartStatus = useAppSelector(
    (state: RootState) => state.cartList.status
  );

  // 장바구니 정보 가져오기
  useEffect(() => {
    if (TOKEN) {
      dispatch(fetchGetCartList(TOKEN));
    }
  }, [TOKEN, dispatch]);

  // 상품 상세 정보 가져오기
  useEffect(() => {
    const getProductDetails = async () => {
      for (const cartItem of cartItems) {
        dispatch(fetchGetProductDetail(cartItem.product_id));
      }
    };

    if (cartItems.length > 0) {
      getProductDetails();
    }
  }, [cartItems, dispatch]);

  return (
   ...중략
  );
}

export default CartPage;

 

- cartItems 은 아래와 같이 이루어져있다.

export interface CartItem {
  my_cart: number;
  is_active: boolean;
  cart_item_id: number;
  product_id: number;
  quantity: number;
  isChecked: boolean;
  item: Item | null;
}

 

1. 문제 원인

✅ 리액트 렌더링 방식

Redux에서 상태를 업데이트 할 때마다, 해당 상태를 구독하고 있는 모든 컴포넌트는 리렌더링이 발생한다.

- 무한렌더링이 발생하는 이유는 아래의 'useEffect'와 'fetchGetProductDetail' 액션과 관련되어있다.

useEffect의 의존성 배열(dependency array)에 cartItems를 포함시켜서, cartItems가 변경될 때마다 이 훅이 실행되도록 설정했다.

// 상품 상세 정보 가져오기
  useEffect(() => {
    const getProductDetails = async () => {
      for (const cartItem of cartItems) {
        dispatch(fetchGetProductDetail(cartItem.product_id));
      }
    };

    if (cartItems.length > 0) {
      getProductDetails();
    }
  }, [cartItems, dispatch]);

또한 fetchGetProductDetail 액션이 성공적으로 수행되면 cartItem의 item 속성이 업데이트 된다.

      .addCase(fetchGetProductDetail.fulfilled, (state, action) => {
        state.status = "succeeded";
        const productId = action.payload.product_id; // product_id 속성 사용
        const cartItem = state.cartItems.find(
          (item) => item.product_id === productId
        ); //기존 cartItem 의 product_id 와 fetchGetProductDetail 의 product_id 와 비교해서 같은거일때 cartItem 의 item 값에 추가
        if (cartItem) {
          cartItem.item = action.payload;
        }
      });

cartItem 의 item 속성이 업데이트되면, 'cartItems' 배열이 변경되고, 이에 따라 'useEffect'가 다시 실행된다.

그리고 다시 fetchGetProductDetail 가 실행되어 'item' 이 업데이트 되고, 또다시 'useEffect' 가 실행되는 등의 무한루프가 발생한다.

 

➡️fetchGetProductDetail.fulfilled 액션이 성공적으로 수행될 때마다 cartItems의 상태가 바뀌면서 해당 상태를 구독하고 있는 CartPage 컴포넌트가 리렌더링된다.
CartPage 컴포넌트가 리렌더링될 때마다 useEffect 훅이 실행되고, 그 안의 getProductDetails 함수가 호출되어 다시 fetchGetProductDetail 액션을 디스패치한다. 그 결과 fetchGetProductDetail.fulfilled가 다시 발생하고 cartItems의 상태가 업데이트되어 CartPage가 다시 리렌더링되는, 이런식으로 무한 루프가 발생한다.비록 상품 상세 정보가 이미 가져와진 상태라 할지라도, Redux에서 상태 업데이트는 컴포넌트 리렌더링을 일으키기 때문에 이러한 무한 루프가 발생하게 된다.

 

2. 문제 해결

- 무한 루프가 발생되는 이유는 carItems 가 변경될때마다 fetchGetProductDetail 가 실행되니, fetchGetProductDetail 을 계속해서 실행할 수 없게 만들어야한다. 

- if (cartItem.item)은 cartItem.item이 존재하는 경우(true)에는 continue를 통해 현재 반복 회차를 건너뛰고 다음 회차로 이동한다. 이미 상품 상세 정보를 가져온 cartItem은 다시 fetchGetProductDetail을 호출하지 않고 건너뛰게 된다.

이를 통해 중복된 API 호출을 피하고, 이미 가져온 상품의 상세 정보를 다시 가져오지 않도록 제어할 수 있다.

 

  // 상품 상세 정보 가져오기
  useEffect(() => {
    const getProductDetails = async () => {
      for (const cartItem of cartItems) {
        // 이미 상품 상세 정보를 가져온 경우는 반복문의 다음 회차로 건너뜀
        if (cartItem.item) {
          continue;
        }
        dispatch(fetchGetProductDetail(cartItem.product_id));
      }
    };

    if (cartItems.length > 0) {
      getProductDetails();
    }
  }, [cartItems, dispatch]);