- Payment Experience implementing an SDK as a reusable shared React component -
1. Background of Implementing a Shared SDK Component
The business logic that had to be handled on the actual payment screen involved much more than simply calling the SDK.
-
Setting customer keys for members and non-members
-
Reflecting changes to the payment amount in real time
-
Creating and integrating payment data with the backend
-
Supporting inline and popup payment methods simultaneously
-
Handling redirects on payment success and failure
-
Updating the backend status when a payment error occurs
-
Applying logic to prevent duplicate payment requests
-
Supporting the React Native WebView environment
Implementing this logic separately on each screen would not only increase code duplication, but also make it very difficult to maintain consistency in payment processing rules. For example, fragmentation issues could arise where one screen records SDK errors in the backend while another simply outputs them to the console.
We also had to consider that each service required a different payment UI. Since payment terms and payment method configurations varied, and the position and design of the payment button differed from one service to another, it was not practically possible to include them in a fixed form inside the shared component.
Therefore, we designed a structure in which the shared component was responsible for SDK integration and payment state management, while each screen could flexibly determine when to execute the actual payment.
2. Expected Benefits
The goals we aimed to achieve by implementing the shared component were as follows.
First, we minimized the coupling between the screens and the SDK. Individual screens were designed to pass only essential data, such as the order number, order name, amount, and redirect URL, without needing to know about the SDK's complex initialization process.
Second, we ensured consistency in payment processing rules. By handling the entire process—from creating payment data through final approval and saving the failure status—in the shared area, we sought to prevent errors caused by differences in behavior between screens.
Third, we integrated inline and popup methods through a single interface. Users only need to select the payment method as an option, and during execution they can enjoy a consistent development experience by calling the same executePayment() method.
Fourth, we improved maintenance efficiency. When SDK options change or error-handling policies are revised, we can respond quickly by updating only the shared component in one place instead of modifying every screen individually.
3. Implementation Approach
3.1 Providing an External Payment Execution Interface
The UI composition of the payment button was left to the discretion of each service screen, while the shared component needed to expose only the payment execution logic through its interface. To achieve this, we used React's `forwardRef` and `useImperativeHandle`.
export interface RequestPaymentContainerRef {
executePayment: () => Promise<void>;
}
useImperativeHandle(ref, () => ({
executePayment: async () => {
if (!innerPaymentRef.current) {
throw new Error('Payment component is not ready.');
}
await innerPaymentRef.current.executePayment();
},
}));
◈ A parent screen that uses the component can easily trigger a payment through the exposed `ref`.
const paymentRef = useRef<RequestPaymentContainerRef>(null)
const handlePayment = async () => {
await paymentRef.current?.executePayment();
};
◈ In particular, executePayment was provided in the form of Promise<void> so that the caller could clearly recognize execution failures. If it was called while the payment component was not ready, it threw an exception, prompting the parent screen to provide appropriate guidance.
3.2 Separating Inline and Popup Payments
Inline payments render the payment methods and terms directly on the page, whereas popup payments open the payment window and create the data when executePayment() is called.
Managing these different mechanisms in a single component would make it highly complex. Therefore, we divided the roles into `RequestPayment` and `RequestPaymentPopup` and configured the parent container to use them selectively.
Although their internal implementations differ, we maintained the same externally exposed interface to achieve polymorphism.
3.3 Configuring Success and Failure Redirects
The completion of payment in the SDK payment window does not mean that all business logic has been completed. It is essential to pass information such as `paymentKey`, received from the redirected URL, to the backend approval API for final confirmation.
The overall process proceeds as follows.
-
Register the preliminary payment information with the backend and receive a unique payment ID.
-
Specify the issued ID as `orderId` and request payment through the SDK.
-
After the payment gateway's payment window has processed the payment, navigate to the designated success or failure URL.
-
On success, call the backend's final approval API.
-
On failure, update the payment status in the backend to failed.
-
After all post-processing is complete, navigate to the final destination screen.
To support this, we designed intermediate bridge paths called `success-redirect` and `fail-redirect`. By completing the business logic at this stage, we strictly distinguished 'payment gateway payment success' from 'completion of internal data processing' and improved data consistency.
4. Considerations During Implementation
4.1 Initialization and Payment Availability
The fact that an SDK instance has been created does not mean that payment is immediately available. With the inline method, the rendering of the payment method and terms UI must be fully completed.
Therefore, we managed the SDK object, widgets, and the actual rendering completion state, ready, separately and strictly. To prevent payment-related incidents, we applied defensive measures such as disabling the button until rendering was complete.
In addition, because the SDK's internal state must also be synchronized when the amount passed through props changes, we included logic to appropriately call widgets.setAmount() again.
4.2 Preventing Duplicate Approval Requests
The approval API is called when the success redirect screen is mounted, but the same request may be made multiple times due to actions such as refreshing or navigating back.
It is difficult to block this completely through frontend state management alone. Therefore, it is essential to design the system to guarantee idempotency at the backend and payment gateway API levels, taking into account network retries and multi-tab environments.
4.3 URL and Routing Environments
Because common components are used in a variety of routing environments, it is important to normalize `basePath` correctly. When appending query parameters, the payment flow can only remain uninterrupted with careful handling, such as choosing the correct delimiter (`?` or `&`) and encoding error messages.
Although this may seem simple at first glance, in embedded environments or complex path structures, these basic operations become critical factors that determine whether a payment succeeds.
4.4 Popup and WebView Environments
Because the popup method registers event listeners at runtime, duplicate listener registration caused by repeated button clicks must be prevented. We thoroughly implemented state management during execution and cleanup logic on unmount.
In particular, we supported `appScheme` injection for React Native WebView environments. By avoiding hardcoding and enabling the environment to be configured dynamically through application settings, we took extensibility into consideration.
5. Conclusion
Implementing a simple payment window with a payment SDK was not difficult. However, the real challenge was applying the complex workflow—from initialization through backend state management—consistently across the entire organization.
Through this project, we maximized usability by breaking payment functionality down by role and providing an encapsulated interface through `forwardRef`.
After introducing common components, the cost and human errors involved in adding payment functionality to new services were noticeably reduced, while the maintainability of the system as a whole was also improved.
Of course, technical completeness does not stop at the frontend. I believe a truly reliable payment system is achieved only when backend idempotency handling and state reconciliation logic work together.
It was a valuable experience that confirmed reusable components go beyond eliminating code duplication: they isolate the complexity of external libraries and deliver consistent business value across the entire service.
chnsik