상품 상세 페이지에서 제품 옵션을 선택하고, 수량을 조절한 뒤 구매하기 버튼을 누르면 필요한 데이터와 함께 상품 결제 페이지로 이동된다. 이 데이터는 상세 페이지에서 결제 페이지로 이동하기만 하면 되므로 Redux 등을 사용하지 않았다.
원래는 가상 결제 라이브러리 등을 가져와서 사용해볼까 했지만, 이것도 마냥 쉬운 일이 아닌데다가 결제 수단을 등록하고 하는 등 일이 복잡하기만 해서 이번 프로젝트에서는 그냥 회원가입 시 일정 포인트를 지급하고 구매시 차감되도록 하는 간단한 시스템만을 구현하였다.
필요한 데이터는 이미 상세 페이지에서 결제 페이지로 전달되었다. 여기서 해줄 것은 단지 화면에 출력해주기만 하면 된다. 상품 데이터 이외에 구매자의 주소와 보유 포인트 정보를 조회해서 와야하는데, 로그인 상태를 유지하고 확인하는 기능은 이미 구현되어 있다. Redux Store에서 관리하는 유저값을 가지고 와서 DB에서 조회하여 필요한 데이터를 가져오면 된다.
결제 버튼을 클릭하게 되면, 우선 DB에 구매 기록을 저장하게 된다.
const addRecord = async () => {
const querys = query(purchaseRecordCollectionRef);
const allPurchaseRecordCount = await getCountFromServer(querys);
const docRef = doc(purchaseRecordCollectionRef, `${allPurchaseRecordCount.data().count + 1}`);
const createdTime = timeStamp.fromDate(new Date());
await setDoc(docRef,
{
purchaseNumber: allPurchaseRecordCount.data().count + 1,
userName: userData.email,
address: userData.address,
address2: userData.address2,
date: createdTime,
purchaseData: purchaseData,
productData: productData,
isDelete: false,
}
);
};
다음에는 유저 DB에 있는 포인트 수치를 갱신한다. 기존에 보유한 포인트가 구입하려는 상품의 가격보다 낮을 경우에는 에러를 발생시키고, 프론트단에서 경고창과 포인트 충전 모달창을 출력하게 된다.
let beforePoint = 0;
// 유저 데이터의 포인트 값을 수정.
const updataUserInfo = async () => {
const docRef = doc(userCollectionRef, userData.email);
const docSnap = await getDoc(docRef);
beforePoint = parseInt(docSnap.data().point);
if (beforePoint < purchaseData.totalAmount) {
throw errorCode.storeError.InsufficientPoint;
};
await setDoc(docRef, {
point: beforePoint - parseInt(purchaseData.totalAmount),
}, { merge: true });
};
포인트가 부족하면 위와 같은 에러 모달창이 출력되고, 확인 버튼을 클릭하면 아래와 같은 포인트 충전 모달창이 출력된다.
여기서 입력한 수치만큼 포인트가 충전되며, 충전을 마친 다음에는 제품을 다시 구매하면 된다.
포인트가 충분한 경우에는 다음 단계로 이동한다. 상품의 재고량과 판매량 수치를 갱신해준다.
// 제품의 재고량과 판매량을 수정.
const updataProductInfo = async () => {
const docRef = doc(storeCollectionRef, productData[0].name);
const docSnap = await getDoc(docRef);
let option1Sales = 0;
let option2Sales = 0;
let option3Sales = 0;
let option4Sales = 0;
let option5Sales = 0;
// eslint-disable-next-line
purchaseData.purchaseList.map((item) => {
if (item.optionNumber === 'option1') {
option1Sales = item.purchaseQuantity;
};
if (item.optionNumber === 'option2') {
option2Sales = item.purchaseQuantity;
};
if (item.optionNumber === 'option3') {
option3Sales = item.purchaseQuantity;
};
if (item.optionNumber === 'option4') {
option4Sales = item.purchaseQuantity;
};
if (item.optionNumber === 'option5') {
option5Sales = item.purchaseQuantity;
};
});
await setDoc(docRef, {
productOptionInventory: {
option1: parseInt(docSnap.data().productOptionInventory.option1 - option1Sales),
option2: parseInt(docSnap.data().productOptionInventory.option2 - option2Sales),
option3: parseInt(docSnap.data().productOptionInventory.option3 - option3Sales),
option4: parseInt(docSnap.data().productOptionInventory.option4 - option4Sales),
option5: parseInt(docSnap.data().productOptionInventory.option5 - option5Sales),
},
productOptionSalesRate: {
option1: parseInt(docSnap.data().productOptionSalesRate.option1 + option1Sales),
option2: parseInt(docSnap.data().productOptionSalesRate.option2 + option2Sales),
option3: parseInt(docSnap.data().productOptionSalesRate.option3 + option3Sales),
option4: parseInt(docSnap.data().productOptionSalesRate.option4 + option4Sales),
option5: parseInt(docSnap.data().productOptionSalesRate.option5 + option5Sales),
},
}, { merge: true });
await setDoc(docRef, {
productSalesRate : parseInt(docSnap.data().productSalesRate) + parseInt(purchaseData.totalQuantity),
}, { merge: true });
};
마지막으로 포인트 사용 기록을 DB에 저장한다.
const recordPointData = async () => {
const querys = query(pointRecordCollectionRef);
const count = await getCountFromServer(querys);
const docRef = doc(pointRecordCollectionRef, `${count.data().count + 1}`);
const createdTime = timeStamp.fromDate(new Date());
await setDoc(docRef,
{
recordNumber: count.data().count + 1,
userEmail: userData.email,
recordType: '-',
pointChangeNumber: parseInt(purchaseData.totalAmount),
recordDesc: '포인트 사용(굿즈 스토어 물건 구매).',
recordDate: createdTime,
leftoverPoint: beforePoint - parseInt(purchaseData.totalAmount),
}
);
};
결제가 정상적으로 이루어지면 위와 같은 결제 완료 컴포넌트가 출력된다.
위에서 언급했듯이, 제품을 구매했을 때 결제 기록과 포인트 사용 기록이 DB에 따로 저장된다. 이후 포스트에서 따로 정리하겠지만..
위와 같이 스토어 마이페이지에서 내역을 확인할 수 있다.
평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.
-> 비동기 처리 방법이 불안정함. 여러 비동기 함수들이 중첩되어 실행되고 있어 콜백 지옥(callback hell)과 같은 복잡성을 초래할 수 있다. 코드의 가독성과 유지 보수성을 높이기 위해 비동기 처리에 async/await을 사용할 필요가 있다.
프로젝트를 진행했을 당시만 해도 위와 같은 코딩 방식이 문제는 있다고 생각하지만 어떻게 개선할 수 있을지 다른 방법을 생각하진 못했다. 나중에 가서 이게 문제가 있는 방법이고 이렇게 하면 안된다는걸 깨달았다..
아래는 코드를 개선할 수 있는 방법의 예시.
try {
// 구매 기록을 저장.
const addRecord = async () => {
// ...
};
// 유저 데이터의 포인트 값을 수정.
const updateUserInfo = async () => {
// ...
};
// 제품의 재고량과 판매량을 수정.
const updateProductInfo = async () => {
// ...
};
const recordPointData = async () => {
// ...
};
await updateUserInfo();
await addRecord();
await recordPointData();
await updateProductInfo();
dispatch({ type: 'STORE_COMPLETE' });
}
catch (error) {
dispatch({ type: 'STORE_ERROR', payload: createErrorData(error) });
if (error.code === errorCode.storeError.InsufficientPoint) {
dispatch({ type: 'STORE_NOTENOUGH_POINT' });
}
}
};
-> 데이터 유효성 검사, 코드 중복성 최소화, 너무 긴 코드가 한번에 작성되어 가독성이 떨어짐. 이미 앞에서도 여러 번 지적된 문제이다. 프로젝트를 진행했던 당시에는 몰랐고 나중에 코드를 리뷰하면서 알아차린 것들이라.. 나중에 전체적으로 리팩토링을 진행할 예정이다.
감사합니다. 이런 정보를 나눠주셔서 좋아요.