국내 결제 시스템만 다루다가 해외 고객을 받으려면 Stripe 같은 글로벌 결제 게이트웨이가 필수다. 다만 대부분의 개발자들은 Stripe API 문서가 영문이라는 이유로, 혹은 복잡해 보인다는 이유로 시작조차 못 한다. 이번에는 Stripe 계정 생성부터 실제 결제 요청, 결제 검증까지 한국 개발자 입장에서 정확히 뭘 하는 건지, 왜 이 순서대로 진행해야 하는지, 어떻게 코드로 구현하는지 완벽하게 정리해서 소개하겠다.

 

1단계: Stripe 계정 생성 및 API 키 발급받기
Stripe는 전 세계 135개국에서 온라인 결제를 처리해주는 서비스다. 한국도 지원하기 때문에 국내 사업자라면 계정을 만들고 바로 결제를 받을 수 있다.

계정 생성 절차:

1. Stripe 공식 사이트에서 회원가입 (이메일, 비밀번호)
2. 이메일 인증 완료
3. 비즈니스 정보 입력 (사업자등록번호, 대표자명, 정산계좌)
4. 본인인증(휴대폰) 및 사업자 확인
5. 승인 완료 후 대시보드 접근 가능

승인까지는 보통 1~2시간 걸린다. 승인받고 난 후 Developers → API keys 메뉴에서 두 가지 키를 확인할 수 있다:

키 종류 용도 노출 여부
Publishable Key 클라이언트(JavaScript)에서 결제 폼 초기화할 때 사용 공개해도 됨
Secret Key 서버(PHP/Python)에서 결제 처리할 때 사용 절대 노출하면 안 됨

Stripe는 Test ModeLive Mode를 구분한다. 초기에는 Test Mode에서 가짜 카드로 테스트하다가, 실제 운영 시 Live Mode로 전환하면 된다.

 

2단계: Stripe PHP 라이브러리 설치
PHP에서 Stripe API를 사용하려면 공식 라이브러리를 설치해야 한다.
composer require stripe/stripe-php

또는 직접 다운로드해서 프로젝트 폴더에 넣어도 된다. 이후 PHP 파일에서 autoload를 포함시키면 준비는 끝이다:

require_once 'vendor/autoload.php';

 

3단계: 결제 인텐트(Payment Intent) 생성하기
Stripe의 결제 흐름은 결제 인텐트(Payment Intent)라는 객체를 중심으로 돈다. 이는 한 번의 결제 거래를 추적하는 역할을 한다.

✗ 잘못된 방식:

// 서버에서 바로 카드 정보를 받아 결제 처리
$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에서 이를 받아 결제 폼을 완성한다.

 

4단계: 클라이언트(JavaScript)에서 결제 폼 구성
<!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>

 

5단계: 결제 완료 후 서버에서 검증
JavaScript에서 결제가 완료되면 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()
    ]);
}

 

6단계: 주의사항 및 흔한 실수

❌ 실수 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 이벤트를 보내주므로 이를 받아서 추가 처리할 수 있다.

 

7단계: 환불(Refund) 처리
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();
}

 

Stripe API는 생각보다 간단하다
Stripe API는 글로벌 결제 시스템 중에서도 가장 개발자 친화적인 편이다. 처음에는 Payment Intent → Confirm Payment → Verify 순서가 복잡해 보이지만, 이 흐름이 가장 안전한 방식이라는 걸 알면 자연스럽게 이해된다. 작은 실수(예: 금액 재검증 생략, Secret Key 노출)가 모여서 결제 사기로 이어질 수 있다는 점을 잊지 말자. 이 글의 5단계(서버 검증)6단계(주의사항)을 참고해서 안전한 결제 시스템을 구축하면, 해외 고객도 자신 있게 받을 수 있을 것이다.