푸드코트 키오스크 만들기
간단한 프로젝트 설명과 트러블 슈팅 과정
🧾 프로젝트 설명
식당 이름을 검색하거나, 음식 종류 카테고리를 골라 필터링이 가능한 키오스크를 구현했다.
✅ 메뉴 목록을 조회할 수 있고(GET), 주문을 생성하는(POST) API를 제공하는 express 서버를 구축했다.
✅ 메뉴를 선택하면 점심 바구니에 담긴다.
✅ 합계 금액이 적힌 주문 버튼을 누르면 영수증이 출력된다.
📌 express 서버 구축
애초에 서버를 전혀 모르기 때문에 나에겐 쉽지 않은 도전이었다. express 강의를 듣고 정리해둔 노트를 참고했다.
post로 주문 생성하기
app.post('/orders', (req, res) => {
const { menu, totalPrice } = req.body;
const orders = {
id: Date.now().toString(),
menu,
totalPrice,
};
res.status(201).send(orders);
});
값을 등록하는 로직이면 사용자에게 값을 받아서, 등록해주어야 하는데 이걸 어떻게 구현하는지 고민이 되었다. 그래도 한 번 배우고 나니까 쉬워보였다.
값 넘겨주기
res.status(200).send({ restaurants });
왜 {} 이렇게 오브젝트로 묶어서 주는지 이해가 잘 안됐다. restaurants: restaurants 이라는 동일한 중복 이름이라 restaurants를 한 번만 쓴 것은 이해했는데 왜 저 형태일까? 여러 데이터를 한 번에 넘겨주고, 받아서 처리하기 편하게 하기 위함이다. 자바스크립트 기본 문법이 약한가싶어 조금 위축됐었다.
프론트엔드에서 값을 받을 때
const data = await response.json();
그냥 받으면 Promise 객체가 반환된다. 이를 풀어주는 과정에서 response.json()을 해주어야 한다. 강의에서 나온 내용인데 직접 문제를 겪으니 기억이 더 잘 날 것 같다.
📌 Custom Hook의 규칙
custom hook을 컴포넌트 내의 함수 안에서 호출하려고 했다. 이것도 강의에서 나온 hook의 규칙이었는데, '난 저러지 않겠지' 라고 생각했는데 저질러버렸다. 해당 함수는 API 요청 관련 함수였는데 커스텀 훅으로 빼려다가 다른 기능이 더 추가되어야 해서 그냥 App.tsx에 남겨두었다.
const handleReceipt = async ({ menu, totalPrice }: {
menu: Menu[],
totalPrice: number
}) => {
const url = 'http://localhost:3000/orders';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ menu, totalPrice }),
});
const data = await response.json();
setReceipt(data);
setCart([]);
};
📌 취소 기능 구현
기존 코드
cartItem의 id가 string으로 되어 있고, 클릭한 아이템의 id와 일치하지 않는 것만 골라내는 식으로 삭제를 구현하려고 했다. 그러나 같은 음식을 두개 주문하는 경우를 고려하지 못했다. id가 겹치니 취소가 제대로 동작하지 않았다.
수정 후 코드
index를 받아서 동일 인덱스를 거르는 식으로 구현했다. 상위의 App.tsx에서 구현해놓은 함수라 하위 컴포넌트로 넘겨준 타입을 계속 바꿔주자니 props drilling 문제를 겪었다. 최종으로 넘겨받아 button에 전달한 함수는 index를 넘겨준다. 중간에 cartItem.id를 Number()로도 감싸보고, onDeleteCart(cartItem.id) 콜백함수에 as number도 써보고 별 짓 다했다. 타입스크립트가 아직도 어렵다. index를 활용한다는 본질을 깨달았으면 되었을 일이다.
// 기존 코드
onClick={() => onDeleteCart(cartItem.id)}
// 수정 후 코드
onClick={() => onDeleteCart(index)}
📌 동일 키 오류 해결
위의 문제와 비슷한 맥락에서 발생한 문제이다.
카트의 아이템을 구성하는 <li> 태그에 key로 넘겨줄 고유한 값을 지정해 주었다.
이 key를 이용해 질리도록 보았던 동일 키 오류를 해결했다. (기존 코드 <li key={cartItem.id}>)
{
cart.map((cartItem, index) => {
const key = `${cartItem.id}-${index}`;
// return (
<li key={key}>
}
}
최종 코드
<>
<h2>🛒 점심 바구니</h2>
{
cart.length > 0
? (
<ul style={{ listStyle: 'none' }}>
{
cart.map((cartItem, index) => {
const key = `${cartItem.id}-${index}`;
return (
<li key={key}>
<span
style={{ width: 180, marginRight: 20, display: 'inline-block' }}
>
{cartItem.name}
(
{(cartItem.price).toLocaleString('ko-kr')}
원)
</span>
<button
type="button"
name={`#${cartItem.name}`}
onClick={() => onDeleteCart(index)}
>
취소
</button>
</li>
);
})
}
</ul>
)
: (
<div style={{ marginTop: 20, marginBottom: 20 }}>바구니가 비어있습니다.</div>
)
}
<OrderButton
totalPrice={totalPrice}
onCreateReceipt={handleCreateReceipt}
/>
</>
📌 로컬 E2E 테스트 (feat: codecept)
과제를 제출하려면 최종 구현까지 마친 후 codecept를 이용한 로컬 E2E 테스트를 진행해야 한다. 기능 구현은 다 되었는데 테스트를 계속 실패해서 또 한참 삽질했다. 테스트에 관한 내용은 구글링으로도 잘 나오지 않아서 해답 코드를 뜯어보았다.
서버를 켜놓고 npm start로 클라이언트를 실행하면 테스트를 진행할 수 있다. codecept를 잘 모르지만 과제를 fork할 때 이미 짜여 있던 테스트 파일을 열어보았다. click에서 자꾸 오류가 나서 공식 문서를 찾아봤다. I click 관련 내용이다. 뭔가 id, name 관련 문제인 듯 했다. 해당 아이템을 선택할 수 없는건가? 직접 '선택' 버튼을 클릭해보면 제대로 작동하는데 컴퓨터는 읽을 수 없었나보다.
오류 메시지는 Clickable element "{"name":"#짜장면"}" was not found by text|CSS|XPath 라고 나온다.
이 문제는 버튼 이름을 제대로 지정해 주는 방법으로 간단하게 해결되었다. 테스트에 쓰인 대로 #까지 꼭 넣어야 된다.
<button
key={menuItem.id}
type="button"
name={`#${menuItem.name}`}
onClick={() => {
onAddCart(menuItem);
}}
>
선택
</button>
일요일까지 끝냈어야 할 4주차 과제를 화요일 저녁이 다 되어 끝냈지만, 추후 과제들이 이전의 것을 바탕으로 진행되는 것 같아 꼼꼼히 하려고 했다. 트러블 슈팅 과정을 엄청나게 겪었는데 기록을 안 남기자니 그 과정에서 배운 내용과 시간이 아까워서 남겨보았다. 극한의 어려운 상황에 처하면 예술혼이 불타오른댔나.. 나는 개발자니까 개발 관련 글을 남긴다.
'개발 > React' 카테고리의 다른 글
[유데미] <클린코드 리액트(React)> 수강 후기 (0) | 2024.04.14 |
---|---|
[React] Context API 개념과 사용법, 학습하며 겪은 어려움 (0) | 2023.02.01 |
[트러블 슈팅] CRA 투두리스트 구현하기 (feat: 자주 실수하는 이슈) (0) | 2023.01.03 |
[트러블 슈팅] react router v6 오류 해결 (0) | 2022.12.25 |