Stripe 연동
Stripe를 결제 수단으로 사용하여 신용카드, Apple Pay, Google Pay 및 40개 이상의 결제 수단을 전 세계적으로 지원할 수 있습니다.
사전 요구 사항
시작하기 전에 다음을 준비해주세요:
개발 중에는 테스트 모드 키(sk_test_... / pk_test_...)를 사용하세요. 실제 결제를 받을 준비가 되었을 때만 라이브 키로 전환하세요.
환경 변수
API 서버에 다음 환경 변수를 추가하세요:
# Stripe 시크릿 키 (서버 전용 — 클라이언트에 절대 노출하지 마세요)
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
# Stripe 웹훅 서명 시크릿 (Stripe Dashboard → Webhooks에서 확인)
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
프론트엔드 애플리케이션에는 퍼블리셔블 키를 추가하세요:
# Stripe 퍼블리셔블 키 (클라이언트에서 안전하게 사용 가능)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY와 STRIPE_WEBHOOK_SECRET은 절대 클라이언트에 노출하지 마세요. 서버 측에서만 사용해야 합니다.
결제 흐름
Stripe 결제 흐름은 네 단계로 구성됩니다: 장바구니 생성, 체크아웃 시작, 클라이언트에서 결제 확인, 웹훅 처리.
SDK를 사용하여 장바구니를 생성하고 상품을 추가합니다:
import { createStorefrontClient } from '@headless-commerce/sdk';
const client = createStorefrontClient({
apiKey: process.env.NEXT_PUBLIC_HC_API_KEY!,
});
// 장바구니 생성
const cart = await client.carts.create({
session_id: 'session_abc123',
});
// 상품 추가
await client.carts.addItem(cart.id, {
variant_id: 'var_xxx',
quantity: 2,
});
import requests
base_url = "https://api.headlesscommerce.io/v1"
headers = {
"Authorization": "Bearer pk_test_your_key",
"Content-Type": "application/json",
}
# 장바구니 생성
cart = requests.post(f"{base_url}/storefront/carts", headers=headers, json={
"session_id": "session_abc123",
}).json()
# 상품 추가
requests.post(
f"{base_url}/storefront/carts/{cart['id']}/items",
headers=headers,
json={"variant_id": "var_xxx", "quantity": 2},
)
# 장바구니 생성
curl -X POST https://api.headlesscommerce.io/v1/storefront/carts \
-H "Authorization: Bearer pk_test_your_key" \
-H "Content-Type: application/json" \
-d '{"session_id": "session_abc123"}'
# 상품 추가
curl -X POST https://api.headlesscommerce.io/v1/storefront/carts/{cart_id}/items \
-H "Authorization: Bearer pk_test_your_key" \
-H "Content-Type: application/json" \
-d '{"variant_id": "var_xxx", "quantity": 2}'
payment_method: 'stripe'를 지정하여 체크아웃 엔드포인트를 호출합니다. API가 Stripe PaymentIntent를 생성하고 client_secret을 반환합니다:
const order = await client.carts.checkout(cart.id, {
email: 'customer@example.com',
shipping_address: {
line1: '서울특별시 강남구 테헤란로 123',
city: '서울',
postal_code: '06234',
country: 'KR',
},
payment_method: 'stripe',
});
// 응답에 Stripe client_secret이 포함됩니다
const { client_secret } = order.payment;
console.log(order.id); // order_xxxxxxxx
console.log(client_secret); // pi_xxx_secret_xxx
order = requests.post(
f"{base_url}/storefront/carts/{cart['id']}/checkout",
headers=headers,
json={
"email": "customer@example.com",
"shipping_address": {
"line1": "서울특별시 강남구 테헤란로 123",
"city": "서울",
"postal_code": "06234",
"country": "KR",
},
"payment_method": "stripe",
},
).json()
# 응답에 Stripe client_secret이 포함됩니다
client_secret = order["payment"]["client_secret"]
print(order["id"]) # order_xxxxxxxx
print(client_secret) # pi_xxx_secret_xxx
curl -X POST https://api.headlesscommerce.io/v1/storefront/carts/{cart_id}/checkout \
-H "Authorization: Bearer pk_test_your_key" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"shipping_address": {
"line1": "서울특별시 강남구 테헤란로 123",
"city": "서울",
"postal_code": "06234",
"country": "KR"
},
"payment_method": "stripe"
}'
프론트엔드에서 Stripe.js를 사용하여 client_secret으로 결제를 확인합니다:
import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
const { error } = await stripe!.confirmPayment({
clientSecret: client_secret,
confirmParams: {
return_url: 'https://your-store.com/order/confirmation',
},
});
if (error) {
// 고객에게 오류 표시 (예: 잔액 부족, 카드 거절)
console.error(error.message);
} else {
// 결제 처리 중 — Stripe가 return_url로 리다이렉트합니다
}
Stripe Elements를 사용한 커스텀 결제 폼의 경우:
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
function CheckoutForm({ clientSecret }: { clientSecret: string }) {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://your-store.com/order/confirmation',
},
});
if (error) {
console.error(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe}>
결제하기
</button>
</form>
);
}
고객이 결제를 완료하면 Stripe가 서버로 웹훅 이벤트를 전송합니다. Headless Commerce가 이를 자동으로 처리하여 주문 상태를 confirmed로 업데이트합니다.
별도의 추가 코드가 필요하지 않습니다 — 플랫폼이 웹훅 검증과 주문 업데이트를 내부적으로 처리합니다.
웹훅 설정
Headless Commerce는 Stripe 웹훅 이벤트를 수신하여 주문 및 결제 상태를 동기화합니다. Stripe Dashboard에서 웹훅을 설정하세요:
- Stripe Dashboard에서 Developers → Webhooks로 이동
- Add endpoint 클릭
- 웹훅 URL 입력:
https://api.headlesscommerce.io/v1/webhooks/stripe
- 다음 이벤트를 선택:
payment_intent.succeeded
payment_intent.payment_failed
charge.refunded
charge.dispute.created
- Signing secret을 복사하여
STRIPE_WEBHOOK_SECRET으로 설정
이벤트 처리
| Stripe 이벤트 | Headless Commerce 동작 |
|---|
payment_intent.succeeded | 주문 상태 confirmed로 변경, 결제 completed 처리 |
payment_intent.payment_failed | 결제 failed 처리, payment.failed 웹훅 전송 |
charge.refunded | 환불 기록, payment.refunded 웹훅 전송 |
charge.dispute.created | 주문에 검토 플래그 설정 |
Admin API를 통해 전액 또는 부분 환불을 처리할 수 있습니다:
import { createAdminClient } from '@headless-commerce/sdk';
const admin = createAdminClient({
apiKey: process.env.HC_ADMIN_API_KEY!,
});
// 전액 환불
const refund = await admin.orders.refund('order_xxxxxxxx', {
reason: 'customer_request',
});
// 부분 환불
const partialRefund = await admin.orders.refund('order_xxxxxxxx', {
amount: 1500,
reason: '배송 중 상품 파손',
});
환불은 Stripe를 통해 자동으로 처리되며 주문 상태가 그에 따라 업데이트됩니다.
테스트
테스트 모드 키
개발 중에는 항상 테스트 모드 API 키를 사용하세요. 테스트 키로 생성된 거래는 실제 결제 네트워크를 사용하지 않습니다.
| 키 유형 | 접두사 | 예시 |
|---|
| 퍼블리셔블 키 | pk_test_ | pk_test_51ABC... |
| 시크릿 키 | sk_test_ | sk_test_51ABC... |
테스트 카드 번호
테스트 모드에서 다음 카드 번호를 사용할 수 있습니다:
| 카드 번호 | 시나리오 |
|---|
4242 4242 4242 4242 | 결제 성공 |
4000 0000 0000 3220 | 3D Secure 인증 필요 |
4000 0000 0000 9995 | 결제 거절 (잔액 부족) |
4000 0000 0000 0002 | 일반 카드 거절 |
만료일은 미래 날짜, CVC는 아무 3자리 숫자, 우편번호는 아무 값이나 사용할 수 있습니다.
로컬에서 웹훅 테스트
Stripe CLI를 사용하여 로컬 개발 서버로 웹훅 이벤트를 전달할 수 있습니다:
# Stripe CLI 설치
brew install stripe/stripe-cli/stripe
# Stripe 계정 로그인
stripe login
# 로컬 엔드포인트로 이벤트 전달
stripe listen --forward-to localhost:3000/v1/webhooks/stripe
# 다른 터미널에서 테스트 이벤트 트리거
stripe trigger payment_intent.succeeded
오류 처리
프론트엔드에서 결제 오류를 적절히 처리하세요:
try {
const order = await client.carts.checkout(cart.id, {
email: 'customer@example.com',
shipping_address: { /* ... */ },
payment_method: 'stripe',
});
} catch (error: unknown) {
if (error instanceof Error && 'code' in error) {
const apiError = error as { code: string; message: string };
switch (apiError.code) {
case 'cart_empty':
// 장바구니가 비어 있음
break;
case 'insufficient_stock':
// 하나 이상의 상품 재고가 부족함
break;
case 'payment_provider_error':
// Stripe에서 PaymentIntent 생성 중 오류 반환
break;
default:
console.error(apiError.message);
}
}
}
confirmPayment() 이후 발생할 수 있는 주요 Stripe 오류:
| 오류 코드 | 설명 | 권장 조치 |
|---|
card_declined | 카드가 거절됨 | 다른 카드 사용 요청 |
expired_card | 카드가 만료됨 | 카드 정보 업데이트 요청 |
insufficient_funds | 잔액 부족 | 다른 카드 사용 요청 |
processing_error | Stripe 처리 오류 | 결제 재시도 |
incorrect_cvc | CVC 불일치 | CVC 재입력 요청 |
다음 단계
웹훅
모든 웹훅 이벤트와 페이로드 형식을 확인하세요.
TossPayments
한국 결제 수단을 위한 TossPayments를 설정하세요.
API 레퍼런스
모든 체크아웃 및 결제 엔드포인트를 살펴보세요.
SDK
전체 SDK 레퍼런스와 모든 리소스 및 메서드를 확인하세요.