Chrome 확장 프로그램에 Stripe Payments를 통합하는 방법 (단계별)
Source: Dev.to
Overview
결제 흐름은 다음과 같습니다:
- 사용자가 확장 프로그램 팝업에서 **“Upgrade to Premium”**을 클릭합니다.
- 확장 프로그램이 서버리스 함수에 호출하여 Stripe Checkout URL을 생성합니다.
- 백그라운드 스크립트가 사용자를 Stripe의 보안 Checkout 페이지로 리디렉션합니다.
- 사용자가 결제를 완료합니다.
- Stripe가 웹훅을 서버리스 함수에 전송합니다.
- 웹훅 핸들러가 구독 데이터를 데이터베이스에 저장합니다.
- 사용자가 프리미엄 기능에 접근할 때 확장 프로그램이 프리미엄 상태를 확인합니다.
Part 1: Setting Up Stripe
Step 1 – Create a Stripe Sandbox Account
개발 및 테스트를 위해 Stripe 샌드박스로 시작합니다. 공식 문서를 참고하세요.
Step 2 – Create Your Product
-
Stripe 대시보드에서 Products → Add Product 로 이동합니다.
-
세부 정보를 입력합니다:
- Name: “Premium Subscription”
- Description: “Monthly premium access”
- Pricing: $9.99 / month (또는 원하는 가격)
-
Price ID(
price_로 시작)를 복사합니다. 나중에 필요합니다.
Step 3 – Get Your API Keys
Developers → API Keys 로 이동하거나 방문하세요.
- Secret Key(샌드박스의 경우
sk_test_로 시작)를 복사합니다. - 안전하게 보관하세요—소스 컨트롤에 절대 커밋하지 마세요.
Reference: Stripe API Keys Documentation.
Part 2: Creating Serverless Functions
두 개의 서버리스 엔드포인트를 만들겠습니다:
- Generate Checkout URLs
- Handle Stripe Webhooks
Setting Up Your Project
Stripe SDK를 설치합니다:
npm install stripe --save
Reference: Stripe SDK Docs.
Initialize Stripe Client
Stripe 클라이언트를 설정할 파일을 생성합니다:
// lib/stripe/stripeClient.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-11-17.clover',
typescript: true,
});
export default stripe;
STRIPE_SECRET_KEY는 .env 파일에 정의되어 있어야 합니다.
Function 1 – Create Checkout Links
이 서버리스 함수는 개인화된 Checkout URL(이메일 미리 채워짐)을 생성합니다.
// app/api/checkout/route.ts
import stripe from '@/lib/stripe/stripeClient';
interface CreateCheckoutParams {
user?: {
customerId?: string;
email?: string;
};
mode: 'payment' | 'subscription';
clientReferenceId?: string;
priceId: string;
couponId?: string | null;
}
export const createCheckoutStripe = async ({
user,
mode,
clientReferenceId,
priceId,
couponId,
}: CreateCheckoutParams): Promise => {
try {
const extraParams: {
customer?: string;
customer_creation?: 'always';
customer_email?: string;
invoice_creation?: { enabled: boolean };
payment_intent_data?: { setup_future_usage: 'on_session' };
tax_id_collection?: { enabled: boolean };
} = {};
// Existing vs. new customers
if (user?.customerId) {
extraParams.customer = user.customerId;
} else {
if (mode === 'payment') {
extraParams.customer_creation = 'always';
extraParams.invoice_creation = { enabled: true };
extraParams.payment_intent_data = { setup_future_usage: 'on_session' };
}
if (user?.email) extraParams.customer_email = user.email;
extraParams.tax_id_collection = { enabled: true };
}
const stripeSession = await stripe.checkout.sessions.create({
mode,
allow_promotion_codes: true,
client_reference_id: clientReferenceId,
line_items: [{ price: priceId, quantity: 1 }],
discounts: couponId ? [{ coupon: couponId }] : [],
success_url: 'https://example.com/checkout-success',
cancel_url: 'https://example.com/home',
locale: 'auto',
...extraParams,
});
return stripeSession.url;
} catch (e) {
console.error('Error creating checkout session:', e);
return null;
}
};
export async function POST(request: Request) {
try {
const { id, userEmail, mode } = await request.json();
if (!id) {
return new Response(JSON.stringify({ error: 'Price ID is missing' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
if (!mode) {
return new Response(
JSON.stringify({
error: "Mode is missing (choose 'payment' or 'subscription')",
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const checkoutUrl = await createCheckoutStripe({
user: { email: userEmail },
mode,
priceId: id,
});
if (!checkoutUrl) {
return new Response(JSON.stringify({ error: 'Failed to create URL' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ checkout_url: checkoutUrl }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Checkout error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
Alternative: 사용자의 이메일을 미리 채울 필요가 없으면 Stripe 대시보드(Products → Checkout Links)에서 고정 Checkout 링크를 직접 만들고 이 함수를 건너뛸 수 있습니다.
Function 2 – Handle Stripe Webhooks
(구현은 간략히 생략했습니다; Stripe 서명을 검증하고 이벤트를 파싱한 뒤 데이터베이스에 구독 상태를 업데이트하는 엔드포인트를 만들면 됩니다.)
Part 3: Building the Chrome Extension
Extension File Structure
your-extension/
├── manifest.json
├── popup/
│ ├── popup.html
│ └── popup.tsx
├── background/
│ └── background.ts
├── utils/
│ └── handleCheckout.ts
└── components/
└── PremiumCard.tsx
Step 1 – Checkout Handler
서버리스 Checkout 엔드포인트를 호출하고 URL을 반환하는 유틸리티입니다.
// utils/handleCheckout.ts
/**
* Generates a Stripe checkout URL by calling your serverless function.
* The email is pre‑filled on the Stripe Checkout page.
*/
export async function createCheckoutURL(
email: string | null,
priceId: string,
mode: 'payment' | 'subscription' = 'subscription'
): Promise {
const CHECKOUT_ENDPOINT = 'https://your-app.vercel.app/api/checkout';
try {
const response = await fetch(CHECKOUT_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: priceId,
userEmail: email,
mode,
}),
});
if (!response.ok) {
console.error('Failed to create checkout URL:', response.statusText);
return null;
}
const { checkout_url } = await response.json();
return checkout_url;
} catch (error) {
console.error('Error creating checkout URL:', error);
return null;
}
}
이제 팝업이나 백그라운드 스크립트에서 createCheckoutURL을 호출하고 반환된 URL을 새 탭으로 열면 됩니다. 웹훅을 통해 사용자의 프리미엄 상태를 업데이트하면 완료됩니다.