컴포넌트 중복 렌더링 문제 해결하기

blueprint·2023년 4월 27일
1

🔥 이슈 발생

중복 렌더링 이슈는 프로젝트 작업 중 가장 나를 애먹였고 해결하는 데 가장 오래 걸렸던 부분이다.

상품 상세 페이지에서 장바구니 버튼을 누르면 해당 상품이 axios 통신을 통해 담겨진다. 동시에 Redux에도 해당 내용이 저장된다. Redux에 담긴 상품을 carts로 가지고 와 map으로 순회할 수 있게 했고, 상위 컴포넌트에서 전달받은 productId를 통해 상품의 정보까지 가지고 오도록 했다. 상품을 가지고 오는 것과 정보 출력하는 것까지는 성공했으나! 여러 차례 중복 렌더링되는 문제를 겪게 됐다.

  • 수정 전 ItemCard
export default function ItemCard({ productId }) {
  const navigate = useNavigate();
  const dispatch = useDispatch();

  const { carts } = useSelector((state) => ({
    carts: state.cartDetailReducer.carts,
  }));

  const cartItemDetails = async () => {
    try {
      const res = await AxiosInstance.get(`products/${productId}/`);
      dispatch(getCarts(res.data));
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    cartItemDetails();
  }, [productId]);

  return (
    <>
      {carts &&
        carts.map((item) => (
          <ItemContainer key={item.product_id}>
            <input type="checkbox" />
            <img src={item.image} alt="상품이미지" />
            <ItemInfo>
              <span>{item.store_name}</span>
              <strong>{item.product_name}</strong>
              <p>{item.price?.toLocaleString()}</p>
              <span>{item.shipping_method}</span>
            </ItemInfo>
            <QuantityButton />
            <ItemPrice>
              <p>{item.price?.toLocaleString()}</p>
              <button onClick={() => navigate("/payment")}>주문하기</button>
            </ItemPrice>
          </ItemContainer>
        ))}
    </>
  );
}

이전 코드에서는 cartDetailReducer의 상태를 불러오기 위해 useSelector를 사용하여 컴포넌트가 렌더링될 때마다 해당 상태를 업데이트하고 렌더링을 반복했기 때문에 중복 렌더링이 발생하는 것이었다.


✏️ 1차 수정

이전 코드에서는 useState를 이용해 상태를 관리하고 있었고, useEffect를 이용해 해당 상품의 데이터를 가져와 화면에 렌더링하는 방식이었다. 따라서 상태가 변경될 때마다 렌더링이 발생해 중복 렌더링이 발생했다.

이번에는 Redux를 이용해 상태를 관리하도록 변경했다. 상태가 변경될 때마다 렌더링이 발생하지 않고, Redux store에서 상태가 업데이트될 때만 렌더링이 발생하므로 중복 렌더링이 발생하지 않을 것이다.

  • Redux
export const cartDetailReducer = (state = initialState, action) => {
  switch (action.type) {
    case "GET_CARTS":
      return {
        ...state,
        image: action.image,
        store_name: action.store_name,
        product_name: action.product_name,
        shipping_method: action.shipping_method,
        price: action.price,
      };
    default:
      return state;
  }
};
  • ItemCard 컴포넌트
const { image, store_name, product_name, shipping_method, price } 
  = useSelector((state) => state.cartDetailReducer);

const cartItemDetails = async (
  image,
  store_name,
  product_name,
  shipping_method,
  price
) => {
  try {
    const res = await AxiosInstance.get(`products/${productId}/`);
    image = res.data.image;
    store_name = res.data.store_name;
    product_name = res.data.product_name;
    shipping_method = res.data.shipping_method;
    price = res.data.price;
    
    dispatch({
      type: "GET_CARTS",
      image,
      store_name,
      product_name,
      shipping_method,
      price,
    });
  } catch (error) {
      console.log(error);
  }
};

useEffect(() => {
  cartItemDetails();
}, [productId]);

그랬더니 웬걸? 담긴 상품들 중 하나로 통일되어 나타나는 오류가 발생했다.
중복 렌더링 문제는 해결됐지만, 모든 상품의 데이터가 하나의 상태 객체에 담겨 있기 때문에 해당 상품의 데이터를 가져오더라도 상태를 변경하지 않고, 상태값을 바로 활용할 수 있어서 데이터가 하나로 통일되어 나오게 됐다.


✏️ 2차 수정

그래서 Redux를 삭제하고, useState로 cartItem이라는 상태를 관리하도록 변경해 보았다. 이 상태는 useEffect에서 비동기로 가져온 cartDetail 데이터를 저장하므로 productId가 변경되어도 cartItem 상태가 변경되지 않는 한 렌더링이 반복되지 않는다. 그러므로 이전 코드보다 효율적으로 렌더링을 처리할 수 있다.

  • ItemCard
export default function ItemCard({ productId, cartId }) {
  const navigate = useNavigate();
  const [modal, setModal] = useState(false);
  const [cartItem, setCartItem] = useState([]);

  const onClickModal = () => {
    setModal(!modal);
  };

  function getCartDetail(id) {
    return AxiosInstance.get(`/products/${id}`).then((res) => res.data);
  }

  useEffect(() => {
    async function getCart() {
      const cartDetail = getCartDetail(productId).then((detail) => {
        setCartItem(detail);
      });
      return cartDetail;
    }

    getCart();
  }, [productId]);

  return (
    <>
      <ItemContainer>
        <input type="checkbox" />
        <img src={cartItem.image} alt="상품이미지" />
        <DeleteBtn onClick={onClickModal} />
        <ItemInfo>
          <span>{cartItem.store_name}</span>
          <strong>{cartItem.product_name}</strong>
          <p>{cartItem.price}</p>
          <span>{cartItem.shipping_method}</span>
        </ItemInfo>
        <QuantityButton onClick={onClickModal} />
        <ItemPrice>
          <p>{cartItem.price}</p>
          <button onClick={() => navigate("/payment")}>주문하기</button>
        </ItemPrice>
      </ItemContainer>
      {modal && <Modal option="delete" cartId={cartId} />}
    </>
  );
}

0개의 댓글