-
[Flutter] 토스페이먼츠를 활용해 주문·결제 프로세스 적용해보기 feat. 승인 프로세스 + 주문 #3Flutter/project 2024. 2. 22. 01:06
이전 글에서 계속 이어집니다. 링크를 남겨둘테니 필요하시면 참고해주세요.
[Flutter] 토스페이먼츠 결제모듈을 도입하다 #1 설계
결제 시스템을 도입할때, 모든 PG사를 하나하나 연결하는 것은 생각만해도 머리가 아픕니다. 이럴때 결제 플랫폼을 사용하면 여러 PG사와 간편결제 등 한번의 연동으로 쉽게 도입이 가능하죠. 연
nomal-dev.tistory.com
[Flutter] 토스페이먼츠를 활용해 주문 · 결제 프로세스 적용해보기 feat. 인증 프로세스 #2
토스페이먼츠의 장점은 직접 UI를 제작하고 API를 통해 연동하는 방법뿐만아니라 Widget과 결제에 필요한 메서드를 제공해 비교적 수월하게 연동이 가능합니다. 물론 직접 UI를 제작하면 UI/UX 디자
nomal-dev.tistory.com
✓ 결제 승인 & 주문
클라이언트에서 서버에게 결제요청(실 결제)시 보내줘야하는 값은 서버가 가지고있지않으면서 승인요청에 필요한 것들입니다.
- 서버가 미리 가지고 있는 값
- Secret key
- 클라이언트가 서버에게 전해줘야하는 값
- paymentKey
- 인증 요청에 성공하면 받게 되는 값
- orderId
- 가주문테이블 생성시 서버로부터 받았던 값
- amount
- 주문금액: 클라이언트에서 이미 알고있으며 결제방법UI를 렌더링할 때도 사용되는 값
- paymentKey
클라이언트에서 서버에게 결제요청을 하는 순간부터는 인증요청이 모두 마친 뒤기때문에 토스페이먼츠의 결제 위젯을 벗어나게됩니다.
그리고 결제요청을 받은 서버는 토스페이먼츠로부터 결제 승인을 받기위한 통신이 이루어지고 클라이언트는 결제에대한 결과를 기다리게되죠. 이때 유저에게 결제가 진행중이라는 것을 알려주기위해 "결제 진행중 화면"을 보여줘야합니다.
주문도 마찬가지로 진행중에는 "결제 진행중" 화면으로 보여주게됩니다.
📌 결제 및 주문 Controller Code
@Riverpod(keepAlive: true) class PurchaseController extends _$PurchaseController { @override Purchase build() { return const PurchaseLoading(); } payAndOrder() async { await _serverPayment(); if (state is PurchaseTossClientError || state is PurchaseTossAppServerError) { ref.read(routerProvider).goNamed(FailOrderScreen.routeName); } await _order(); if (state is PurchaseOrderError) { ref.read(routerProvider).goNamed(FailOrderScreen.routeName); } ref.read(routerProvider).goNamed(SuccessOrderScreen.routeName); } _serverPayment() async { // 가주문 테이블 존재 여부 체크 final orderTableState = ref.watch(temporaryAndPaymentControllerProvider); if (orderTableState.couponAmount == null || orderTableState.orderName == null || orderTableState.pointAmount == null || orderTableState.totalAmount == null) { state = const PurchaseTossClientError(); return; } // 클라이언트 토스 결제 에러 발생유무 한번 더 체크 final clientPaymentState = ref.watch(clientPaymentControllerProvider); // 토스 자체 에러 if (clientPaymentState.fail != null) { state = const PurchaseTossClientError(); return; } // 토스 앱서버 결제 시도 final appServerTossState = await ref .read(paymentServiceProvider) .startServerPayment( model: PaymentDataModel( amount: clientPaymentState.success!.amount!, orderId: clientPaymentState.success!.tossOrderId!, paymentKey: clientPaymentState.success!.paymentKey!)); // 서버 에러가 뜬다면? 에러화면으로 넘기고 함수는 종료된다 if (appServerTossState is PurchaseTossAppServerError) { state = appServerTossState; return; } appServerTossState as PurchaseTossAppServerSuccess; state = appServerTossState; // success 화면 컴포넌트 구성 await ref .read(orderSuccessControllerProvider.notifier) .updateSuccessOrderInfo( paymentTime: appServerTossState.model!.approvedAt, amount: appServerTossState.model!.amount, orderNumber: appServerTossState.model!.orderId, representativeProduct: appServerTossState.model!.orderName); // success 화면 컴포넌트 구성 await ref.read(orderSuccessControllerProvider.notifier).updateStoreInfo(); } // 📌 주문 _order() async { // 주문할 메뉴 목록 저장 await ref.read(orderControllerProvider.notifier).orderPreparation(); final orderState = ref.watch(orderControllerProvider); // 주문 결제 시작 final orderResult = await ref .read(orderServiceProvider) .startTransaction(model: orderState); if (orderResult is OrderError) { state = const PurchaseOrderError(); return; } } }
⭐️ _serverPayment()
먼저 서버측에 결제 요청을하는 메서드부터 살펴보겠습니다.
// 가주문 테이블 존재 여부 체크 final orderTableState = ref.watch(temporaryAndPaymentControllerProvider); if (orderTableState.couponAmount == null || orderTableState.orderName == null || orderTableState.pointAmount == null || orderTableState.totalAmount == null) { state = const PurchaseTossClientError(); return; } // 클라이언트 토스 결제 에러 발생유무 한번 더 체크 final clientPaymentState = ref.watch(clientPaymentControllerProvider); // 토스 자체 에러 if (clientPaymentState.fail != null) { state = const PurchaseTossClientError(); return; }
- API요청을하기전에 마지막으로 클라이언트측에 문제가 없는지 확인함과 동시에 결제요청API에 필요한 파라미터를 불러오는 과정
// 토스 앱서버 결제 시도 final appServerTossState = await ref .read(paymentServiceProvider) .startServerPayment( model: PaymentDataModel( amount: clientPaymentState.success!.amount!, orderId: clientPaymentState.success!.tossOrderId!, paymentKey: clientPaymentState.success!.paymentKey!));
- 서버에게 결제요청을 하는 부분
// 서버 에러가 뜬다면? 에러화면으로 넘기고 함수는 종료된다 if (appServerTossState is PurchaseTossAppServerError) { state = appServerTossState; return; } appServerTossState as PurchaseTossAppServerSuccess; state = appServerTossState; // success 화면 컴포넌트 구성 await ref .read(orderSuccessControllerProvider.notifier) .updateSuccessOrderInfo( amount: appServerTossState.model!.amount, orderNumber: appServerTossState.model!.orderId, representativeProduct: appServerTossState.model!.orderName); // success 화면 컴포넌트 구성 await ref.read(orderSuccessControllerProvider.notifier).updateStoreInfo(); }
- 서버에서 승인 결과를 알려주는 부분
🤷🏻♂️ "success 화면 컴포넌트 구성이라는게 뭔가요?"
OrderSuccessScreen, OrderSuccessController [Project] 맛있는 외식의 시작, '미리'
✓ What Project? "지역 소상공인 식음료매장들의 당일 재고 상품을 파격적인 할인가로 제공하는 O2O 커머스 서비스" 코로나로 인해 전국의 학교들이 비대면수업을 하면서 지역 경제가 침체되던 때
nomal-dev.tistory.com
'미리'앱에서는 결제완료 화면을 구성할때 매장명, 주문상품, 결제일시, 주문번호, 결제금액을 보여주게 되어있습니다.
이때 주문상품, 결제일시, 주문번호, 결제금액은 승인이 성공했을때 서버가 클라이언트에게 반환해 주는 값이 사용됩니다.
- 나머지 결제완료 화면 구성 데이터 출처
- 매장명: cartController(장바구니)
⭐️ _order()
// 📌 주문 _order() async { // 주문할 메뉴 목록 저장 await ref.read(orderControllerProvider.notifier).orderPreparation(); final orderState = ref.watch(orderControllerProvider); // 주문 시작 final orderResult = await ref .read(orderServiceProvider) .startTransaction(model: orderState); if (orderResult is OrderError) { state = const PurchaseOrderError(); return; } }
- 주문에 필요한 파라미터를 가져온 후, 주문 API를 실행하는 부분
주문에 필요한 파라미터를 구성하는 것은 서버의 요구에 맞게 작성하면 됩니다. 이부분은 서비스마다 다르니 참고만 해주세요!
아래 코드는 저희 서버측 요구사항에 맞게 주문 API의 파라미터를 구성한 메서드입니다.@Riverpod(keepAlive: true) class OrderController extends _$OrderController { // 주문서 작성! @override OrderModel build() { return OrderModel(); } // 📌 요청사항 watchUserRequest({required String value}) async { state = state.copyWith(orderRequest: value); } // 📌 주문 준비 orderPreparation() async { // 주문서 유저 정보 컨트롤러 final userInfo = ref.read(paymentFormControllerProvider) as OrderUserInfoData; // 가주문 컨트롤러 final orderTempState = ref.watch(temporaryAndPaymentControllerProvider); // 서버 결제 컨트롤러 final purchaseState = ref.read(purchaseControllerProvider) as PurchaseTossAppServerSuccess; // 할인 금액 컨트롤러 final amountState = ref.watch(orderAmountControllerProvider); // 쿠폰 가져오기 final coupon = ref .read(choiceCouponControllerProvider.notifier) .getCurrentCouponInfo(); // 장바구니 컨트롤러 final cartState = ref.watch(userCartControllerProvider) as CartData; List<CartMenu> tempCartMenus = []; for (var targetCartMenu in cartState.mapper!.cartMenu!) { CartMenu cartMenu = CartMenu(cartMenuId: targetCartMenu.cartMenuId, cartMenuOptions: []); if (targetCartMenu.menuDetailOption.isNotEmpty) { for (var option in targetCartMenu.menuDetailOption) { cartMenu.cartMenuOptions .add(CartMenuOption(cartMenuOptionsId: option.cartMenuOptionId!)); } tempCartMenus.add(cartMenu); } } // 요청사항을 제외한 모든 값들을 채워 넣는다 state = state.copyWith( // 주문서 유저 정보 컨트롤러 userNick: userInfo.model!.userNick, userTel: userInfo.model!.userTel, // 가주문 컨트롤러 representativeMenuName: orderTempState.orderName, storeId: cartState.mapper!.storeId, // 서버 결제 컨트롤러 orderAmount: purchaseState.model!.amount, paymentKey: purchaseState.model!.paymentKey, paymentMethod: purchaseState.model!.paymentMethod, orderNumber: purchaseState.model!.orderId, orderMenuCount: cartState.mapper!.cartMenu!.length, // 활성화 쿠폰 가져오기 userMereCouponId: coupon?.type == CouponType.mere ? coupon!.couponId : null, userStoreCouponId: coupon?.type == CouponType.store ? coupon!.couponId : null, // 할인금액 컨트롤러 usePointAmount: amountState.usedPoint, useCouponAmount: amountState.couponPrice, // 장바구니 컨트롤러 cartMenus: tempCartMenus, ); } }
📌 실제 동작 모습
어려울 것같 았던 주문 결제 프로세스를 막상 구현해보니 생각보다 수월하게 진행되어서 PG사의 위대함(?)을 알게된 순간이였습니다. 사실 어떤 기능을 만들 때, 구현보다는 설계부분이 더 오래걸리고 생각할게 많다라는 것을 개발하는 분들이라면 다들 공감하실겁니다.
설계가 더 오래걸린다는 것은 그저 포트폴리오를 위해 "나 이런것도 해봤다?"에 초점을 맞추기보단 도메인 서비스에대한 이해를 바탕으로,
"요구사항에 맞게 필요한 기술을 적절히 활용하여 프로젝트에 잘 녹여낼 수 있다"가 더 중요하다는 것을 말하는 것이기도 합니다.
결제시스템은 개발 측면에서 에러처리, 정산 등이 더 존재하지만 이슈가 생겼을 시 회사의 대처 프로세스도 매우 중요합니다. 그렇기때문에 기능을 설계할 때 정산팀, 재무팀, 기획팀 등 다양한 파트의 사람들과 적극적인 소통을 통해 요구사항을 명확히 파악하는 능력이 구현 능력만큼 중요하다고 생각합니다.
긴 글 읽어주셔서 감사합니다.
이 글이 꼭 정답은 아닙니다. 잘못된 부분이나 부족한 부분을 알려주시면 학습 후 수정하겠습니다.
'Flutter > project' 카테고리의 다른 글
[Flutter] 토스페이먼츠를 활용해 주문·결제 프로세스 적용해보기 feat. 인증 프로세스 #2 (0) 2024.02.21 [Flutter] 토스페이먼츠 결제모듈을 도입하다 #1 설계 (2) 2024.02.21 [Flutter] 클라이언트의 이미지 처리 전략 두가지 feat. AWS amplify, re-sizing (4) 2024.02.20 [Flutter] API 요청 횟수를 줄여 네트워크 대역폭을 절약해보자! feat. 배민 북마크 버그 (0) 2024.02.14 [Flutter] 무한스크롤 성능 최적화를 해보자 feat. Lazy Loading, Throttle #3 (0) 2024.02.13 - 서버가 미리 가지고 있는 값