1️⃣ 장바구니 페이지
1. pages/cart/index.tsx
import { useQuery } from "react-query";
import { QueryKeys, graphqlFetcher } from "../../queryClient";
import GET_CART, { Cart } from "../../graphql/cart";
import CartList from "../../assets/components/cart/list";
const CartIndex = () => {
// 장바구니에 담은 아이템들 불러오기
const { data } = useQuery(QueryKeys.CART, () => graphqlFetcher(GET_CART), {
// 처음에 장바구니에 담고 다른 페이지 갔다가 돌아와서 다시 누르면 안 들어가는 현상 때문에 staleTime과 cacheTime을 따로 설정
staleTime: 0,
cacheTime: 1000,
});
const cartItems = Object.values(data || {}) as Cart[];
if (!data) return <div>장바구니가 비었어요</div>;
return <CartList items={cartItems} />;
};
export default CartIndex;
2. components/cart/list.tsx
import { Cart } from "../../../graphql/cart";
import CartItem from "./item";
const CartList = ({ items }: { items: Cart[] }) => {
return (
<ul>
{items?.map((item: any) => (
<CartItem {...item} key={item.id} />
))}
</ul>
);
};
export default CartList;
3. components/cart/item.tsx
import { useMutation } from "react-query";
import { Cart, UPDATE_CART } from "../../../graphql/cart";
import { graphqlFetcher } from "../../../queryClient";
import { SyntheticEvent } from "react";
const CartItem = ({ id, imageUrl, price, title, amount }: Cart) => {
// 장바구니에서도 수량 변경을 할 수 있게 하기 위해 추가
const { mutate: updateCart } = useMutation(
({ id, amount }: { id: string; amount: number }) =>
graphqlFetcher(UPDATE_CART, { id })
);
// 수량 업데이트 함수
const handleUpdateAmount = (e: SyntheticEvent) => {
const amount = Number((e.target as HTMLInputElement).value);
updateCart({ id, amount });
};
return (
<li className="cart-item">
<img src={imageUrl} alt="" />
<p className="cart-item__price">{price}</p>
<p className="cart-item__title">{title}</p>
<input
type="number"
className="cart-item__amount"
value={amount}
onChange={handleUpdateAmount}
/>
</li>
);
};
export default CartItem;
4. mocks/handlers.ts 에 장바구니 업데이트 부분 추가
// 장바구니 업데이트
graphql.mutation(UPDATE_CART, (req, res, ctx) => {
const newData = { ...cartData };
const { id, amount } = req.variables;
if (!newData[id]) {
throw new Error("없는 데이터입니다");
}
const newItem = {
...newData[id],
amount,
};
newData[id] = newItem;
cartData = newData;
return res(ctx.data(newItem));
}),
5. graphql/cart.ts 에도 장바구니 업데이트 부분 추가
// 장바구니 업데이트
export const UPDATE_CART = gql`
mutation UPDATE_CART($id: string, $amount: number) {
cart(id: $id, amount: $amount) {
id
imageUrl
price
title
amount
}
}
`;
2️⃣ invalidateQueries vs 낙관적 업데이트
처음에 장바구니 아이템 수량을 업데이트를 하려고 할 때 invalidateQueries를 사용하려고 했었다.
하지만 여러번 요청을 보내야 하므로 낙관적 업데이트를 통해 수량을 업데이트 하는 게 나을 것 같다고 하셨다.
1. invalidateQueries
: 캐시된 쿼리들을 무효화(invalidate)한다. 특정 쿼리나 쿼리 그룹에 속한 모든 쿼리들을 강제로 재요청하고, 캐시된 데이터를 업데이트할 수 있다.
const handleUpdateAmount = (e: SyntheticEvent) => {
const amount = Number((e.target as HTMLInputElement).value);
updateCart(
{ id, amount },
mutation -> query 이 순서로 뜨게 하기 위해
{
onSuccess: () => queryClient.invalidateQueries(QueryKeys.CART),
}
);
};
- updateCart와 invalidataionQueries 둘 다 비동기 요청,, 비동기 요청으로 인해 순서가 바뀔 수 있으므로 onSuccess에 넣어준다.
2. 낙관적 업데이트
- Optimistic Updates는 사용자 인터페이스에서 발생한 작업을 즉시 반영하여 응답을 기다리지 않고 사용자 경험을 개선하는 기술이다. 일반적으로 네트워크 요청에 의해 데이터가 변경될 때 사용되며, 사용자의 작업이 서버에 도달하기 전에 로컬 상태를 변경하여 향상된 반응성을 제공한다.
- 만약 동일 useQuery를 쓰는 뷰가 많다면, 1개 업데이트로 전부 반영이 되므로 처음에 정의하기에 번거로울지라도 더 효율적일 수 있다.
const { mutate: updateCart } = useMutation(
({ id, amount }: { id: string; amount: number }) =>
graphqlFetcher(UPDATE_CART, { id, amount }),
{
onMutate: async ({ id, amount }) => {
await queryClient.cancelQueries(QueryKeys.CART);
// Snapshot the previous value
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 prevCart;
},
onSuccess: (newValue) => {
// 아이템 하나에 대한 데이터
const prevCart = queryClient.getQueryData<{ [key: string]: Cart }>(
QueryKeys.CART
);
const newCart = {
...(prevCart || {}),
[id]: newValue,
};
queryClient.setQueryData(QueryKeys.CART, newCart); // Cart 전체에 대한 데이터
},
}
);
3️⃣ 장바구니 페이지 선택, 전체선택 버튼 추가 / 삭제
- 제어 컴포넌트 방식(state 사용)을 사용하지 않고 비제어 컴포넌트 방식 사용
💡 formdata 사용하기
- Formdata는 HTML단이 아닌 자바스크립트 단에서 폼 데이터를 다루는 JAVASCRIPT API이다.
- FormData 객체는 자동으로 name 속성이 있는 요소들에서만 데이터를 수집하고, name 속성이 없는 요소는 무시한다.
💡 createRef 사용하기
- CartItem 컴포넌트에 ref를 넘겨주기 위해서 items.map에 createRef로 ref를 만들어 checkboxRefs에 저장한다.
- CartItem 컴포넌트를 호출할 때에 ref값도 함께 넘겨준다.
1. components/cart/list.tsx
import { SyntheticEvent, createRef, useRef } from "react";
import { Cart } from "../../../graphql/cart";
import CartItem from "./item";
const CartList = ({ items }: { items: Cart[] }) => {
// 전체 폼 요소에 대한 참조
const formRef = useRef<HTMLFormElement>(null);
// 각 아이템 체크박스 요소에 대한 참조 배열
const checkboxRefs = items.map(() => createRef<HTMLInputElement>());
// 전책 선택 핸들러 함수
const handleCheckboxChanged = (e: SyntheticEvent) => {
if (!formRef.current) return;
// 변경된 체크박스 요소 확인
const targetInput = e.target as HTMLInputElement;
// 현재 폼의 데이터를 FormData로 가져오기
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;
}
};
return (
// 폼 엘리먼트에 참조를 설정하고, 체크박스 변경 시 이벤트 핸들러를 호출
<form ref={formRef} onChange={handleCheckboxChanged}>
<label>
<input className="cart_select-all" type="checkbox" name="select-all" />
전체선택
</label>
<ul className="cart">
{items?.map((item: any, i) => (
<CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
))}
</ul>
</form>
);
};
export default CartList;
2. components/cart/item.tsx
import { useMutation } from "react-query";
import { Cart, DELETE_CART, UPDATE_CART } from "../../../graphql/cart";
import { QueryKeys, getClient, graphqlFetcher } from "../../../queryClient";
import { forwardRef, ForwardedRef, SyntheticEvent } from "react";
const CartItem = (
{ id, imageUrl, price, title, amount }: Cart,
ref: ForwardedRef<HTMLInputElement>
) => {
const queryClient = getClient();
// 장바구니 수량 업데이트
const { mutate: updateCart } = useMutation(
({ id, amount }: { id: string; amount: number }) =>
graphqlFetcher(UPDATE_CART, { id, amount }),
{
onMutate: async ({ id, amount }) => {
await queryClient.cancelQueries(QueryKeys.CART);
// Snapshot the previous value
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 prevCart;
},
onSuccess: (newValue) => {
// 아이템 하나에 대한 데이터
const prevCart = queryClient.getQueryData<{ [key: string]: Cart }>(
QueryKeys.CART
);
const newCart = {
...(prevCart || {}),
[id]: newValue,
};
queryClient.setQueryData(QueryKeys.CART, newCart); // Cart 전체에 대한 데이터
},
}
);
// 장바구니 삭제
const { mutate: deleteCart } = useMutation(
({ id }: { id: string }) => graphqlFetcher(DELETE_CART, { id }),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.CART);
},
}
);
// 수량 업데이트 함수
const handleUpdateAmount = (e: SyntheticEvent) => {
const amount = Number((e.target as HTMLInputElement).value);
if (amount < 1) return;
updateCart({ id, amount });
};
// 삭제 함수
const handleDeleteItem = () => {
deleteCart({ id });
};
return (
<li className="cart-item">
<input
className="cart-item__checkbox"
type="checkbox"
name={`select-item`}
ref={ref}
/>
<img className="cart-item__image" src={imageUrl} alt="" />
<p className="cart-item__price">{price}</p>
<p className="cart-item__title">{title}</p>
<input
type="number"
className="cart-item__amount"
value={amount}
onChange={handleUpdateAmount}
min={1}
/>
<button
className="cart-item__button"
type="button"
onClick={handleDeleteItem}
>
삭제
</button>
</li>
);
};
export default forwardRef(CartItem);
- forwrdRef - CartItem 컴포넌트를 외부에서 참조 가능하도록 만들어줌. 부모 컴포넌트에서 'ref'를 통해 자식 컴포넌트에 직접 접근할 때 유용
- ref : ForwardedRef<HTMLInputElement> - CartItem 컴포넌트에 전달되는 'ref'에 대한 타입 지정. 부모 컴포넌트에서 CartItem에 접근할 때 해당 ref 사용 가능
3. mocks/handler.ts
// 장바구니 지우기
graphql.mutation(DELETE_CART, ({ variables: { id } }, res, ctx) => {
const newData = { ...cartData };
delete newData[id];
cartData = newData;
return res(ctx.data(id));
}),
3일차 끝..!
state를 사용하지 않고 바닐라 js로 해보는 건 처음이었는데.. 역시 근본인 js를 잘해야하는구나를 깨달았다..
어려웠지만 하나하나 해석해보고 새로운 걸 습득한 거로만으로도 만족!
열심히 해야겠다 정말..!!
'REACT > 쇼핑몰 프로젝트' 카테고리의 다른 글
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (7/10) (0) | 2024.01.24 |
---|---|
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (6/10) (0) | 2024.01.23 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (5/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (4/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (1/10) (1) | 2024.01.13 |