
최근 사이드프로젝트에서 구독 서비스를 개발하면서 결제 수단 등록 기능을 구현했다.
단순해 보이는 기능이지만, 실제로는 본인인증, 결제 수단별 차별화 로직, 사용자 경험 등을 고려해야 하는 복잡한 시스템이었다.
이번 글에서는 Next.js + TypeScript 환경에서 PortOne(구 아임포트)을 활용해 구현한 useRegisterPaymentMethod 훅의 개발 과정을 공유하고자 한다.
참고로 본인인증 및 결제수단 등록 등 전체적인 방법은 🚩포트원 공식문서 여기를 보면 된다.
interface RegisterPaymentMethodParams {
paymentMethod: PaymentMethod; // 'CARD' | 'KAKAO_PAY'
billingParams: BillingRequestParams;
isFromMyPage?: boolean; // 마이페이지 호출 여부
}
isFromMyPage 플래그를 통해 호출 컨텍스트를 구분하여 리다이렉트 URL을 다르게 처리할 수 있게 했다.
이 훅은 마이페이지에서 뿐만 아니라 결제 페이지에서도 사용될 수 있기 때문이다.
본인인증 -> 서버 검증 -> 빌링키 생성 -> 빌링키 정보 반환으로 플로우가 진행되는데
카카오페이는 본인인증을 하지 않으므로 건너뛰었다.
// 1. 본인인증 (카드결제인 경우만)
if (paymentMethod === 'CARD') {
const identityRes = await requestIdentityVerification(isFromMyPage);
if (!identityRes) {
throw new Error('본인인증에 실패하였습니다');
}
identityVerificationId = identityRes.identityVerificationId;
}
// 카카오페이는 본인인증 건너뜀
...
// 2. 서버 검증(카드결제인 경우만)
if (paymentMethod === 'CARD') {
verifyData = await verifyPayment({
...
});
}
// 3. 빌링키 생성
const billingKeyRes = await requestBillingKey(paymentMethod, {
...billingParams,
...(verifyData && {
customer: {
customerId:...,
fullName: ...,
phoneNumber: ...,
email: ...,
},
}),
});
// 4. 성공 시 빌링키 정보 반환!!
return { ... }
카드 결제: 본인인증 → 서버 검증 → 빌링키 생성
카카오페이: 본인인증 건너뜀 → 빌링키 생성
빌링키(Billing Key)란?
고객의 카드번호, 유효기간 등 민감한 결제 정보를 암호화하여 만든 고유 식별 값으로, 구독 서비스나 자동 결제 시 매번 정보를 입력하지 않고도 가맹점이 고객의 동의 하에 안전하게 재결제할 수 있도록 하는 '결제용 암호화 키'입니다.
우리는 KG이니시스 통합본인인증 방법을 사용했고, 방법은 포트원 개발자센터에 자세히 나와있다.
const billingKeyMutation = useMutation({
mutationFn: async ({ paymentMethod, billingParams, isFromMyPage }) => {
setIsProcessing(true);
try {
// 결제 수단 등록 로직
} finally {
setIsProcessing(false);
}
},
onSuccess: (data) => {
const methodText = data.paymentMethod === 'CARD' ? '카드' : '카카오페이';
openAlert('alert', { message: `${methodText} 결제 수단이 성공적으로 등록되었습니다.` });
},
onError: (error: Error) => {
openAlert('error', { message: error.message || '결제 수단 등록에 실패했습니다.' });
},
});
React Query의 useMutation을 활용해 비동기 처리와 로딩 상태를 자동으로 관리했따.
export const PaymentMethod = () => {
const { registerPaymentMethod } = useRegisterPaymentMethod();
const handleAddCard = () => {
registerPaymentMethod({
paymentMethod: 'CARD',
billingParams: {
finalPrice: 0,
planName: '카드 등록',
},
isFromMyPage: true,
});
};
return (
<TextButton
size='sm'
label='추가하기'
iconLeft={<Plus />}
onClick={handleAddCard}
/>
);
};
UI 컴포넌트에서는 단순히 registerPaymentMethod 함수를 호출하기만 하면 됐다.
모바일에서 본인인증을 진행할 때 다음과 같은 플로우가 발생하는데,
1. 사용자가 결제 수단 등록 시작
2. 본인인증 페이지로 리다이렉트 (외부 앱 또는 웹뷰)
3. 인증 완료 후 우리 서비스로 다시 돌아옴
4. 이때 기존 결제 진행 상황이 초기화되는 문제
제가 처음 구현한 useRegisterPaymentMethod에서는 이 재개 로직이 완벽하게 처리되지 않았다.
이 부분을 고려하지 못한 내 잘못이다..
// 초기 구현 - 재개 로직 부족
const billingKeyMutation = useMutation({
mutationFn: async ({ paymentMethod, billingParams, isFromMyPage }) => {
// 본인인증은 처리했지만, 리다이렉트 후 재개 로직이 부족
if (paymentMethod === 'CARD') {
const identityRes = await requestIdentityVerification(isFromMyPage);
// ...
}
}
});
후에 팀원이 리팩토링하면서 이 문제를 해결하기 위해 전용 리다이렉트 핸들러를 도입했고,
성공적으로 결제 수단 등록 로직이 진행될 수 있었다!
export const PaymentRedirectHandler = () => {
const pathname = usePathname();
// 현재 URL이 /mypage로 시작하는지 체크하여 isFromMyPage 전달
const isFromMyPage = pathname.startsWith('/mypage');
usePaymentResume({ isFromMyPage });
return null;
};
이 컴포넌트는 다음과 같은 역할을 담당한다!
usePaymentResume 훅을 통해 중단된 결제 프로세스 자동 재개이를 통해 모바일 환경에서도 끊김 없는 결제 경험을 제공할 수 있게 되었음.