- 사용자가 장바구니에 담은 내역을 확인
- 선택한 도서 아이템의 수량과 가격 합계를 표시
- 장바구니 담은 도서 아이템을 삭제
- 각 도서 아이템을 선택하여 주문서 작성
이렇게 필요한 기능을 미리 작성한 뒤 구현하는 방식.. 좋다!
장바구니 내역 확인
cart api 호출
각 항목 선택해서 수량 추가하는 방법 중.. Input Check타입을 사용하는 방법도 있지만, 아이콘으로 동작하도록 하는 방법
루프되는 아이템이 추가타입을 넣거나 or 체크된 데이터 배열을 (상태) 새로 생성하는 방법
해당 배열 아이템 컴포넌트의 해당 id를 가진 데이터가 목록에 있는지 판단해야 한다. checked
반복되는 상태 확인 `useMemo()`
//carts.tsx
const [checkedItems, setCheckedItems] = useState<number[]>([]);
const handleCheckedItem = (id: number) => {
if (checkedItems.includes(id)) {
//언체크
setCheckedItems(checkedItems.filter((item) => item !== id));
} else {
//체크
setCheckedItems([...checkedItems, id]);
return;
}
};
//cartItem.tsx
const isChecked = useMemo(() => {
return checkedItems.includes(cart.id);
}, [checkedItems, cart.id]);
const handleChecked = () => {
onCheck(cart.id);
};
return (
<CartItemStyle>
<div className="info">
<CheckIconButton isChecked={isChecked} onCheck={handleChecked} />
//CheckIconButton.tsx
interface Props {
isChecked: boolean;
onCheck: () => void;
}
const CheckIconButton = ({ isChecked, onCheck }: Props) => {
return (
<CheckIconButtonStyle onClick={onCheck}>
{isChecked ? <FaRegCheckCircle /> : <FaRegCircle />}
</CheckIconButtonStyle>
);
};
낙관적 업데이트
const deleteCartItem = (id: number) => {
deleteCart(id).then(() => {
setCarts(carts.filter((cart) => cart.id !== id));
});
};
매번 api 요청을 보내는게 아닌, 명시적으로 요청을 보내면서 ui 를 변경시킨다. 이후 다시 데이터를 로드하거나 받아오는 행위를 했을때 api 요청을 보내 잘 업데이트 되었는지 확인한다.
alert + confirm
확인 , 취소. 메시지와 함수를 함께 넘겨서 동작하게 한다.
const showConfirm = useCallback((message: string, onConfirm: () => void) => {
if (window.confirm(message)) {
onConfirm();
}
}, []);
`flex: 1;` : 늘어나겠다. 0은 늘리지 않겠다는 동작
코커톤에서 같은 프론트가 요걸로 나머지를 다 채웠길래.. 난 잘 안써서 왜쓰는지 물어봤다. flex 적용한 나머지 부분에 `width: 100%` 와 같은 효과.
근데 div 를 잘못 나눠서 내가 다시 수정했다... 안좋은 기억으로 마주친 flex:1 ... 많이 사용하겠지?
총 수량, 총 금액
체크된 아이템의 length 로 할 수 있지만, 같은 도서 여러권일 경우가 안된다. reduce 메서드로 각 아이템을 전체적으로 순회하면서 직접 더해주어야 한다.
총 금액의 경우, 각 가격 * 수량을 누적으로 동일하게 더해준다.
const totalQuantity = useMemo(() => {
return carts.reduce((acc, cart) => {
if (checkedItems.includes(cart.id)) {
return acc + cart.quantity;
}
return acc;
}, 0);
}, [carts, checkedItems]);
const totalPrice = useMemo(() => {
return carts.reduce((acc, cart) => {
if (checkedItems.includes(cart.id)) {
return acc + (cart.price*cart.quantity);
}
return acc;
}, 0);
}, [carts, checkedItems]);
복잡한 계산 같은 경우에는 백엔드에서 진행하기도 하지만, 프론트에서 동적으로 계산하는 경우가 더 많다!
아마 더 많은 api 호출을 줄이고, 서버 성능을 위해 할 수 있는 처리는 프론트에서 하고싶은게 아닐까..
라우트 데이터 전달
useNavigate 함수 안에 state 항목을 활용해서 정보를 전달한다.
const handleOrder = () => {
if (checkedItems.length === 0) {
showAlert('주문할 상품을 선택해 주세요.');
return;
}
//주문서 작성으로 데이터 전달
const orderData: Omit<OrderSheet, 'delivery'> = {
items: checkedItems,
totalPrice,
totalQuantity,
firstBookTitle: carts[0].title,
};
showConfirm('주문하시겠습니까?', () => {
navigate('/order', { state: orderData });
});
};
이동된 링크에서는 location 함수로 state 를 받아서 사용할 수 있다.
const location = useLocation();
const orderDataFromCart = location.state;
const {totalQuantity, totalPrice} = orderDataFromCart;
# 주문서 작성
- 장바구니로 부터 넘어온 정보를 화면에 표시하고 보존
- 주소, 수령인, 전화번호를 입력 및 검증
- 데이터를 가공하여 서버에 전달
useFrom()
useForm 에는 register, handleSubmit, formState, 타입을 지정해야 한다.
타입은 order 모델의 delivery 만떼와서, 상세정보를 추가해서 사용한다.
레지스터는 데이터의 키값을 찾아서 체크한다.
form 외부의 버튼을 눌렀을때, submit 동작하도록 함수처리 할 수 있다.
각각의 fieldset 은 입력이 안되었을때 에러를 띄울 수 있다. (formState)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<DeliveryForm>();
//..
<InputText
inputType="text"
{...register('address', { required: true })}
/>
//...
<Button
size="large"
scheme="primary"
onClick={handleSubmit(handlePay)}
>
클릭하면, 주문서 페이지에서 데이터를 뽑아온 다음, 서버로 넘겨준다.
const handlePay = (data: DeliveryForm) => {
const orderData: OrderSheet = {
...orderDataFromCart,
delivery: {
...data,
address: `${data.address} ${data.addressDetail}`,
},
};
//서버로 넘겨준다
console.log(orderData);
};
주소찾기 카카오 우편번호 서비스 (타입스크립트)
리엑트 라이브러리 존재.... 하지만!! 다음 제공 api 를 사용해보자!
- 스크립트 로드
- 핸들러
- 입력
스크립트 로드 시 index.html에 사용하기엔 번거로우니, useEffect 에서 스크립트 로딩되도록 한다.
useEffect(() => {
const script = document.createElement('script'); //<script></script>
script.src = SCRIPT_URL; //<script src="URL"></script>
script.async = true;
document.head.appendChild(script); //<head><script src="URL"></script></head>
return () => {
document.head.removeChild(script);
};
});
타입스크립트 고려가 안된 라이브러리를 사용할때 > `window.d.ts` 파일 새로 만들어서 타입 정의
interface Window {
daum: {
Postcode: any;
};
}
form 내부 버튼은 기본으로 submit 이 동작하기 때문에 type 를 버튼으로!
타입스크립트로 주소창 여는 핸들러
const FindAddressButton = ({ onCompleted }: Props) => {
const handleOpen = () => {
new window.daum.Postcode({
oncomplete: (data: any) => {
onCompleted(data.address as string);
},
}).open();
};
전달된 address 를 useFrom 의 setValue(key, value) 활용
`<FindAddressButton onCompleted={(address) => { setValue('address', address); }} />`
서버로 넘겨준다
export const order = async (orderData: OrderSheet) => {
const response = await httpClient.post('/orders', orderData);
return response.data;
};
//..
showConfirm('주문을 진행하시겠습니까?', () => {
order(orderData).then(() => {
showAlert('주문이 처리되었습니다');
navigate('/orderList');
});
});
#주문 내역
- 저장된 주문 내역을 목록(table)로 표시
- [자세히] 버튼으로 상세 정보 패널을 토글
데이터가 존재하지 않으면, 더이상 요청하지 않도록! > 이거 ,,, 이전프로젝트때 useEffect 에서 렌더링이 많이되어서 어려웠었는데,, 해결방법을 알았다!
const selectedOrderItem = (orderId: number) => {
if (orders.filter((item) => item.id === orderId)[0].detail) {
setSelectedItemId(orderId);
return;
}
fetchOrder(orderId).then((orderDetail) => {
setSelectedItemId(orderId);
setOrders(
orders.map((item) => {
if (item.id === orderId) {
return {
...item,
detail: orderDetail,
};
}
return item;
}),
);
});
};
if 문으로 요청방어