[ React ] mutation (update & delete) & checkbox handling (forwarded Ref)

CJY00N·2023년 7월 11일
0

react

목록 보기
4/10
post-thumbnail

⚡️ 장바구니 수량 업데이트

장바구니 수량 업데이트 쿼리문 작성

기존 ADD_CART 쿼리문과 비슷하나 amount가 추가된 것이다.
▼ src/graphql/cart.js

export const UPDATE_CART = gql`
  mutation UPDATE_CART($id: string, $amount: number) {
    cart(id: $id, amount: $amount) {
      id
      imageUrl
      price
      title
      amount
    }
  }
`;

업데이트 핸들러 함수 정의

업데이트를 하는데 cartData가 없는 것은 에러를 발생시킨다.
기존에서 amount만 전달된 값으로 바꿔주면 된다.
▼ src/mocks/handler.ts

  graphql.mutation(UPDATE_CART, (req, res, ctx) => {
    const newCartData = { ...cartData };
    const { id, amount } = req.variables;
    if (!newCartData[id]) {
      throw new Error("없는 데이터입니다.");
    }
    const newItem = {
      ...newCartData[id],
      amount,
    };
    newCartData[id] = newItem;
    cartData = newCartData;
    return res(ctx.data(newItem));
  }),

=> 이렇게만 했을 때의 문제점은 amount의 수량을 화살표키(?)로 증감하여도 데이터 상으로는 amount가 변경되지만, 화면으로 보여지는 것은 바로 변경되지 않는다.

⚡️ Query Invalidation

https://tanstack.com/query/v4/docs/react/guides/query-invalidation

invalidateQueries()

: 캐시된 쿼리들을 무효화(invalidate)한다. 특정 쿼리나 쿼리 그룹에 속한 모든 쿼리들을 강제로 재요청하고, 캐시된 데이터를 업데이트할 수 있다.

▼ src/components/cart/item.tsx

  const handleUpdateAmount = (e: SyntheticEvent) => {
    const amount = Number((e.target as HTMLInputElement).value);
    updateCart(
      { id, amount },
      {
        onSuccess: () => queryClient.invalidateQueries(QueryKeys.CART),
      }
    );
  };

onSuccess에 넣지 않고 따로 넣을 경우 updateCart와 invalidationQueries 둘 다 비동기 요청이기 때문에 순서가 뒤죽박죽되어 제대로 작동하지 않는 것을 주의하자.

좋은 방법인가?

: 하나의 변경에도 모든 API를 다시 요청하므로 효율적인 방법이라고 볼 수 없다. request는 줄일 수 있다면 줄여야 한다. 캐시를 변경하는 방법은 없을까?

위 사진에서 보이는 것처럼 UPDATE를 할 때마다 GET을 같이한다.

⚡️ Optimistic Updates(낙관적 업데이트)

https://tanstack.com/query/v4/docs/react/guides/optimistic-updates

  • Optimistic Updates는 사용자 인터페이스에서 발생한 작업을 즉시 반영하여 응답을 기다리지 않고 사용자 경험을 개선하는 기술이다. 일반적으로 네트워크 요청에 의해 데이터가 변경될 때 사용되며, 사용자의 작업이 서버에 도달하기 전에 로컬 상태를 변경하여 향상된 반응성을 제공한다.
  • 만약 동일 useQuery를 쓰는 뷰가 많다면, 1개 업데이트로 전부 반영이 되므로 처음에 정의하기에 번거로울지라도 더 효율적일 수 있다.

▼ src/components/cart/item.tsx

  const { mutate: updateCart } = useMutation(
    ({ id, amount }: { id: string; amount: number }) =>
      graphqlFetcher(UPDATE_CART, { id, amount }),
    {
      onMutate: async ({ id, amount }) => {
        await queryClient.cancelQueries(QueryKeys.CART);
        const prevCart = queryClient.getQueryData<{ [key: string]: Cart }>(
          QueryKeys.CART
        );
        if (!prevCart?.[id]) return prevCart;

        const newCart = {
          ...(prevCart || {}),
          [id]: { ...prevCart[id], amount },
        };

        queryClient.setQueryData(QueryKeys.CART, newCart);
        return newCart;
      },
      onSuccess: (newValue) => {
        const prevCart = queryClient.getQueryData<{
          [key: string]: Cart;
        }>(QueryKeys.CART);

        const newCart = {
          ...(prevCart || {}),
          [id]: newValue,
        };

        queryClient.setQueryData(QueryKeys.CART, newCart);
      },
    }
  );

onSucess에 들어오는 newVaule는 바뀐 값 하나만 들어오는데, Cart 전체에 대한 데이터를 변경해야하므로, 나머지는 그대로 두고 새로 들어온 데이터만 변경된다.

update할 때마다 get cart를 하지 않게 된다.

상품 수량 업데이트 mutation 호출&사용

amount가 1보다 작으면 그냥 return하도록 한다.
▼ src/components/cart/item.tsx

  const handleUpdateAmount = (e: SyntheticEvent) => {
    const amount = Number((e.target as HTMLInputElement).value);
    if (amount < 1) return;
    updateCart({ id, amount });
  };

