새로운 Apple 가이드라인에 따른 Capacitor 앱 내 Stripe 결제 링크 구현하기
2025년 5월 1일부로, Apple은 다음 판결에 따라 App Store Review Guidelines에 중대한 변경 사항을 적용했습니다. Epic v. Apple 사건. 이번 변경으로 미국 내 앱 개발자는 디지털 상품 및 서비스에 대해 외부 결제 수단으로 연결되는 링크를 사용할 수 있게 되었으며, 이는 Apple의 인앱 결제 시스템에 대한 대안을 제공합니다.
모바일 결제의 역사를 바꾼 Epic과의 거대한 전쟁
이 순간에 이르기까지의 과정은 길고도 치열했습니다. 시작은 2020년 8월이었습니다. 인기 게임 Fortnite의 제작사인 Epic Games가 Apple의 30% 수수료를 우회하는 직접 결제 옵션을 도입하며 Apple App Store 가이드라인을 의도적으로 위반했습니다. 이에 Apple은 즉각 App Store에서 Fortnite를 삭제했고, Epic은 iOS 앱 배포 및 인앱 결제에 대한 Apple의 통제권에 이의를 제기하며 소송을 제기했습니다.
수년간의 법정 공방과 항소, 재항소가 이어진 끝에, 법원은 마침내 Apple이 개발자가 앱 외부의 대체 결제 수단으로 사용자를 안내할 수 있도록 허용해야 한다는 판결을 내렸습니다. 이 결정은 2008년 출범 이후 동일한 수익 모델로 운영되어 온 App Store 생태계의 경제 구조를 근본적으로 변화시킵니다.
최종 판결 - 더 이상의 항소는 없다
이번 판결이 특히 중요한 이유는 더 이상 항소할 수 없는 최종 판결이라는 점입니다. 2025년 초 대법원이 Apple의 상고를 기각하면서 하급심의 결정이 확정되었습니다. 이는 개발자들이 Apple이 추가적인 법적 대응으로 이 결정을 뒤집을 수 없다는 확신을 가지고 외부 결제 수단을 구현할 수 있음을 의미합니다.
법으로 보장된 평등한 대우
가장 중요한 점은, 이번 판결이 외부 결제 수단을 사용하는 앱에 대해 Apple이 차별을 할 수 없음을 명시했다는 것입니다. 법원은 특히 Apple이 다음과 같은 행위를 하는 것을 금지했습니다:
- 외부 결제 수단을 사용하는 앱에 추가 수수료를 부과하거나 과도한 요구 사항을 강요하는 행위
- Apple의 IAP 시스템만 사용하는 앱을 검색 결과에서 우대하거나 상단에 노출하는 행위
- 기술적 조치를 통해 외부 결제 경험을 Apple 자체 시스템보다 열등하게 만드는 행위
- 기본적인 소비자 정보를 넘어선 과도한 정보 공개 요구를 강요하는 행위
이러한 명시적인 보호 조치 덕분에 개발자는 Apple의 미묘한 보복이나 차별에 대한 두려움 없이 Stripe 또는 기타 외부 결제 제공업체를 구현할 수 있습니다. 이제 법적으로 공정한 경쟁 환경이 조성되었으며, Apple은 결제 방식 선택과 관계없이 모든 앱을 동등하게 대우해야 합니다.
이번 판결은 Apple의 폐쇄적인 생태계(walled garden) 방식에 대한 가장 중대한 도전 중 하나이며, 모바일 앱 수익화 방식의 중대한 전환점을 시사합니다. Apple의 30% 수수료(소규모 비즈니스는 15%)에 대해 오랫동안 불만을 제기해 온 개발자들에게 이번 판결은 더 높은 수익률과 고객 경험에 대한 더 큰 통제권을 제공하는 길이 될 것입니다.
Apple 인앱 결제 대신 Stripe를 사용할 때의 재무적 이점
이러한 변화가 개발자에게 미치는 재무적 영향은 상당합니다:
-
결제 처리 수수료 절감Apple은 일반적으로 인앱 결제 시 30%의 수수료(소규모 비즈니스는 15%)를 부과하지만, Stripe의 수수료는 거래당 약 2.9% + $0.30에 불과합니다. 이 차이는 수익 마진을 크게 높일 수 있습니다.
-
더 빠른 정산: Apple의 경우 정산까지 보통 45~90일이 소요됩니다. 반면 Stripe는 영업일 기준 2~3일 이내에 결제 대금을 은행 계좌로 입금해 줍니다.
-
간소화된 환불 프로세스: Apple의 복잡한 환불 시스템을 거칠 필요 없이 Stripe dashboard에서 직접 환불을 처리할 수 있습니다.
이러한 비용 절감과 현금 흐름 개선은 특히 소규모 개발자와 기업에게 큰 전환점이 될 수 있습니다.
이 글에서는 Apple의 최신 가이드라인을 준수하면서, Capacitor 앱에 Stripe Payment Links를 구현하여 이러한 새로운 규칙을 활용하는 방법을 알아봅니다. 업데이트된 가이드라인.
이 구현 방식은 Stripe의 Payment Links 공식 문서를 기반으로 하며, Capacitor 앱에 맞게 최적화되었습니다.
새로운 가이드라인 이해하기
업데이트된 App Store Review Guidelines에 따라, 이제 개발자는 디지털 상품 및 구독 서비스에 한해 사용자를 외부 웹사이트로 안내하여 결제를 진행할 수 있습니다. 이 변경 사항은 현재 미국 App Store를 통해 배포되는 앱에만 적용됩니다.
주요 핵심 사항:
- 이제 앱 내 디지털 상품에 외부 결제 옵션을 연결할 수 있습니다.
- 미국 App Store의 앱에만 적용됩니다.
- Apple의 공개 요구 사항을 계속 준수해야 합니다.
- 모든 고객 지원과 환불 처리는 계속 개발자의 책임입니다.
Capacitor 앱에 Stripe 결제 링크 설정하기
기술적인 구현 방법을 살펴보겠습니다:
1단계: Stripe Dashboard에서 결제 링크 생성
먼저 Stripe Dashboard에서 결제 링크를 생성하세요:
- Stripe Dashboard의 Payment Links 섹션으로 이동하세요.
- “+ New”를 클릭해 새 결제 링크를 만드세요.
- 상품 또는 구독 상세 정보를 설정하세요.
- “After payment” 설정에서 “Don’t show confirmation page”를 선택하세요.
- Universal Link를 성공 URL로 설정하세요 (나중에 설정합니다)
- “Create Link”를 클릭해 결제 링크를 생성하세요
2단계: Capacitor 앱에 Universal Links 설정하기
결제 완료 후 사용자를 앱으로 다시 리다이렉트하려면 Universal Links를 설정하세요:
- 다음 파일을 생성합니다:
apple-app-site-association도메인에 있는 파일:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["YOURTEAMID.com.yourdomain.yourapp"],
"components": [
{
"/": "/checkout_redirect*",
"comment": "Matches any URL whose path starts with /checkout_redirect"
}
]
}
]
}
}
-
이 파일을 다음 위치에 호스팅하세요:
https://yourdomain.com/.well-known/apple-app-site-association -
올바른 MIME 타입으로 제공되는지 확인하세요
application/json -
적절한 entitlement를 추가해 Capacitor 앱이 Universal Links를 처리하도록 구성하세요. 먼저,
capacitor.config.ts:
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
// Your existing app configuration (appId, appName, etc.)
plugins: {
Geolocation: {
// Request precise location access on iOS
iosLocationAccuracy: 'reduced'
}
}
};
export default config;
- Xcode 프로젝트에 Associated Domains entitlement를 추가하세요:
- Xcode 프로젝트 열기
- 앱 타겟 선택
- “Signing & Capabilities”로 이동하세요
- “+ Capability”를 클릭하고 “Associated Domains”를 선택하세요
- 추가
applinks:yourdomain.com
3단계: Fallback 페이지 생성
앱이 설치되어 있지 않은 경우를 처리할 수 있도록 리다이렉트 URL에 fallback 페이지를 생성하세요:
<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0;url=https://yourdomain.com/app-download">
</head>
<body>
<p>Redirecting to download page...</p>
</body>
</html>
4단계: Capacitor 앱에 결제 버튼 구현하기
이제 앱에 결제 버튼을 추가하세요:
import { Capacitor } from '@capacitor/core';
export async function openPaymentLink(userEmail, userId) {
// Use your actual Stripe payment link
const baseUrl = 'https://buy.stripe.com/your_payment_link';
// Add URL parameters to customize the experience
const params = new URLSearchParams({
prefilled_email: encodeURIComponent(userEmail),
client_reference_id: userId
});
const fullUrl = `${baseUrl}?${params.toString()}`;
// Simple window.open works in both web and Capacitor
// Using _blank opens in Safari on iOS which is important for users with saved Stripe Link credentials
window.open(fullUrl, '_blank');
}
Safari가 중요한 이유: 인앱 브라우저 대신 Safari를 통해 결제 링크를 열면(
window.open) Stripe Link에 결제 정보를 저장해둔 사용자의 경우 인증 정보가 자동으로 불러와집니다. 이를 통해 사용자가 신용카드 정보를 다시 입력할 필요가 없는 매끄러운 결제 경험을 제공하며, 결제 과정의 번거로움과 이탈률을 크게 낮출 수 있습니다.
5단계: 앱에서 Universal Links 처리하기
사용자가 다시 리다이렉트될 때 Universal Links를 처리할 수 있도록 앱을 설정하세요:
- 먼저, App 플러그인을 설치하세요:
npm install @capacitor/app
- 앱에 App 플러그인을 등록하세요:
import { App } from '@capacitor/app';
// In your initialization code
App.addListener('appUrlOpen', (event) => {
// Example URL: https://yourdomain.com/checkout_redirect?session_id=cs_test_...
const url = new URL(event.url);
if (url.pathname.startsWith('/checkout_redirect')) {
// Extract any parameters you need
const params = new URLSearchParams(url.search);
const sessionId = params.get('session_id');
// Handle successful payment
if (sessionId) {
// Verify the payment on your server if needed
verifyPayment(sessionId);
// Update UI to reflect successful purchase
updatePurchaseStatus(true);
}
}
});
async function verifyPayment(sessionId) {
// Call your backend to verify the payment
// This is optional if you're relying on webhooks
}
function updatePurchaseStatus(success) {
// Update your app UI to reflect purchase status
}
6단계: 주문 처리를 위한 Webhook 설정
마지막으로, 결제 성공을 처리할 서버의 webhook을 구성하세요:
// Using Express.js as an example
const express = require('express');
const stripe = require('stripe')('sk_test_your_stripe_secret_key');
const app = express();
// Use raw body parser for webhook signature verification
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = 'whsec_your_webhook_secret';
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the checkout.session.completed event
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Retrieve client_reference_id (your user ID)
const userId = session.client_reference_id;
// Grant access to the purchased content
await grantAccess(userId, session.id);
}
res.status(200).send();
});
async function grantAccess(userId, sessionId) {
// Your logic to grant access to the purchased content
// This could be updating a database, sending a notification, etc.
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Android 호환성
분명히 말씀드리자면, Epic 대 Apple 판결은 모바일 결제 환경을 근본적으로 변화시켰습니다. 이는 iOS 앱에 직접적인 영향을 미칠 뿐만 아니라, 외부 결제 수단을 사용해 온 Android 개발자들의 입지를 더욱 강화합니다.
이제 Android 개발자들은 외부 결제 솔루션을 완전히 안심하고 구현할 수 있습니다. Apple 판결로 세워진 선례는 모든 플랫폼의 개발자들이 향후 발생할 수 있는 제한으로부터 보호받을 수 있게 합니다. 이번 법원 판결은 많은 Android 개발자들이 수년간 해온 방식, 즉 더 낮은 수수료의 대안 결제 옵션을 제공하는 행위에 정당성을 부여했습니다.
Google Play Store는 항상 Apple보다 외부 결제 방식에 대해 덜 제한적이었으며, 이제 법적 선례까지 마련되었으므로 Android 앱에 Stripe 또는 기타 외부 결제 제공업체를 도입하는 데 실질적인 리스크는 거의 없습니다. 법적 근거가 확실하므로 안심하고 구현을 진행하셔도 됩니다.
앞서 살펴본 iOS 구현 방식은 Android 기기에서도 거의 동일하게 작동합니다. Google Play Store는 외부 결제 방식에 대해 동일한 제한을 두지 않으므로, 별도의 고지 다이얼로그 없이도 동일한 Stripe Payment Links 방식을 사용할 수 있습니다.
딥 링크(iOS의 universal links와 동일)를 처리하려면 다음 작업이 필요합니다:
- 앱 링크(App Links) 설정하기
AndroidManifest.xml리다이렉트 URL 처리를 위해 - 프로젝트 루트 디렉토리에
.well-known/assetlinks.json도메인 파일에 앱 상세 정보를 포함하여 - 페이지네이션된 목록과 동일한
appUrlOpen결제 성공 시 실행될 리스너 로직 구현
Capacitor의 장점은 플랫폼별 설정을 마친 후에는 실제 결제 흐름인 code이 양쪽 플랫폼 모두 동일하게 유지된다는 점입니다.
결제 UI 만들기
Capacitor 앱에 바로 추가할 수 있는 Vue 기반 결제 버튼 컴포넌트 예시입니다:
<template>
<div class="payment-container">
<div class="pricing-card">
<h2 class="mb-4 text-xl font-bold">{{ product.name }}</h2>
<p class="mb-6 text-gray-600">{{ product.description }}</p>
<div class="mb-6 price-tag">
<span class="text-2xl font-bold">${{ product.price }}</span>
<span v-if="product.isSubscription" class="text-sm text-gray-500">/month</span>
</div>
<button
@click="handlePayment"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
Purchase Now
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Dialog } from '@capacitor/dialog';
const props = defineProps({
product: {
type: Object,
required: true
},
userEmail: {
type: String,
default: ''
},
userId: {
type: String,
required: true
}
});
const isLoading = ref(false);
async function showExternalPaymentDisclosure() {
const { value } = await Dialog.confirm({
title: 'Leaving App for Payment',
message: 'You are about to leave this app to make a payment. Apple is not responsible for the privacy or security of payments that are not made through the App Store. All payment-related issues, including refunds, must be handled by our support team.',
okButtonTitle: 'Continue',
cancelButtonTitle: 'Cancel'
});
return value;
}
async function openPaymentLink() {
// Use your actual Stripe payment link
const baseUrl = 'https://buy.stripe.com/your_payment_link';
// Add URL parameters to customize the experience
const params = new URLSearchParams({
prefilled_email: encodeURIComponent(props.userEmail),
client_reference_id: props.userId
});
const fullUrl = `${baseUrl}?${params.toString()}`;
// Simple window.open works in both web and Capacitor
// Using _blank opens in Safari on iOS which is important for users with saved Stripe Link credentials
window.open(fullUrl, '_blank');
}
async function handlePayment() {
isLoading.value = true;
try {
// Only show the disclosure on iOS
if (window.Capacitor?.getPlatform() === 'ios') {
const userConfirmed = await showExternalPaymentDisclosure();
if (!userConfirmed) return;
}
await openPaymentLink();
} catch (error) {
console.error('Payment error:', error);
await Dialog.alert({
title: 'Payment Error',
message: 'There was an error initiating the payment. Please try again.'
});
} finally {
isLoading.value = false;
}
}
</script>
지역별 대응 방법
새로운 Apple 가이드라인은 미국 App Store 앱에만 적용되므로, 사용자 지역을 감지하여 적절한 결제 수단을 적용하는 전략이 필요합니다. IP 지오로케이션을 활용한 더 확실한 방법은 다음과 같습니다:
import { Capacitor } from '@capacitor/core';
async function determinePaymentMethod() {
// Always use Stripe for Android
if (Capacitor.getPlatform() !== 'ios') {
return 'external';
}
try {
// Use a geolocation service to determine user's country
const response = await fetch('https://ipapi.co/json/');
const locationData = await response.json();
// Check if the user is in the United States
if (locationData.country_code === 'US') {
return 'external'; // Can use Stripe Payment Links
} else {
return 'iap'; // Must use In-App Purchases
}
} catch (error) {
console.error('Error detecting region:', error);
return 'iap'; // Default to IAP to be safe
}
}
export async function processPayment(product, userEmail, userId) {
const paymentMethod = await determinePaymentMethod();
if (paymentMethod === 'external') {
// Use Stripe Payment Links
await initiateExternalPayment(userEmail, userId);
} else {
// Use Apple's In-App Purchase
await initiateInAppPurchase(product.appleProductId);
}
}
이 방식은 무료 ipapi.co 사용자의 IP 주소를 기반으로 국가를 판별하는 서비스입니다. MaxMind와 같은 다른 지오로케이션 서비스를 사용하거나, 보안을 강화하기 위해 서버 측에서 이 확인 과정을 구현할 수도 있습니다.
참고: 이 방식이 작동은 하지만, IP 지오로케이션이 항상 100% 정확하지는 않다는 점을 유의해야 합니다. 미션 크리티컬한 애플리케이션이라면 여러 탐지 방법을 병용하거나 사용자가 직접 지역을 선택할 수 있도록 하는 것을 고려하세요.
Capacitor 플러그인을 활용한 더 정확한 위치 탐지
더 정확한 위치 탐지를 위해, Capacitor Geolocation 플러그인을 @capgo/capacitor-nativegeocoder와 함께 사용하여 사용자의 국가를 더 정밀하게 판별할 수 있습니다:
- 먼저, 필요한 플러그인을 설치하세요:
npm install @capacitor/geolocation @capgo/capacitor-nativegeocoder
- Capacitor 프로젝트에서 플러그인을 설정합니다. 다음 내용을 추가하세요:
capacitor.config.ts:
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
// Your existing app configuration (appId, appName, etc.)
plugins: {
Geolocation: {
// Request precise location access on iOS
iosLocationAccuracy: 'reduced'
}
}
};
export default config;
- 위치 기반 지역 탐지 구현하기:
import { Capacitor } from '@capacitor/core';
import { Geolocation } from '@capacitor/geolocation';
import { NativeGeocoder } from '@capgo/capacitor-nativegeocoder';
async function isUserInUSA() {
try {
// Request permission first
const permissionStatus = await Geolocation.requestPermissions();
if (permissionStatus.location === 'granted') {
// Get current position
const position = await Geolocation.getCurrentPosition({
timeout: 10000,
enableHighAccuracy: false
});
// Use NativeGeocoder to reverse geocode the coordinates
const results = await NativeGeocoder.reverseGeocode({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
useLocale: true,
maxResults: 1
});
if (results.addresses.length > 0) {
// Check if the user is in the USA
return results.addresses[0].countryCode === 'US';
}
}
// If we couldn't determine location or permission denied, fall back to IP detection
return await isUserInUSAByIP();
} catch (error) {
console.error('Error detecting location:', error);
// Fall back to IP detection on error
return await isUserInUSAByIP();
}
}
async function isUserInUSAByIP() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
return data.country_code === 'US';
} catch (error) {
console.error('Error detecting IP location:', error);
return false; // Default to false to be safe
}
}
export async function determinePaymentMethod() {
// Always use Stripe for Android
if (Capacitor.getPlatform() !== 'ios') {
return 'external';
}
// Check if user is in the USA
const isUSA = await isUserInUSA();
return isUSA ? 'external' : 'iap';
}
export async function processPayment(product, userEmail, userId) {
const paymentMethod = await determinePaymentMethod();
if (paymentMethod === 'external') {
// Use Stripe Payment Links
await initiateExternalPayment(userEmail, userId);
} else {
// Use Apple's In-App Purchase
await initiateInAppPurchase(product.appleProductId);
}
}
이 구현 방식은 사용자가 실제로 미국에 있는지 확인하는 더 정확한 방법을 제공합니다. 먼저 기기의 GPS와 native geocoder를 사용하여 국가를 판별하려고 시도합니다. 권한 문제나 기타 오류로 인해 실패할 경우, IP 기반 탐지로 전환(fallback)합니다.
필요한 권한을 다음 파일에 추가하는 것을 잊지 마세요: info.plist (iOS) 및 AndroidManifest.xml (Android) 파일:
iOS용 (ios/App/App/Info.plist):
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to determine which payment method to use based on regional availability.</string>
Android용 (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
이 방식을 사용하면 새로운 Apple 가이드라인에 따라 사용자가 외부 결제 옵션을 이용할 수 있는지 가장 정확하게 판단할 수 있습니다.
구독 관리
결제에 Stripe를 사용하면 구독 서비스를 제공하고 관리할 수 있다는 큰 장점이 있습니다. Capacitor 앱에서 구독을 관리하는 방법은 다음과 같습니다:
1. 구독 관리 페이지 만들기
사용자의 활성 구독 상태를 보여줄 구독 관리 페이지를 앱에 추가하세요:
<template>
<div class="subscription-manager">
<div v-if="isLoading" class="loading-indicator">
Loading subscription data...
</div>
<div v-else-if="subscription" class="subscription-info">
<h2 class="mb-4 text-xl font-bold">Your Subscription</h2>
<div class="mb-6 plan-details">
<p><span class="font-medium">Plan:</span> {{ subscription.planName }}</p>
<p><span class="font-medium">Status:</span> {{ subscription.status }}</p>
<p><span class="font-medium">Renews:</span> {{ formatDate(subscription.currentPeriodEnd) }}</p>
</div>
<button
@click="manageSubscription"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
Manage Subscription
</button>
</div>
<div v-else class="no-subscription">
<p class="mb-4">You don't have an active subscription.</p>
<button
@click="goToPricingPage"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
View Plans
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getUserSubscription } from '../services/subscription';
const subscription = ref(null);
const isLoading = ref(true);
onMounted(async () => {
try {
const userData = await getUserSubscription();
subscription.value = userData.subscription;
} catch (error) {
console.error('Failed to load subscription:', error);
} finally {
isLoading.value = false;
}
});
function formatDate(timestamp) {
return new Date(timestamp * 1000).toLocaleDateString();
}
function manageSubscription() {
// Open Stripe Customer Portal
window.open(subscription.value.portalUrl, '_blank');
}
function goToPricingPage() {
// Navigate to pricing page
// router.push('/pricing');
}
</script>
2. 구독 관리를 위한 Customer Portal
Stripe는 사용자가 직접 구독을 관리할 수 있는 Customer Portal을 제공합니다. 서버에서 이 포털로 연결되는 링크를 생성할 수 있습니다:
// Server-side code (Node.js)
const stripe = require('stripe')('sk_your_stripe_secret_key');
async function createPortalSession(customerId) {
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://yourdomain.com/account',
});
return session.url;
}
App Store 규정 준수 확인
구현 방식이 Apple 가이드라인을 준수하도록 하려면 다음 사항을 확인하세요:
- 외부 결제에 대한 적절한 고지 사항 포함
- Apple 가이드라인에 따라 앱을 이탈함을 알리는 모달 시트 구현
- 앱 내 결제 시 Apple의 수수료 정책을 우회하려 하지 마세요
- 해당 거래에 대해 Apple은 책임이 없음을 사용자에게 명확히 안내하세요
필수 고지 모달을 구현하는 예시입니다:
import { Dialog } from '@capacitor/dialog';
async function showExternalPaymentDisclosure() {
const { value } = await Dialog.confirm({
title: 'Leaving App for Payment',
message: 'You are about to leave this app to make a payment. Apple is not responsible for the privacy or security of payments that are not made through the App Store. All payment-related issues, including refunds, must be handled by our support team.',
okButtonTitle: 'Continue',
cancelButtonTitle: 'Cancel'
});
return value;
}
export async function initiateExternalPayment(userEmail, userId) {
const userConfirmed = await showExternalPaymentDisclosure();
if (userConfirmed) {
await openPaymentLink(userEmail, userId);
}
}
구현 사항 테스트하기
구현 내용을 테스트하려면:
- 앱에서 결제 버튼을 클릭하여 고지 창이 뜨고 Stripe 결제 페이지로 이동하는지 확인하세요
- Stripe 테스트 카드를 사용하여 테스트 결제를 완료하세요
4242 4242 4242 4242 - 결제 후 유니버설 링크를 통해 앱으로 다시 리다이렉트되어야 합니다
- webhook이 다음을 수신했는지 확인하세요:
checkout.session.completedevent
마치며
iOS 앱에서 디지털 상품에 외부 결제 수단을 사용할 수 있게 된 것은 개발자에게 더 큰 유연성을 제공하는 중요한 변화입니다. 현재 이 변화는 미국 App Store 앱에만 적용되지만, Apple의 인앱 결제 시스템을 대체할 중요한 대안이 됩니다.
Capacitor와 함께 Stripe Payment Links를 사용하면 Apple의 가이드라인을 준수하면서도 간결한 체크아웃 경험을 빠르게 구현할 수 있습니다. Apple의 인앱 결제 시스템이 여전히 적용되는 미국 외 지역의 사용자의 경우, @capgo/native-purchases StoreKit 및 Google Play Billing을 직접 구현해야 합니다. 이 방식은 Apple의 인앱 결제 시스템과 비교했을 때 Stripe의 강력한 결제 인프라, 낮은 수수료(30% 대비 3%), 그리고 훨씬 빠른 정산(수개월이 아닌 수일 내)이라는 장점도 제공합니다.
이러한 트랜잭션은 Apple의 생태계 외부에서 발생하므로, 모든 고객 지원 및 환불 문제를 직접 처리해야 한다는 점을 유의하세요.
여러분의 Capacitor 앱에 Stripe Payment Links를 구현해 보셨나요? 아래 댓글로 경험을 공유해 주세요!
자주 묻는 질문
Q: 이 방식이 Apple의 가이드라인을 준수하나요?
A: 네, 2025년 5월 1일부로 Apple은 미국 App Store에서 배포되는 앱의 디지털 상품 및 서비스에 대해 필수 고지 사항을 포함한다는 조건하에 외부 결제 수단으로의 링크 연결을 허용합니다.
Q: 외부 결제 수단을 사용할 때도 Apple에 수수료를 지불해야 하나요?
A: 아니요, 새로운 규정의 주요 이점 중 하나는 Apple 시스템 외부에서 처리되는 결제에는 수수료가 부과되지 않는다는 점입니다.
Q: 이 새로운 규정의 혜택을 받으려면 회사가 미국에 기반을 두어야 하나요?
A: 아니요, 앱이 US App Store에서 제공되고 구매를 진행하는 사용자가 미국에 있다면 전 세계 어느 기업이든 외부 결제 수단을 도입할 수 있습니다. 이 판결은 회사 위치가 아니라 마켓플레이스(US App Store)와 사용자 위치에 적용됩니다. 즉, 유럽, 아시아, 남미 등 어디에 있든 개발자는 미국 고객을 위해 Stripe Payment Links를 구현할 수 있습니다.
Q: 미국 이외의 지역에 있는 사용자가 외부 결제 옵션을 사용하려고 하면 어떻게 되나요?
A: (본문에서 설명하는 것처럼) 지역 감지 기능을 구현해 미국 사용자에게만 외부 결제 옵션을 제공해야 합니다. 다른 지역에서는 Apple의 인앱 결제 시스템을 계속 사용해야 합니다.
Q: 앱 외부에서 소비되는 실물 상품이나 서비스에도 이 방식을 사용할 수 있나요?
A: 네, Apple은 앱 외부에서 소비되는 실물 상품과 서비스(예: 차량 공유, 음식 배달)에 대해 외부 결제 수단 사용을 항상 허용해 왔습니다.
Capacitor의 Stripe Payments: 새로운 Apple 가이드라인 이어서 보기
사용 중이라면 Capacitor에서의 Stripe 결제: 새로운 Apple 가이드라인 보안 및 컴플라이언스 계획을 세우려면, 다음 항목과 연결하세요: 암호화 암호화 구현 상세 정보: 컴플라이언스 컴플라이언스 구현 상세 정보: Capgo 보안 스캐너 Capgo Security Scanner 제품 워크플로우: Capgo 보안 Capgo Security 제품 워크플로우 및 Capgo 신뢰 센터 Capgo Trust Center 제품 워크플로우: