메인 콘텐츠로 건너뛰기

개요

Laravel Cashier (Stripe)는 Stripe의 과금 기능을 Laravel에서 다루기 위한 공식 패키지입니다. Subscription의 생성·상태 확인·취소, 1회 결제, 청구서 다운로드, Webhook 처리를 통일된 API로 구현할 수 있습니다.

설치와 설정

먼저 Cashier를 설치하고, 필요한 테이블을 생성해 주세요.
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
다음으로 App\Models\User 같은 과금 대상 모델에 Billable 트레이트를 추가합니다.
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}
.env에 Stripe 키를 설정합니다.
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

고객 관리

Stripe Customer를 아직 생성하지 않았을 가능성이 있는 경우 createOrGetStripeCustomer()를 사용합니다.
$stripeCustomer = $user->createOrGetStripeCustomer();
명시적으로 생성하려면 createAsStripeCustomer()를 사용합니다.
$stripeCustomer = $user->createAsStripeCustomer();

Subscription

신규 생성

newSubscription()create()로 Subscription을 시작합니다. $paymentMethodId에는 Stripe.js 등에서 취득한 Payment Method ID를 전달합니다.
$user->newSubscription('default', 'price_monthly')
    ->create($paymentMethodId);
price_monthly는 예시입니다. 실제 구현 시에는 Stripe 대시보드에서 생성한 실제 Price ID를 지정해 주세요.

상태 확인

subscribed()로 유효한 Subscription인지 확인할 수 있습니다.
if ($user->subscribed('default')) {
    // Active subscription...
}

취소와 재개

$user->subscription('default')->cancel();

if ($user->subscription('default')->onGracePeriod()) {
    // The user is on the grace period...
}

$user->subscription('default')->resume();

1회 결제 (Charge)

charge()로 1회만 과금할 수 있습니다. 금액은 통화의 최소 단위로 전달해 주세요(예: USD라면 100$1.00). 여기서도 $paymentMethodId에는 Stripe에서 생성한 Payment Method ID를 사용합니다.
$payment = $user->charge(100, $paymentMethodId);
과금에 실패한 경우 charge()는 예외를 던집니다.

Payment Element

Stripe의 Payment Element를 사용하면 카드·Apple Pay·Google Pay·iDEAL 등 여러 결제 수단을 통일적으로 다룰 수 있습니다.

Subscription에서의 이용

Setup Intent를 생성해 뷰에 전달합니다.
return view('subscribe', [
    'intent' => $user->createSetupIntent()
]);
Blade 뷰에서 Payment Element를 마운트하고, Setup Intent의 client_secret을 전달합니다.
<div id="payment-element"></div>
<button id="submit">Subscribe</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $intent->client_secret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmSetup({
            elements,
            confirmParams: {
                return_url: '{{ route("subscription.complete") }}',
            },
        });

        if (error) {
            // 오류 메시지를 사용자에게 표시...
        }
    });
</script>
Stripe의 리다이렉트 대상 URL에는 setup_intent 파라미터가 부여됩니다. 이를 사용해 Payment Method ID를 취득하고, Subscription을 생성합니다.
use Illuminate\Http\Request;

Route::get('/subscription/complete', function (Request $request) {
    $setupIntent = $request->user()->findSetupIntent(
        $request->setup_intent
    );

    $paymentMethod = $setupIntent->payment_method;

    $request->user()
        ->newSubscription('default', 'price_xxx')
        ->create($paymentMethod);

    return redirect('/dashboard');
})->name('subscription.complete');

1회 결제에서의 이용

1회 결제에서는 pay()로 Payment Intent를 생성합니다. Order 모델에는 user_id, amount, status, stripe_payment_intent_id 컬럼이 필요합니다.
use App\Models\Order;
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $amount = 1000;

    $payment = $request->user()->pay($amount);

    $order = Order::create([
        'user_id' => $request->user()->id,
        'amount' => $amount,
        'status' => 'pending',
        'stripe_payment_intent_id' => $payment->id,
    ]);

    return view('checkout', [
        'clientSecret' => $payment->client_secret,
        'order' => $order,
    ]);
});
Payment Element를 마운트해 결제를 확정합니다.
<div id="payment-element"></div>
<button id="submit">Pay Now</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $clientSecret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: '{{ route("payment.complete") }}',
            },
        });

        if (error) {
            // 오류 메시지를 사용자에게 표시...
        }
    });
</script>
리다이렉트 후에는 payment_intent 파라미터로 Order를 검색하고, Payment Intent가 인증된 사용자의 것이며 succeeded 상태인지 확인한 후 주문을 확정합니다.
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/payment/complete', function (Request $request) {
    $order = Order::where('user_id', $request->user()->id)
        ->where('stripe_payment_intent_id', $request->payment_intent)
        ->firstOrFail();

    $paymentIntent = $request->user()
        ->stripe()
        ->paymentIntents
        ->retrieve($request->payment_intent);

    if ($paymentIntent->customer === $request->user()->stripe_id &&
        $paymentIntent->status === 'succeeded') {
        $order->update(['status' => 'paid']);

        // 주문을 확정하는 처리...
    }

    return redirect('/dashboard');
})->name('payment.complete');

청구서

청구서 목록은 invoices()로 가져올 수 있습니다.
$invoices = $user->invoices();
PDF 다운로드에는 dompdf/dompdf를 설치하고, downloadInvoice()를 사용합니다. $invoiceId에는 invoices()로 가져온 청구서 ID를 전달해 주세요.
composer require dompdf/dompdf
return $user->downloadInvoice($invoiceId);

Webhook 설정

Cashier는 Stripe Webhook용 라우트를 자동 등록하며, 기본적으로 /stripe/webhook을 사용합니다. Stripe 대시보드에 이 URL을 설정해 주세요. Webhook 생성은 cashier:webhook으로 실행할 수 있습니다.
php artisan cashier:webhook
CSRF 보호에서 stripe/*를 제외해 주세요.
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})
STRIPE_WEBHOOK_SECRET.env에 설정하면 Cashier의 서명 검증 미들웨어에서 Webhook 요청을 검증할 수 있습니다.
마지막 수정일 2026년 7월 13일