굿즈 스토어 프로젝트 09 - 상품 결제 페이지.

이유승·2023년 7월 22일
0

상품 상세 페이지에서 제품 옵션을 선택하고, 수량을 조절한 뒤 구매하기 버튼을 누르면 필요한 데이터와 함께 상품 결제 페이지로 이동된다. 이 데이터는 상세 페이지에서 결제 페이지로 이동하기만 하면 되므로 Redux 등을 사용하지 않았다.

  • 필요한 데이터?
    상품을 결제하는데 모든 데이터가 있을 필요는 없다. 상품 이름, 배송료 등의 상품 정보어떤 옵션을 몇 개 주문했는지 등의 구매 정보만이 필요하다. 상세 페이지에서 데이터를 가공하고 따로 배열에 저장하고 했던 것은 이렇게 필요한 데이터가 한 덩어리로 넘어갈 수 있도록 하기 위함이었다.



1. 결제는 어떻게 이루어지는가?

원래는 가상 결제 라이브러리 등을 가져와서 사용해볼까 했지만, 이것도 마냥 쉬운 일이 아닌데다가 결제 수단을 등록하고 하는 등 일이 복잡하기만 해서 이번 프로젝트에서는 그냥 회원가입 시 일정 포인트를 지급하고 구매시 차감되도록 하는 간단한 시스템만을 구현하였다.

필요한 데이터는 이미 상세 페이지에서 결제 페이지로 전달되었다. 여기서 해줄 것은 단지 화면에 출력해주기만 하면 된다. 상품 데이터 이외에 구매자의 주소와 보유 포인트 정보를 조회해서 와야하는데, 로그인 상태를 유지하고 확인하는 기능은 이미 구현되어 있다. Redux Store에서 관리하는 유저값을 가지고 와서 DB에서 조회하여 필요한 데이터를 가져오면 된다.

  • 원래는 신규 배송지 입력 시스템을 구현하려고 했는데, 어차피 주소 검색 API를 그대로 사용하는 것인데다 이미 회원쪽 기능과 DB를 수정하지 않고서든 1회성으로 새 주소를 입력하는 것 뿐이다보니 구현하지 않았다. 다음 프로젝트에서는 꼭 구현해볼 것이다.

결제 기능의 동작 순서

결제 버튼을 클릭하게 되면, 우선 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),
            }
        );
    };

결제가 정상적으로 이루어지면 위와 같은 결제 완료 컴포넌트가 출력된다.



2. 결제 내역 및 포인트 내역.

위에서 언급했듯이, 제품을 구매했을 때 결제 기록과 포인트 사용 기록이 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' });
            }
    	}
    };
    

-> 데이터 유효성 검사, 코드 중복성 최소화, 너무 긴 코드가 한번에 작성되어 가독성이 떨어짐. 이미 앞에서도 여러 번 지적된 문제이다. 프로젝트를 진행했던 당시에는 몰랐고 나중에 코드를 리뷰하면서 알아차린 것들이라.. 나중에 전체적으로 리팩토링을 진행할 예정이다.

profile
프론트엔드 개발자를 준비하고 있습니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기