수량(amount)을 1 아래로 조절할 수 없도록 최솟값을 설정하고 handleUpdateAmount함수를 onChange함수에 연결해주었다.

      <label>
        <input className="cart-item_amount" type="number" value={amount} min={1} onChange={handleUpdateAmount />{" "}</label>

⚡️ 장바구니 페이지 스타일링

각 상품마다 삭제버튼과 체크박스, 전체에 대한 전체선택 체크박스를 추가하였다.

.cart-item {
  display: flex;
  flex-direction: row;
  padding-bottom: 10px;
  margin-bottom: 10px;
  border-bottom: 1px solid #000;
  justify-content: space-between;
  align-items: center;

  &_image {
    width: 100%;
    height: 200px;
    object-fit: contain;
  }
}

⚡️ 장바구니 항목 삭제 기능

삭제 쿼리문 작성

▼ src/graphql/cart.ts

export const DELETE_CART = gql`
  mutation DELETE_CART($id: string) {
    id
  }
`;

삭제 핸들러 등록

▼ src/mockes/handler.ts

  graphql.mutation(DELETE_CART, (req, res, ctx) => {
    const id = req.variables.id;
    const newCartData = { ...cartData };
    delete newCartData[id];
    cartData = newCartData;
    return res(ctx.data(id));
  }),

삭제 mutation 호출&사용

  • 낙관적 업데이트 대신 invalidateQueries를 사용했다.
    ▼ src/components/cart/item.tsx
  const { mutate: deleteCart } = useMutation(
    ({ id }: { id: string }) => graphqlFetcher(DELETE_CART, { id }),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(QueryKeys.CART);
      },
    }
  );
  const handleDeleteItem = () => {
    deleteCart({ id });
  };
      <button className="cart-item_removeButton" type="button" onClick={handleDeleteItem}>x</button>

⚡️ 장바구니 상품 전체선택 처리하기

제어 컴포넌트 방식(state 사용)을 사용하지 않고 비제어 컴포넌트 방식을 사용한다.

formdata 사용하기

https://developer.mozilla.org/en-US/docs/Web/API/FormData

  • Formdata는 HTML단이 아닌 자바스크립트 단에서 폼 데이터를 다루는 JAVASCRIPT API이다.
  • FormData 객체는 자동으로 name 속성이 있는 요소들에서만 데이터를 수집하고, name 속성이 없는 요소는 무시한다.

createRef 사용하기

  • CartItem 컴포넌트에 ref를 넘겨주기 위해서 items.map에 createRef로 ref를 만들어 checkboxRefs에 저장한다.
  • CartItem 컴포넌트를 호출할 때에 ref값도 함께 넘겨준다.

▼ src/components/cart/index.tsx

...
  const checkboxRefs = items.map(() => createRef<HTMLInputElement>());
...
        {items.map((item, i) => (
          <CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
        ))}
...

forwardedRef 사용하기

  • 장바구니에 있는 상품목록의 ref를 정의하고 부모 컴포넌트에서 ref에 접근해야 하므로 forwaredRef를 사용한다.
  • CartItem의 props에 ref를 추가한다.
    (ForwardedRef 식으로 )
  • input태그의 ref를 props의 ref로 지정해준다.
  • export를 해줄때 forwaedRef로 감싸준다.

▼ src/components/cart/item.tsx

...
const CartItem = (
  { id, title, imageUrl, price, amount }: Cart,
  ref: ForwardedRef<HTMLInputElement>
) => {
  
...

        <input
        className="cart-item_checkbox"
        type="checkbox"
        name={`select-item`}
        ref={ref}
      />
...
  
export default forwardRef(CartItem);

...

전체선택 핸들러 함수

  • 전체선택 체크박스가 선택되면 모든 체크박스가 선택된다.
  • 전체선택 체크박스가 선택해제되면 모든 체크박스가 선택해제된다.
  • 전체선택 체크박스가 선택되지 않은 상태일 때 선택된 체크박스의 갯수가 전체 아이템의 갯수와 같으면(모두 선택된 경우) 전체선택 체크박스가 선택된다.

▼ src/components/cart/index.tsx

  const formRef = useRef<HTMLFormElement>(null);

  const handleCheckboxChanged = (e: SyntheticEvent) => {
    if (!formRef.current) return;
  
    const targetInput = e.target as HTMLInputElement;
    const data = new FormData(formRef.current);
    const selectedCount = data.getAll("select-item").length;
  
    if (targetInput.classList.contains("cart_select-all")) {
      const allchecked = targetInput.checked;
      checkboxRefs.forEach((inputElem) => {
        inputElem.current!.checked = allchecked;
      });
    } else {
      const allchecked = selectedCount === items.length;
      formRef.current.querySelector<HTMLInputElement>(
        ".cart_select-all"
      )!.checked = allchecked;
    }
  };

전체를 form으로 감싸고 ref를 formRef로 지정하고 onChange함수에handleCheckboxChanded함수를 넣어준다.

  return (
    <form ref={formRef} onChange={handleCheckboxChanged}>
      <label>
        <input className="cart_select-all" name="select-all" type="checkbox" />
        전체선택
      </label>
      <ul className="cart">
        {items.map((item, i) => (
          <CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
        ))}
      </ul>
    </form>
  );

첫번째 사진처럼 전체선택되지 않았을 때 두번째 사진처럼 선택되지 않은 나머지 두 항목을 체크하면 전체선택 체크박스도 함께 체크된다.

profile
COMPUTER SCIENCE ENGINEERING / Web Front End

0개의 댓글