국내 결제 시스템만 다루다가 해외 고객을 받으려면 Stripe 같은 글로벌 결제 게이트웨이가 필수다. 다만 대부분의 개발자들은 Stripe API 문서가 영문이라는 이유로, 혹은 복잡해 보인다는 이유로 시작조차 못 한다. 이번에는 Stripe 계정 생성부터 실제 결제 요청, 결제 검증까지 한국 개발자 입장에서 정확히 뭘 하는 건지, 왜 이 순서대로 진행해야 하는지, 어떻게 코드로 구현하는지 완벽하게 정리해서 소개하겠다.
계정 생성 절차:
1. Stripe 공식 사이트에서 회원가입 (이메일, 비밀번호)
2. 이메일 인증 완료
3. 비즈니스 정보 입력 (사업자등록번호, 대표자명, 정산계좌)
4. 본인인증(휴대폰) 및 사업자 확인
5. 승인 완료 후 대시보드 접근 가능
승인까지는 보통 1~2시간 걸린다. 승인받고 난 후 Developers → API keys 메뉴에서 두 가지 키를 확인할 수 있다:
| 키 종류 | 용도 | 노출 여부 |
|---|---|---|
| Publishable Key | 클라이언트(JavaScript)에서 결제 폼 초기화할 때 사용 | 공개해도 됨 |
| Secret Key | 서버(PHP/Python)에서 결제 처리할 때 사용 | 절대 노출하면 안 됨 |
Stripe는 Test Mode와 Live Mode를 구분한다. 초기에는 Test Mode에서 가짜 카드로 테스트하다가, 실제 운영 시 Live Mode로 전환하면 된다.
composer require stripe/stripe-php
또는 직접 다운로드해서 프로젝트 폴더에 넣어도 된다. 이후 PHP 파일에서 autoload를 포함시키면 준비는 끝이다:
require_once 'vendor/autoload.php';
✗ 잘못된 방식:
// 서버에서 바로 카드 정보를 받아 결제 처리
$charge = StripeCharge::create([
'amount' => 10000,
'currency' => 'krw',
'source' => $_POST['stripeToken']
]);
이 방식은 구식이다. 클라이언트에서 카드 정보를 직접 핸들링하면 PCI-DSS 규정 위반이 되기 쉽다.
✓ 올바른 방식:
require_once 'vendor/autoload.php';
StripeStripe::setApiKey('sk_test_your_secret_key_here');
try {
// 1. 결제 인텐트 생성
$paymentIntent = StripePaymentIntent::create([
'amount' => 10000, // 금액 (단위: 센트, 1000 = 10원)
'currency' => 'krw', // 한국 원화
'metadata' => [
'order_id' => '12345', // 주문 ID
'user_id' => '999' // 사용자 ID
]
]);
// 2. 클라이언트에 반환 (JavaScript에서 사용)
echo json_encode([
'clientSecret' => $paymentIntent->client_secret,
'status' => 'success'
]);
} catch (StripeExceptionApiErrorException $e) {
echo json_encode([
'error' => $e->getMessage(),
'status' => 'error'
]);
}
이제 client_secret 값을 클라이언트로 전송하면, JavaScript의 Stripe Elements에서 이를 받아 결제 폼을 완성한다.
<!DOCTYPE html>
<html>
<head>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<form id="payment-form">
<div id="card-element"></div>
<button type="submit" id="submit-button">결제하기</button>
</form>
<script>
const stripe = Stripe('pk_test_your_publishable_key_here');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
// 1. 서버에서 결제 인텐트의 clientSecret 받기
const response = await fetch('/create_payment_intent.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 10000 })
});
const data = await response.json();
const clientSecret = data.clientSecret;
// 2. 결제 처리
const result = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: '홍길동'
}
}
});
if (result.paymentIntent.status === 'succeeded') {
alert('결제 성공!');
console.log('Payment Intent ID:', result.paymentIntent.id);
} else {
alert('결제 실패: ' + result.error.message);
}
});
</script>
</body>
</html>
paymentIntent.id를 서버로 전송해서 다시 한 번 검증하는 게 보안상 필수다.
✗ 잘못된 방식:
// 클라이언트가 보낸 금액을 그대로 믿음
if ($_POST['amount'] == 10000) {
// 상품 제공
}
// 위험: 클라이언트 쪽에서 금액을 변조할 수 있음
✓ 올바른 방식:
require_once 'vendor/autoload.php';
StripeStripe::setApiKey('sk_test_your_secret_key_here');
try {
$paymentIntentId = $_POST['payment_intent_id']; // 클라이언트에서 전송
// 1. Stripe 서버에서 결제 인텐트 조회
$paymentIntent = StripePaymentIntent::retrieve($paymentIntentId);
// 2. 상태 확인
if ($paymentIntent->status !== 'succeeded') {
throw new Exception('결제 상태가 완료되지 않음');
}
// 3. 금액 확인 (중요!)
if ($paymentIntent->amount !== 10000) {
throw new Exception('결제 금액이 맞지 않음');
}
// 4. metadata에서 주문 ID 확인
$orderId = $paymentIntent->metadata['order_id'];
$userId = $paymentIntent->metadata['user_id'];
// 5. 데이터베이스 업데이트
// UPDATE orders SET status = 'paid' WHERE id = $orderId;
echo json_encode([
'status' => 'success',
'message' => '결제가 검증되었습니다',
'order_id' => $orderId
]);
} catch (Exception $e) {
echo json_encode([
'status' => 'error',
'message' => $e->getMessage()
]);
}
❌ 실수 1: Secret Key를 JavaScript에서 사용하기
Secret Key는 절대 클라이언트에 노출되면 안 된다. 반드시 서버(PHP) 코드에서만 사용하자.
❌ 실수 2: 클라이언트 쪽 금액을 그대로 믿기
항상 서버에서 Stripe API로 조회해서 금액을 재검증해야 한다. 클라이언트는 변조될 수 있다.
❌ 실수 3: 테스트 카드 번호로 실제 결제 테스트
Stripe는 테스트/라이브 모드를 구분한다. 개발 중에는 반드시 sk_test_와 pk_test_로 시작하는 테스트 키를 사용하자. 테스트 카드 번호는 아래 참고:
- 성공: 4242 4242 4242 4242 (유효기간/CVC는 아무거나)
- 실패: 4000 0000 0000 0002
❌ 실수 4: Webhook 없이 결제 추적하기
결제 완료 후 서버가 응답하지 못하는 경우(네트워크 오류 등)를 대비해서 Webhook을 설정하는 게 좋다. Stripe에서는 payment_intent.succeeded 이벤트를 보내주므로 이를 받아서 추가 처리할 수 있다.
require_once 'vendor/autoload.php';
StripeStripe::setApiKey('sk_test_your_secret_key_here');
try {
$paymentIntentId = 'pi_1234567890'; // 원본 결제 ID
// 환불 생성
$refund = StripeRefund::create([
'payment_intent' => $paymentIntentId,
'reason' => 'requested_by_customer' // 고객 요청
]);
if ($refund->status === 'succeeded') {
echo '환불이 완료되었습니다';
// 데이터베이스에서 주문 상태를 'refunded'로 업데이트
}
} catch (StripeExceptionApiErrorException $e) {
echo '환불 처리 중 오류: ' . $e->getMessage();
}