일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 기술 낙서장
- MSW
- SW캡스톤디자인
- react-query&Next.js
- ClientSide
- state 관리
- 사내 오류 해결
- 일상 생각
- node.js
- 사내 오류 대응
- 리액트
- Next.js
- state 사용하기
- 캡스톤디자인 후기
- SSR
- javascript
- 더블엔씨
- react-query v5
- 사내 이슈
- nextjs
- React
- TypeScript
- react-query 도입후기
- 결제페이지
- no router instance found
- JS변수
- router instance
- react-query
- 기술낙서장
- React.JS
- Today
- Total
코딩을 잘하고 싶은 코린이 동토니
사내 결제 페이지 개편하기 본문
사내 결제 페이지 개편이유
사내 결제 페이지 (이하 payment) 프로젝트가 1차적으로 릴리즈되었을 당시 통일되지 않은 부분들이 굉장히 많았었습니다.
1. 기존의 payment 프로젝트가 억지로 끼워 맞춘 부분이 많았다.
예를들어 api호출을 할때 axios, react-query가 혼합되어 사용 되었다거나,
니콘머니 충전, 콘구매 페이지 까지의 처리방식과 실제 결제를 진행하는 페이지에서의 처리방식이 다르거나,
컨벤션이나 구조가 잡혀있지 않아서 각 파일마다 코드의 흐름이 다르다던가 등등 여러가지의 이유가 있었습니다.
2. 불필요한 컴포넌트 분리가 너무 많았다.
리팩토링을 진행한 가장 큰 이유였습니다.
컴포넌트가 굳이 분리가 되지 않아도 되는 부분에서 분리가 되어 있었고 오히려 분리가 되지 않아야 할 부분에서 분리가 되어있어,
결제수단 관련 컴포넌트의 경우 10개가 넘어가는 컴포넌트로 구성되어서 모든 히스토리를 알고있는 사람이 아니라면 이해하기가 굉장히 힘든 구조였습니다.
3. 불필요한 recoilState가 너무 많았다.
1번과 이어지는 이유로 기존 틀을 깨지 않고 이슈들을 해결하려다 보니 같은 depth에서 관리되어야 하는 state들이 서로 다른 depth에 존재해서 억지로 recoil을 사용하여 처리한 경우가 대다수였습니다.
실제로 리팩토링 전 recoil의 atom과 selector의 갯수는 약 30개가 넘어갔었습니다.
4. PG사 별 결제 처리 프로세스가 통일성있게 이루어지지 않았다
리팩토링 전에는 각 PG사를 추가할 때 마다 pages폴더 하위에 해당 PG사 이름의 페이지를 하나더 만들고 결제처리를 진행하였습니다.
이는 추후 PG사가 추가될 때 마다 불필요한 폴더와 파일들만 늘어날 뿐이였습니다.
리팩토링 진행 시 고려했던 점
1. 관심사 분리
프론트엔드에서 가장 중요하게 생각하는 부분중에 하나라고 생각합니다. View를 그려주는 컴포넌트와 해당 페이지에서 처리할 로직들을 서로 다른 컴포넌트에서 관리하여
1. 미래의 확장성이 용이하기 위해
2. 유지보수가 용이하기 위해 두가지 이유때문입니다.
예를들어 하나의 컴포넌트에서 100개의 데이터를 보여주어야 한다면 해당 컴포넌트에서 data를 불러오고 map을통해 화면을 그려줘야 할겁니다.
근데 여기서 누군가가 ‘100개의 데이터중에서 ~~한 옵션을 가진 데이터들은 ~~상황에 노출이 되면 안됩니다’라는 요청을 하게 된다면
하나의 컴포넌트에서 해당 조건에 부합하는 state를 만들고 data를 불러오고 state의 조건에 부합하는 data라면 필터링을 거치고 map을 돌리게 됩니다.
이러한 요청들이 1,2개 까지는 받아 들일 수 있지만 3~4개가 되는 요청들이 온다면?
혹은 추후에 기획이 변경되어 기존의 조건에서 다른 조건으로 변경하려한다면 작업자는 하나의 컴포넌트에 작성된 View 코드와 View를 위한 함수들까지 한번에 다 읽고 파악해야 할 겁니다.
사실 어떤 작업을 하더라도 진행하려는 작업의 코드는 전부 읽어야 하지만, 컴포넌트가 적절하게 용도에 따라 구분이 되어있다면 얘기가 좀 달라집니다.
View만 관리하는 컴포넌트가 있다면 디자인자체가 변경되지 않는이상 해당 컴포넌트는 작업자가 따로 건드리지 않아도 항상 똑같은 View를 보장해주게 되고, 작업자는 해당 View 컴포넌트를 그려주기 위한 Logic 컴포넌트만 작업을 하면 됩니다.
따라서 이런 이점을 payment에 적용해보고자 리팩토링 진행시 가장 많이 고려했습니다.
2. PG사 결제 처리 프로세스 통일
결제 처리는 정말 특이 케이스가 아닌 이상 항상 같은 틀안에서 움직이게 됩니다.
상품 선택 ⇒ 상품 구매요청 ⇒ 결제 진행 ⇒ 결제 완료 ⇒ 배송
제가 통일하고자 했던 부분은 결제 진행 전과 후 였습니다.
결제가 진행되는 부분은 결제처리의 주도권이 PG사에게 있어 저희가 핸들링 할 수 있는 방법은 없습니다. PG사의 요청에 맞게 필요한 property들을 넘겨주는게 끝이죠.
하지만 그 이전과 이후의 주도권은 저희에게 있습니다.
이전에는 각 PG사별로 pages 폴더 하위에 PG사별 이름을 딴 폴더를 생성하고 작업자의 입맛대로 PG사의 취소 혹은 완료 처리를 진행했었습니다.
이렇게 되면 각 PG사 연동을 진행한 사람이 아닌경우 정확한 히스토리를 파악하기 힘들게 됩니다.
예를들어 naverPay의 경우 naverPay/confirm, naverPay/cancel이 있다면 왜 confirm과 cancel이 따로 존재해야하는지 해당 작업자에게 물어보지 않는 이상 이유를 알 수 없습니다.
하지만 이런부분들을 next.js fs기반의 라우팅을 이용한다면 통일성있게 처리 할 수 있기에 이부분도 리팩토링시 고려대상이 되었습니다.
3. 불필요한 state 제거
위에서 말했듯 억지로 프로젝트를 구성하게 되며 불필요한 state들이 너무나 많았습니다.
특히 전역으로 관리되는 변수들이 많고 해당 변수들을 집합체로 모아 하나의 값으로 바꾸는 selector까지 있다보니 특정 조건을 처리해야될때 의도치 않은 side-effect가 일어나는경우가 많았습니다.
예를들어서 결제가 가능한 상태인지 아닌지를 판단하기 위한 orderable의 경우 spendNiconMoney, userPaymentEmail, userNiconMoney, spendReserveNiconMoney, finalPrice, selectedPaymentMethodKey 등이 엮여있습니다.
여기서 selectedPaymentMethodKey를 관리하는 로직을 수정했을 때 결제수단 선택 방식만 바꾸려던게 자칫하면 결제가 가능한 상태임에도 불구하고 결제가 불가능하게 될 수 도 있습니다.
리팩토링 진행 과정
1. 컴포넌트 폴더구조 변경
앞서 말한 관심사 분리를 위해서 가장 먼저 진행하게 되었습니다.
리팩토링 전 component 폴더의 구조를 살펴보면
위와같이 각 page마다 하나의 폴더를 소유(1:1)하는 형태의 구조였습니다.
그리고 해당되는 페이지의 컴포넌트들을 하나의 폴더에 다 넣어놓는 방식이였습니다. 전 이런 형태의 구조가 payment 프로젝트에는 어울리지 않는다 라고 생각했습니다.
payment 프로젝트는 다른 프로젝트들과 다르게 페이지가 4개(콘구매, 니콘머니충전, 결제진행중,결제완료)밖에 존재하지 않습니다.
하지만 내부에서 처리되어야할 로직들은 꽤 있었습니다.
콘 구매 페이지를 예로 들면 component/con-items 폴더하위에 콘 구매 관련 컴포넌트들이 전부 존재하게 됩니다.
또한 일반 결제수단(nice, tossPay, naverPay)은 콘 구매페이지에만 존재하기 때문에 component/con-items내부에 일반 결제수단과 관련된 컴포넌트들이 존재하게 됩니다. 그러면 작업자가 보게 되었을때
‘나는 결제수단 중에서 일반 결제수단을 수정을 진행해야 되고 component/public/paymentMethod가 존재하니까 여기를 보면 되겠지?’ 하고 접근할 가능성이 있습니다.
이 얘기는 코드 파악을 위해 사용하는 리소스에 불필요한 리소스가 포함이 될 수 있습니다.
또 하나의 예시로 유저가 구매하려는 콘 아이템의 정보의 디자인을 수정하려 하는데 component/con-items폴더 내부에 다른 컴포넌트들까지 있다보니 한눈에 콘아이템과 관련된 컴포넌트를 찾기가 어려워집니다.
1-1. 폴더구조 변경의 해결책
가장 먼저 기존 페이지와 컴포넌트간의 폴더가 1:1로 구성이되던 형태를 유지하되 컴포넌트 폴더가 하위 폴더를 여러개 가질 수 있는 구조로 변경하였습니다. (1:N)
즉 이런 구조를 가질 수 있게 됩니다.
페이지(1) : 컴포넌트 폴더 (1) : 컴포넌트 기능단위 폴더 (N)
따라서 리팩토링 버전에서는 다음과 같은 구조로 변경하였습니다.
변경점을 나열해보자면
- 기존 global 컴포넌트를 관리하던 public 폴더의 명칭을 common으로 변경
- next.js에서의 public폴더의 의미는 현재 저희가 사용하는 assets의 용도이므로 이를 명확하게 하기위해서 변경
- orders 컴포넌트의 common은 콘 구매, 니콘머니 충전페이지에서 공통적으로 사용되는 컴포넌트들을 관리하도록 변경 (ex. nicon-pay, paymentMethods)
- con-items에서 con관련 컴포넌트들만 모아놓은 con폴더 생성
이러한 구조 변경으로 기대하는 바는
- 니콘머니 충전, 콘 구매 페이지에서 공통적으로 사용되는 컴포넌트들을 관리 할 수 있다.
- 콘 구매 페이지에서만 사용되는 컴포넌트와 니콘머니 충전에서 사용되는 컴포넌트의 구분이 확실해진다.
- 콘 구매에서 뷰 단위의 폴더를 하나 더 생성하여 한 페이지에서의 서로다른 뷰를 관리하는 컴포넌트들의 구분이 확실해진다.
정도입니다.
2. pages 폴더 구조 변경
pages 폴더도 많이 바뀌게 되었습니다. 기존 payment 프로젝트의 pages 폴더구조를 살펴보면
기존 pages 폴더의 가장 큰 문제는 각 PG사 마다 페이지 폴더가 따로 생성이 되어있었다는 점입니다.
이렇게 되면 추후 연동해야하는 PG사가 늘어날 때 마다 폴더를 계속해서 생성해주어야 하는 문제가 생깁니다.
또한 결제 진행 전까지의 과정과 결제 취소 과정이 통일 되어있지않아 있어서, 각 PG사마다 서로 다른 라우터와 방법을 통해 핸들링 하게 됩니다.
2-1. Pages 폴더 구조 해결
따라서 저는 다음과 같이 구조를 변경하였습니다.
next.js의 가장 큰 장점중 하나인 라우팅 시스템을 이용해서 PG사별 이름을 모두 가질 수 있는 [PG]폴더를 생성하고,
각 PG사별 결제 진행전까지 핸들링할 relay/[type]폴더, PG사별 결제 요청 관리를 위한 [orderId] 폴더를 생성해서
결제 진행전, 결제 요청 중, 결제 진행 중 이벤트를 통합해서 하나의 페이지에서 PG사별 정보만 갈아끼우는 방식으로 진행하였습니다.
모든 PG사를 통한 결제는 PG사 이름에 따른 url로 넘어오게 되고
결제 중 발생되는 취소, 실패 등의 이벤트는 query-string으로 넘겨받아 관리할 수 있도록 변경하였습니다.
3. 주요 컴포넌트
결제 관련 프로젝트를 진행할 때 가장 중요하게 봐야하는 것은 구매 정보(총 결제 가격과 상품), 결제 수단, 결제 라고 생각합니다. 이 중 결제가격, 결제수단 관리의 컴포넌트들을 살펴보겠습니다.
결제 수단
- PaymentCategory
- PaymentMethodList
- PamentMethods
- NiconPay
- GeneralPay
결제수단 관리 컴포넌트들은 위와 같이 5개의 컴포넌트로 관리하게 됩니다.
해당 컴포넌트들을 리팩토링하면서 가장 중요하게 봤던 것은 기능과 View의 분리였습니다.
우선 해당 컴포넌트들이 어떻게 View를 그려주는지 그림으로 살펴보겠습니다.
- PaymentCategory는 ‘니콘페이’ , ‘다른 결제 수단’을 보여주는 컴포넌트입니다. 해당 카테고리를 선택했을 때, 각자 보여줘야 하는 결제수단들이 달라져야 합니다.
따라서 PaymentCategory는 해당 버튼을 클릭했을 때 내부 컴포넌트들을 보여줄 수 있도록 children이 포함되어 있습니다.
또한 카테고리 선택 함수까지 props로 내려받아 기능적인 부분은 일절 관여하지 않는 View 컴포넌트입니다. - PaymentMethodList는 PaymentCategory로 래핑되어 있는 컴포넌트입니다. 선택된 카테고리에 따라 니콘페이를 보여줄지, 다른 결제 수단들을 보여줄지 결정하는 View컴포넌트입니다.
- PaymentMethods는 결제수단의 가장 최상위 컴포넌트로 PaymentCategory와 PaymentMethodList를 포함한 컴포넌트입니다.
const PaymentMethods = () => {
...
return (
<PaymentCategory ...props>
<PaymentMethodList ...props/>
</PaymentCategory>
)
}
위와 같은 형태를 띄고 있으며 PaymentMethods는 카테고리 선택, 카테고리선택에 따른 니콘페이, 다른결제수단들을 보여줄지 결정해줍니다.
즉 이 컴포넌트는 View를 그려주기위한 로직 컴포넌트 입니다.
또한 결제수단에 직접적으로 영향을 미치는 로직은 useHandlePaymentMethods Hook을 통해 관리하며 컴포넌트 내부에서는 이벤트 핸들러들을 가지고 있습니다. 자세한 설명은 Hook부분에서 하도록 하겠습니다.
결제 금액 관련 컴포넌트
- SpendNiconMoney
- ReserverNiconMoney
- orderPriceTable
컴포넌트 설명
- SpendNiconMoney는 유저가 사용할 니콘머니를 관리하며,결제시 니콘머니 전액결제 여부 판단을 주요 목적으로 가진 컴포넌트입니다.
전액결제를 선택할 수 있는 케이스가 굉장히 복잡하여 해당 판단을 useCalculateSpendNiconMoney Hook을 통해 핸들러를 반환받아서 사용하게 됩니다. - ReserveNiconMoney는 적립니콘머니 상품에 한해 사용되게 됩니다. SpendNiconMoney와 비슷한 컴포넌트지만 적립니콘머니 사용여부만 판단하면 되므로 비교적 간단한 로직 컴포넌트 입니다.
- orderPriceTable은 상품가격, 니콘머니, 적립 니콘머니 정보들을 불러와 최종적으로 유저가 결제해야 할 금액을 그려주는 View 컴포넌트 입니다.
Hook
기존 다른 프로젝트의 hook들의 경우 재사용성을 더 많이 고려하여 사용했습니다. 하지만 Hook이 재사용성에 용이한것도 맞지만 기능분리의 성격도 굉장히 강합니다. 따라서 저는 다음과 같은 부분을 고려하여 리팩토링을 진행하였습니다.
- 컴포넌트 내부에서 중요한 기능 로직의 경우 Hook으로 분리
- 예외사항이 발생되더라도 해당하는 Hook에서만 수정되고 컴포넌트에선 별도의 수정이 일어나지 않도록 설계
각각의 Hook들이 전부 중요하지만 가장 중요하게 생각하는 Hook 두개만 살펴보겠습니다.
useHandlePaymentMethods
usehandlePaymentMethods는 카테고리에 따른 결제수단들을 반환시켜주는 Hook입니다.
코드를 살펴보자면
const useHandlePaymentMethods = () => {
const currentOrderPage = useRecoilValue<PageType>(currentOrderPageState)
const conItemPackages = useRecoilValue<ConItemPackage[]>(conItemPackagesState)
const paymentMethodsApiUrl =
currentOrderPage === 'con-items' && conItemPackages.length > 0
? `/payment-methods/con-items/${conItemPackages[0].conItemId}`
: '/payment-methods/nicon-money'
const setSelectedPaymentCategory = useSetRecoilState<PaymentCategoryType>(
selectedPaymentCategoryState,
)
const [paymentMethodList, setPaymentMethodList] =
useState<Nullable<PaymentMethodType>>(null)
const [isDisabledNiconPay, setIsDisabledNiconPay] = useState<boolean>(false)
const shouldShowNaverPay = () => {
if (isNaverPayVersion) {
return isOknamePaymentSelfAuthVersion || userSelfAuth
}
return false
}
const { data: paymentMethod, isSuccess } = useQuery(
paymentMethodsKey.byUrl(paymentMethodsApiUrl),
() => fetchPaymentMethods(paymentMethodsApiUrl),
)
useEffect(() => {
if (isSuccess) {
const updatedPaymentMethodList = shouldShowNaverPay()
? paymentMethod
: {
niconPayPaymentMethods: paymentMethod.niconPayPaymentMethods,
generalPaymentMethods: paymentMethod.generalPaymentMethods.filter(
(method) => method.key !== 'naverPay',
),
}
setPaymentMethodList(updatedPaymentMethodList)
setIsDisabledNiconPay(
!paymentMethod.niconPayPaymentMethods[0].niconPayMethodPayload.isAllowedAccount &&
!paymentMethod.niconPayPaymentMethods[0].niconPayMethodPayload.isAllowedCard,
)
}
}, [isSuccess, paymentMethod, isHanaPromotion])
useEffect(() => {
if (isDisabledNiconPay) {
setSelectedPaymentCategory('general-pay')
}
}, [isDisabledNiconPay])
if (!paymentMethodList) return { methods: null, isDisabledNiconPay: true }
export type MethodsType = {
[K in PaymentCategoryType]: K extends 'nicon-pay'
? NiconPayPaymentMethod[]
: GeneralPaymentMethod[]
}
const methods: MethodsType = {
'nicon-pay': paymentMethodList.niconPayPaymentMethods,
'general-pay': paymentMethodList.generalPaymentMethods,
} as const
return { methods, isDisabledNiconPay }
}
먼저 콘구매 혹은 니콘머니 충전에 결제가능한 결제수단을 호출하게 됩니다.
그 이후 paymentMethodList라는 state에 호출한 결제수단들을 카테고리에 맞게 set하게 됩니다.
마지막으로 paymentMethodList state를 통해 methods라는 객체에 해당 정보들을 다시 저장하게됩니다.
여기서 왜 다시 저장하지? 라는 의문을 가질 수도 있습니다.
하지만 제가 원했던건 View에 직접 그려지는 state는 되도록 수정하지 않도록 하는것이였습니다.
만약 paymentMethodList state자체를 return하게 되면 니콘페이가 결제 불가능한 상품에 대해서는 View만 그려주는 컴포넌트가 또다시 paymentMethodList에 접근하여 tossAccount와 tossCard가 disabled인지 판단해야 합니다.
하지만 paymentMethodList에 결제수단들을 저장해두고 methods라는 객체에 니콘페이 결제가 불가능한 상품인지 미리 담아두게 된다면 결제수단 관련 특정 조건이 생겼을 때 setPaymentMethodList에 대해서만 수정을 하면 됩니다.
즉 역할분담이 확실하게 이루어지죠
해당 hook의 사용은 다음과 같이 이루어집니다.
const PaymentMethods = () => {
const toast = useToast()
const { isDisabledNiconPay, methods } = useHandlePaymentMethods()
...
if (!methods) return <PaymentMethodSkeleton />
return (
<Wrapper>
<Title>결제 방식</Title>
<PaymentCategory
categories={categoryObject[orderPage]}
onClickPaymentCategory={onClickPaymentCategory}
>
{selectedPaymentMethodKey !== 'niconMoney' && (
<PaymentMethodList
methodsList={methods}
isDisabledNiconPay={isDisabledNiconPay}
/>
)}
</PaymentCategory>
</Wrapper>
)
}
export default PaymentMethods
이렇게 하면 선택한 카테고리에 따라 method가 선택이 되고 해당 카테고리에 매핑되는 결제수단들만 노출됩니다.
useHelpPGOrder
useHelpPGOrder Hook은 각 PG사 결제창을 호출할 수 있도록 도와주는 Hook입니다.
코드를 살펴보자면
type PgOrderType = 'nice' | 'toss' | 'tossPay' | 'naverPay' | 'niconMoney'
const useHelpPGOrder = (
pgName: PgOrderType,
router: NextRouter,
newOrder?: CheckNewOrderType,
): (() => void) | undefined => {
const isHanaPromotion = useRecoilValue<boolean>(isHanaPromotionState)
const isNativePortingVersion = useCompareVersion('5.0.0')
const methodId = useRecoilValue<Nullable<MethodId>>(niconPayMethodIdState)
const { userId } = useRecoilValue<User>(userInfoState)
const pgStartHandler = useCallback(() => {
if (newOrder) {
const {
finalPrice,
id: orderId,
paymentInfo: { paymentEmail },
} = newOrder.order
const orderName = getOrderName(newOrder.order)
const returnUrl = getReturnUrl(pgName)
const purchasedUrl = `/purchased/${orderId}`
if (!orderName) {
failOrder('주문이 잘못 되었습니다.')
return
}
switch (pgName) {
case 'niconMoney':
return () => {
requestOrder({ orderId, router, failOrder })
}
case 'nice':
return () => {
if (!returnUrl) {
return alert('결제수단이 없습니다.')
}
startNicePay({
finalPrice,
orderId,
orderName,
returnUrl,
paymentEmail,
isHana: isHanaPromotion,
failOrder,
})
}
case 'toss':
return () => {
if (!methodId) {
return alert('결제수단이 없습니다.')
}
startBrandPay({
finalPrice,
orderId,
orderName,
customerKey: String(userId),
methodId,
purchasedUrl,
router,
paymentEmail,
failOrder,
})
}
case 'tossPay':
return () => {
const tossPayCancelUrl = `${process.env.NEXT_PUBLIC_APP_URL}/tossPay/relay/cancel?orderId=${orderId}`
if (!returnUrl) {
return alert('결제수단이 없습니다.')
}
startTossPay({
finalPrice,
orderId,
orderName,
returnCancelUrl: tossPayCancelUrl,
returnUrl,
router,
failOrder,
})
}
case 'naverPay':
return () => {
openNaverPay({ router, order: newOrder.order })
}
}
}
}, [newOrder, pgName, router, methodId, userId])
const failOrder = useCallback(
(alertMessage?: string) => {
alert(alertMessage)
postMessage(isNativePortingVersion ? 'goBack' : 'close')
},
[isNativePortingVersion],
)
return pgStartHandler()
}
export default useHelpPGOrder
Hook의 parameter로 연결할 PG사 이름, 라우터 이동을 위한 router, PG사 연동을 위한 order 데이터를 받게됩니다.
해당 parameter들을 통해 각 PG사 넘겨줄 property들을 정리하고 해당 PG사를 실행시켜주는 함수를 반환합니다.
그러면 결제 요청 page에서는 다음과 같은 방법으로 결제 창을 호출 할 수 있게 됩니다.
//주문서 생성 체크
const { data: newOrder, isSuccess } = useQuery(
CheckOrderKey.checkOrder(Number(orderId), 'order'),
() => checkOrder(Number(orderId), 'order'),
{
enabled: !!orderId,
retry: (failureCount) => orderCheckPolling(failureCount),
retryDelay: 1000,
onError: (error: any) => {
handleCheckOrderError(error)
postMessage(isNativePortingVersion ? 'goBack' : 'close')
},
},
)
//결제창 호출 핸들러
const startPgOrder = useHelpPGOrder(pgName, router, newOrder)
//주문서 생성 체크 완료시 결제창 호출 핸들러 실행
useEffect(() => {
if (isSuccess && startPgOrder) {
startPgOrder()
}
}, [isSuccess])
- 내부에서 작성한 주문서를 체크하고 결제창 호출 여부를 판단하게 됩니다.
- 해당 데이터가 있다면 주문서를 토대로 useHelpPGOrder를 실행시킵니다.
- useHelpPGOrder가 반환한 핸들러가 정확한 시점에 실행될 수 있도록 useEffect를 통해 모든 체크가 완료된 이후 핸들러를 실행시킵니다.
이런식으로 관리하게 되면 다음에 다른 PG사를 연동하려 할때 굉장히 간편해집니다.
예를 들어 카카오페이를 추가해야된다면 page를 건드릴 필요 없이 hook과 카카오페이 sdk를 호출할 함수만 건드리면 됩니다.
type PgOrderType = ... | 'KaKaoPay' // 카카오 페이 추가
const useHelpPGOrder = (
pgName: PgOrderType,
router: NextRouter,
newOrder?: CheckNewOrderType,
): (() => void) | undefined => {
const isNativePortingVersion = useCompareVersion('5.0.0')
const methodId = useRecoilValue<Nullable<MethodId>>(niconPayMethodIdState)
const { userId } = useRecoilValue<User>(userInfoState)
const pgStartHandler = useCallback(() => {
if (newOrder) {
...
do something...
//kakaoPay 호출을 위한 property 전처리 진행
switch (pgName) {
...
case 'KaKaoPay' :
return () => {
kakaoPay handler ()
} //카카오페이 핸들러 추가
}
}
}, [newOrder, pgName, router, methodId, userId])
...
return pgStartHandler()
}
export default useHelpPGOrder
이렇게 하면 간단하게 카카오 페이를 연동할 수 있게 됩니다.
결제 처리 프로세스 통일
마지막으로 PG사별로 page가 분리되어 있던 걸 어떻게 통합 하였는지에 대해 적어보겠습니다.
니콘내콘 앱 결제 플로우는 다음과 같이 이루어집니다.
콘 옵션 선택시 해당 콘정보를 토대로 간단하게만 나타내주면
1차 주문서 작성 ⇒ 결제수단 선택 ⇒ 구매하기 버튼 클릭시 해당 콘 홀딩 ⇒ PG사 결제 완료후 api에서 주문서 비교 후 성공 시 sqs로 send 진행 ⇒ 콘 발송 및 api에서 payment로 redirect ⇒ 결제 완료
결제 처리 방식을 통일할 때 프론트에서 주시해야 할 부분은 구매하기 버튼 클릭 이후 부터입니다.
구매하기 버튼 클릭 이후 다음과 같은 코드로 결제창 연결페이지로 이동하게 됩니다.
const useHandleOrder = () => {
...
const handleOrderNiconMoneyComplete = (orderId: number) => {
if (selectedPaymentMethodKey === 'tossAccount') {
return router.replace(`/tossAccount/${orderId}`)
}
alert('허용되지 않은 결제수단입니다.')
postMessage(isNativePortingVersion ? 'goBack' : 'close')
}
const handleOrderConComplete = (orderId: number) => {
return router.replace(`/${selectedPaymentMethodKey}/${orderId}`)
}
const handleOrder = {
con: (orderId: number) => handleOrderConComplete(orderId),
niconMoney: (orderId: number) => handleOrderNiconMoneyComplete(orderId),
}
const { mutate: mutateConOrder } = useMutation(
() =>
postOrderConItem({
conItemPackages,
paymentMethodKey: selectedPaymentMethodKey as string,
paymentEmail: userPaymentEmail === '' ? null : userPaymentEmail,
finalPrice: orderFinalPrice,
spendNiconMoney,
spendReserveNiconMoney,
}),
{
onSuccess: async (orderId) => {
await handleOrder['con'](orderId)
},
},
)
const { mutate: mutateNiconMoneyOrder } = useMutation(
() =>
postOrderNiconMoney({
finalPrice: orderFinalPrice,
paymentEmail: userPaymentEmail === '' ? null : userPaymentEmail,
paymentMethodKey: selectedPaymentMethodKey as string,
sellingPrice: orderFinalPrice,
}),
{
onSuccess: async (orderId) => {
await handleOrder['niconMoney'](orderId)
},
},
)
}
구매하기 버튼을 클릭할 경우 콘 구매 or 니콘머니 충전인지 판단 후 주문서 체크를 위한 mutate를 진행하게 되고 주문서 체크 성공시 handleOrder['something'](orderId)를 실행시키게 됩니다.
그리고 handleOrder함수에서는 주문서 체크에 성공했으니 실제 PG사 호출 페이지로 router.replace를 시키게 됩니다.
여기서 중요한 점은 위에서 설명했듯 pages폴더 하위에 [PG] slug 폴더가 생성되어있습니다.
next.js의 dynamic routing을 통해서 해당 [PG] 라우터에는 PG사 이름으로 url을 생성 할 수 있습니다.
그럼 예시로 제가 콘을 토스페이로 선택하였고 해당 주문서 id가 1111이라면 구매하기 버튼 클릭 후 유저는 해당 url로 이동하게 됩니다.
<https://payments/**tossPay/1111**>
그럼 해당 [PG]/[orderId] 페이지에서는 결제창을 호출하고 결제창에서 결제가 완료될 때까지의 역할을 수행하게 됩니다.
그럼 만약 제가 취소를 한다면 어떻게 될까요
이부분은 payment-api도 함께 수정되어야 했습니다.
각 PG사 마다 결제취소를 진행한 경우 다시 redirect 시킬 url을 설정하도록 되어있습니다.
해당 url을 이런식으로 바꾸면 통일성 있게 관리가 가능합니다.
res.redirect('https://ncnc-payment/tossPay/relay/cancel')
res.redirect('https://ncnc-payment/tossPay/relay/close')
payment-api에서 PG사 결제 서버와 통신하며 취소가 발생했을시 항상 프론트에선 같은 페이지로 이동하게 됩니다. 그럼 프론트쪽에서는 다음과 같이 핸들링을 해주게 됩니다.
const PGRelay: NextPage = () => {
const router = useRouter()
const { type, PG, orderId } = router.query
useEffect(() => {
switch (PG) {
case 'tossPay':
if (type === 'close') {
alert(message)
postMessage(isNativePortingVersion ? 'goBack' : 'close')
return
}
if (type === 'cancel') {
if (orderId) {
cancelOrder(Number(orderId))
alert('결제가 취소되었습니다.')
postMessage(isNativePortingVersion ? 'goBack' : 'close')
}
}
break
...
return <></>
}
export default PGRelay
같은 PG사에서 여러 종류의 이벤트가 발생되더라도 각 이벤트에 맞게 제가 적절하게 이벤트를 처리할 수 있게됩니다.
그렇다면 결제를 취소하고 해당 결제 취소의 메세지를 같이 노출시켜줘야 하는 경우라면 어떻게 해야 할까요?
이도 역시 payment-api를 통해 핸들링이 가능합니다.
res.redirect(`tossPay/relay/cancel?message=${message}`)
이렇게 PG사 쪽에서 함께 전달해야되는 값을 query-string으로 담아서 보내게 되면 프론트에서 해당 query-string을 통해 적절하게 대응을 해주면 됩니다.
const PGRelay: NextPage = () => {
const router = useRouter()
const { ..., message } = router.query
useEffect(() => {
switch (PG) {
case 'tossPay':
if (type === 'close') {
alert(message)
postMessage(isNativePortingVersion ? 'goBack' : 'close')
return
}
...
return <></>
}
export default PGRelay
위와같이 결제 진행 처리 통일 문제를 url에 정보를 함께 담아 보내는 형식으로 리팩토링 하였습니다.
'기술낙서장' 카테고리의 다른 글
사내 스마트 스토어 옵션 가격 책정 오류 수정하기 (1) | 2024.09.03 |
---|---|
스마트 스토어 자동화 프로그램 429에러 처리하기 (0) | 2024.09.02 |
스마트 스토어 프로그램 health Check (1) | 2024.09.02 |
playwright로 네이버 스마트스토어 자동 로그인 구현하기 (1) | 2024.09.02 |