Laravel Cashier (Stripe)
번역일: 2026년 7월 2일
Laravel Cashier (Stripe)
- 소개
- Cashier 업그레이드
- 설치
- 설정
- 퀵스타트
- 고객
- 결제 수단
- 구독
- 구독 평가판
- Stripe 웹훅 처리
- 단건 결제
- 체크아웃
- 인보이스
- 결제 실패 처리
- 강력한 고객 인증 (SCA)
- Stripe SDK
- 테스트
소개
Laravel Cashier Stripe는 Stripe의 구독 결제 서비스를 Laravel 애플리케이션에서 쉽게 사용할 수 있도록 도와주는 공식 패키지입니다. 구독 관리, 쿠폰 적용, 구독 변경, 수량 조정, 유예 기간, 인보이스 PDF 생성 등 반복적으로 구현하게 되는 결제 관련 보일러플레이트 코드를 대부분 처리해 줍니다.
NOTE
Stripe 일회성 결제만 사용하고 구독은 필요하지 않다면, Cashier를 사용하지 않고 Stripe SDK를 직접 활용하는 방법을 고려해 보세요.
Cashier 업그레이드
Cashier를 새 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 먼저 꼼꼼히 확인하세요.
WARNING
예기치 않은 변경을 방지하기 위해 Cashier는 고정된 Stripe API 버전을 내부적으로 사용합니다. Cashier 15는 Stripe API 버전 2023-10-16을 사용합니다. Stripe의 새로운 기능이나 개선 사항을 활용하려면 Cashier 패키지 버전을 업그레이드하세요.
설치
Composer를 사용해 Cashier 패키지를 설치합니다.
composer require laravel/cashier설치 후, vendor:publish Artisan 명령어로 Cashier의 마이그레이션 파일을 프로젝트에 퍼블리시합니다.
php artisan vendor:publish --tag="cashier-migrations"그런 다음 데이터베이스 마이그레이션을 실행합니다.
php artisan migrateCashier의 마이그레이션은 users 테이블에 여러 컬럼을 추가하고, 고객의 구독 정보를 저장하는 subscriptions 테이블과 복수 구독 아이템을 저장하는 subscription_items 테이블을 새로 생성합니다.
필요하다면 vendor:publish 명령어로 Cashier의 설정 파일도 퍼블리시할 수 있습니다.
php artisan vendor:publish --tag="cashier-config"마지막으로, Stripe가 결제 이벤트를 올바르게 처리할 수 있도록 반드시 Cashier의 웹훅 처리를 설정하세요.
WARNING
Stripe는 Stripe 고객 ID를 저장하는 컬럼이 대소문자를 구별하도록 권장합니다. MySQL을 사용하는 경우 stripe_id 컬럼의 콜레이션을 utf8_bin으로 설정해야 합니다. 자세한 내용은 Stripe 공식 문서를 참고하세요.
설정
Billable 모델
Cashier를 사용하기 전에, 결제 주체가 될 모델(보통 User 모델)에 Billable 트레이트를 추가하세요. 이 트레이트는 구독 생성, 쿠폰 적용, 결제 수단 업데이트 등 일반적인 결제 작업을 위한 다양한 메서드를 제공합니다.
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}Cashier는 기본적으로 User 모델이 결제 주체라고 가정합니다. 다른 모델을 사용하고 싶다면 useCustomerModel 메서드로 변경할 수 있습니다. 보통 AppServiceProvider의 boot 메서드에서 호출합니다.
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}WARNING
Laravel 기본 제공 App\Models\User 외의 모델을 사용한다면, 해당 모델의 테이블명과 일치하도록 Cashier 마이그레이션을 퍼블리시한 후 수정해야 합니다.
API 키
다음으로 애플리케이션의 .env 파일에 Stripe API 키를 설정합니다. Stripe API 키는 Stripe 대시보드에서 확인할 수 있습니다.
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secretWARNING
STRIPE_WEBHOOK_SECRET 환경 변수는 반드시 설정해야 합니다. 이 값은 수신된 웹훅이 실제로 Stripe에서 전송된 것인지 검증하는 데 사용됩니다.
통화 설정
Cashier의 기본 통화는 미국 달러(USD)입니다. .env 파일에서 CASHIER_CURRENCY 환경 변수를 설정해 기본 통화를 변경할 수 있습니다. 예를 들어 한국 원화(KRW)를 사용하려면 다음과 같이 설정합니다.
CASHIER_CURRENCY=krw통화 외에도 인보이스에 금액을 표시할 때 사용할 로케일을 설정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용해 통화 형식을 지정합니다.
CASHIER_CURRENCY_LOCALE=ko_KRWARNING
en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 익스텐션이 설치되어 있어야 합니다.
세금 설정
Stripe Tax를 활용하면 Stripe가 생성하는 모든 인보이스에 대해 세금을 자동으로 계산할 수 있습니다. AppServiceProvider의 boot 메서드에서 calculateTaxes 메서드를 호출하면 자동 세금 계산이 활성화됩니다.
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}세금 계산이 활성화되면 신규 구독이나 단건 인보이스 생성 시 Stripe가 자동으로 세금을 계산합니다.
이 기능이 올바르게 동작하려면 고객의 이름, 주소, 세금 ID 등의 청구 정보가 Stripe에 동기화되어 있어야 합니다. Cashier에서 제공하는 고객 데이터 동기화와 세금 ID 기능을 활용하면 됩니다.
WARNING
calculateTaxes를 활성화하면 단건 결제나 단건 결제 체크아웃에는 세금이 계산되지 않습니다. Stripe Tax는 현재 이 두 가지 케이스를 지원하지 않습니다.
로깅
Cashier에서 Stripe 관련 치명적 오류가 발생할 때 사용할 로그 채널을 지정할 수 있습니다. .env 파일의 CASHIER_LOGGER 환경 변수로 설정합니다.
CASHIER_LOGGER=stackStripe API 호출에서 발생하는 예외는 애플리케이션의 기본 로그 채널을 통해 기록됩니다.
커스텀 모델 사용
Cashier가 내부적으로 사용하는 모델을 직접 정의한 모델로 교체할 수 있습니다. Cashier 모델을 상속하여 커스텀 모델을 만드세요.
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}모델을 정의했다면 AppServiceProvider의 boot 메서드에서 Cashier가 해당 모델을 사용하도록 지정합니다.
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}퀵스타트
단일 상품 판매
NOTE
Stripe Checkout을 사용하기 전에 Stripe 대시보드에서 고정 가격의 상품을 먼저 등록해야 합니다. 또한 Cashier의 웹훅 처리도 반드시 설정하세요.
애플리케이션에서 단일 상품이나 서비스의 결제를 받는 것은 Stripe Checkout을 통해 간단하게 구현할 수 있습니다. 고객이 결제 버튼을 클릭하면 Stripe의 Checkout 페이지로 이동하여 결제를 완료하게 됩니다.
먼저 라우트 파일에 다음과 같은 라우트를 정의합니다.
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
$stripePriceId = 'price_deluxe_album';
$quantity = 1;
return $request->user()->checkout([$stripePriceId => $quantity], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->middleware('auth');
Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');위 예시에서 볼 수 있듯, checkout 메서드를 호출하면 Stripe 대시보드에 등록된 특정 가격 ID에 대해 Stripe Checkout 세션으로 고객을 리다이렉트합니다. 결제가 성공하거나 취소되면 success_url 또는 cancel_url로 이동합니다.
필요하다면 checkout 메서드에 추가 Stripe 옵션을 전달할 수도 있습니다.
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
return $request->user()->checkout(['price_deluxe_album' => 1], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
'allow_promotion_codes' => true,
]);
})->middleware('auth');WARNING
Checkout은 대부분의 결제 옵션을 지원하지만, 일부 Stripe 기능은 사용할 수 없습니다. 지원 범위는 Stripe Checkout 세션 생성 API 문서를 참고하세요.
게스트(비회원) 체크아웃
로그인하지 않은 사용자도 결제할 수 있도록 Auth::guest 체크아웃을 사용할 수 있습니다.
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
Route::get('/checkout', function (Request $request) {
return Cashier::guest()->checkout(['price_deluxe_album' => 1], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
});결제 완료 후 웹훅 처리
Stripe에서 결제 이벤트를 수신할 수 있도록 Cashier의 웹훅 처리를 반드시 설정하세요. Stripe 대시보드에서 웹훅이 활성화되면, Cashier의 웹훅 컨트롤러로 Stripe 이벤트를 수신할 수 있습니다. 특히 checkout.session.completed 이벤트가 수신될 때 주문 처리 등의 후속 작업을 수행할 수 있습니다.
구독 판매
NOTE
Stripe Checkout을 사용하기 전에 Stripe 대시보드에서 고정 가격의 상품을 먼저 등록해야 합니다. 또한 Cashier의 웹훅 처리도 반드시 설정하세요.
애플리케이션에서 구독 서비스를 제공하는 것도 간단합니다. 아래 예시는 월간 기본 플랜(price_basic_monthly)과 연간 기본 플랜(price_basic_yearly)이 있다고 가정합니다.
먼저 고객이 이 두 가지 요금제 사이에서 선택할 수 있는 페이지를 구성합니다.
Route::get('/billing', function () {
return view('billing');
})->middleware('auth');다음은 구독 체크아웃 세션을 생성하는 라우트입니다.
use Illuminate\Http\Request;
Route::get('/subscribe/{plan}', function (Request $request, $plan) {
$plans = [
'monthly' => 'price_basic_monthly',
'yearly' => 'price_basic_yearly',
];
return $request->user()->newSubscription('default', $plans[$plan])
->checkout([
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->middleware('auth');이처럼 newSubscription 메서드로 구독명과 요금제 ID를 지정한 뒤 checkout을 호출하면, 고객이 Stripe Checkout 페이지에서 결제 정보를 입력하고 구독을 완료할 수 있습니다.
구독 상태에 따른 접근 제어
구독 상태를 확인하여 특정 기능에 대한 접근을 제어할 수 있습니다. 예를 들어 특정 페이지를 활성 구독자에게만 공개하고 싶다면 미들웨어를 활용하세요.
Route::get('/dashboard', function () {
// 구독 중인 사용자만 접근 가능
})->middleware(['auth', 'subscribed']);Cashier는 subscribed 미들웨어를 내장하고 있습니다. 사용 방법은 구독 상태 확인 섹션을 참고하세요.
고객 포털 제공
구독 중인 고객이 요금제를 변경하거나 구독을 취소할 수 있도록 Stripe의 청구 포털을 연결할 수 있습니다. 아래와 같이 라우트를 추가하면, 버튼 클릭 시 Stripe 청구 포털로 이동합니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware('auth');자세한 내용은 청구 포털 섹션을 참고하세요.
고객
고객 조회
Cashier::findBillable 메서드를 사용하면 Stripe 고객 ID로 고객을 조회할 수 있습니다. 반환값은 Billable 모델의 인스턴스입니다.
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);고객 생성
구독 없이 Stripe 고객만 먼저 생성하고 싶을 때는 createAsStripeCustomer 메서드를 사용합니다.
$stripeCustomer = $user->createAsStripeCustomer();Stripe에 고객이 생성된 이후 원하는 시점에 구독을 시작할 수 있습니다. 추가 파라미터는 Stripe API 문서의 고객 생성을 참고하세요.
$stripeCustomer = $user->createAsStripeCustomer($options);이미 Stripe 고객으로 등록되어 있다면 생성 없이 반환하는 createOrGetStripeCustomer 메서드도 사용할 수 있습니다.
$stripeCustomer = $user->createOrGetStripeCustomer();고객 정보 수정
경우에 따라 Stripe의 고객 정보를 직접 업데이트해야 할 수도 있습니다. updateStripeCustomer 메서드를 사용하세요.
$stripeCustomer = $user->updateStripeCustomer($options);잔액
Stripe에서는 고객의 "잔액(balance)"을 설정하거나 차감할 수 있습니다. 이 잔액은 새 인보이스 발행 시 크레딧 또는 추가 청구금으로 적용됩니다. 전체 잔액을 확인하려면 balance 메서드를 사용합니다.
$balance = $user->balance();고객 잔액을 크레딧으로 충전하려면 양수 값을 creditBalance 메서드에 전달합니다.
$user->creditBalance(500, '프리미엄 고객 크레딧');반대로 잔액을 차감하려면 debitBalance 메서드를 사용합니다.
$user->debitBalance(300, '불량 데이터 처리 비용');applyBalance 메서드는 고객의 잔액 트랜잭션을 새로 생성합니다. 트랜잭션 내역을 조회하려면 balanceTransactions 메서드를 사용하세요.
// 모든 잔액 트랜잭션 조회
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// 트랜잭션 금액
$amount = $transaction->amount(); // KRW 1,000
// 관련 인보이스가 있으면 조회
$invoice = $transaction->invoice();
}세금 ID
Cashier에서는 고객의 세금 ID를 쉽게 관리할 수 있습니다. taxIds 메서드로 등록된 모든 세금 ID를 컬렉션으로 가져올 수 있습니다.
$taxIds = $user->taxIds();특정 세금 ID를 ID로 조회하려면 다음과 같이 합니다.
$taxId = $user->findTaxId('txi_belgium');새 세금 ID를 추가하려면 createTaxId 메서드에 유효한 타입과 값을 전달합니다.
$taxId = $user->createTaxId('kr_brn', '1234567890');createTaxId 메서드는 즉시 고객 계정에 부가세 ID를 추가합니다. 세금 ID 검증은 Stripe가 처리하지만, 이 작업은 비동기로 수행됩니다. 세금 ID를 삭제하려면 deleteTaxId 메서드를 사용합니다.
$user->deleteTaxId('txi_korea');Stripe와 고객 데이터 동기화
일반적으로 사용자가 이름, 이메일, 주소 등 정보를 업데이트하면 Stripe에도 해당 정보를 동기화해야 합니다. 동기화하면 Stripe의 고객 정보가 애플리케이션과 일치하게 됩니다.
이를 자동화하려면 Billable 모델에서 customer.updated 이벤트를 수신하는 이벤트 리스너를 등록하세요. 이벤트 리스너 안에서 syncStripeCustomerDetails 메서드를 호출합니다.
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* 모델의 "booted" 메서드
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}이제 사용자 정보가 업데이트될 때마다 Stripe와 자동으로 동기화됩니다. 만약 초기 고객 생성 시에도 Stripe와 동기화하고 싶다면, created 이벤트에도 동일한 리스너를 추가하세요.
Stripe에 동기화되는 필드(이름, 이메일, 주소 등)를 커스터마이즈하려면 Billable 모델에서 Cashier가 제공하는 메서드들을 오버라이드하면 됩니다. 예를 들어 stripeName 메서드를 오버라이드하면 Stripe에 전송할 이름 필드를 변경할 수 있습니다.
/**
* Stripe에 동기화할 고객 이름을 반환합니다.
*/
public function stripeName(): string|null
{
return $this->company_name;
}마찬가지로 stripeEmail, stripePhone, stripeAddress, stripePreferredLocales 메서드도 오버라이드할 수 있습니다. 이 메서드들은 Stripe 고객 업데이트 API의 각 파라미터에 매핑됩니다. 자동 데이터 동기화를 완전히 직접 제어하고 싶다면 syncStripeCustomerDetails 메서드 자체를 오버라이드할 수도 있습니다.
청구 포털
Stripe는 청구 포털을 제공하여 고객이 직접 구독, 결제 수단, 인보이스를 관리할 수 있도록 합니다. redirectToBillingPortal 메서드를 호출하면 고객을 청구 포털로 리다이렉트합니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware('auth');NOTE
Stripe의 청구 포털을 사용하기 전에 Stripe 대시보드에서 청구 포털을 먼저 활성화해야 합니다.
인자로 전달한 URL은 포털 이용을 마친 고객이 돌아올 주소입니다. URL을 지정하지 않으면 애플리케이션의 홈으로 이동합니다.
billingPortalUrl 메서드를 사용하면 리다이렉트 대신 포털 URL 문자열을 직접 얻을 수 있습니다.
$url = $request->user()->billingPortalUrl(route('dashboard'));결제 수단
결제 수단 저장
Stripe를 통해 구독을 생성하거나 단건 결제를 진행하려면 결제 수단을 저장하고 Stripe에서 해당 ID를 가져와야 합니다. 구독 결제와 단건 결제 각각에 사용할 메서드가 다르니 아래에서 구분하여 살펴봅니다.
구독 결제용 결제 수단
향후 구독에서 사용할 목적으로 고객의 카드를 저장하려면, Stripe의 SetupIntent API를 사용해 결제 수단 정보를 안전하게 수집해야 합니다. SetupIntent는 고객의 결제 수단을 저장하기 위한 인텐트(의도)를 Stripe에 알리는 객체입니다.
Cashier에서 제공하는 createSetupIntent 메서드를 호출하면 SetupIntent를 생성하고 뷰에 전달할 수 있습니다.
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);SetupIntent를 뷰로 전달한 뒤, Stripe.js를 사용해 결제 수단 입력 폼을 구성합니다. 아래는 예시입니다.
<input id="card-holder-name" type="text">
<!-- Stripe Elements 자리 표시자 -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
결제 수단 저장
</button>다음으로 Stripe.js 라이브러리를 로드하고 Stripe Elements로 카드 폼을 초기화합니다.
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('여기에-Stripe-공개키를-입력하세요');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// 오류 메시지를 사용자에게 표시합니다...
} else {
// 카드가 성공적으로 등록되었습니다...
}
});
</script>Stripe가 카드 정보를 확인하면 setupIntent.payment_method 값에 결제 수단 ID가 담깁니다. 이 값을 서버로 전송하여 고객 계정에 결제 수단을 연결합니다.
$user->addPaymentMethod($paymentMethodId);NOTE
SetupIntent와 결제 수단 저장에 대한 더 자세한 내용은 Stripe 공식 문서를 참고하세요.
단건 결제용 결제 수단
단건 결제를 위해 고객의 카드를 한 번만 청구할 때는 SetupIntent 없이 Payment Intent만 사용하면 됩니다. 자세한 내용은 단건 결제 섹션을 참고하세요.
결제 수단 조회
Billable 모델 인스턴스의 paymentMethods 메서드는 등록된 모든 결제 수단의 컬렉션을 반환합니다.
$paymentMethods = $user->paymentMethods();특정 타입의 결제 수단만 조회하려면 인자로 타입을 전달합니다.
$paymentMethods = $user->paymentMethods('sepa_debit');기본(default) 결제 수단만 가져오려면 defaultPaymentMethod 메서드를 사용합니다.
$paymentMethod = $user->defaultPaymentMethod();특정 결제 수단을 ID로 조회하려면 findPaymentMethod 메서드를 사용합니다.
$paymentMethod = $user->findPaymentMethod($paymentMethodId);결제 수단 존재 여부 확인
고객 계정에 기본 결제 수단이 등록되어 있는지 확인하려면 hasDefaultPaymentMethod 메서드를 사용합니다.
if ($user->hasDefaultPaymentMethod()) {
// ...
}적어도 하나 이상의 결제 수단이 등록되어 있는지 확인하려면 hasPaymentMethod 메서드를 사용합니다.
if ($user->hasPaymentMethod()) {
// ...
}기본 결제 수단 변경
updateDefaultPaymentMethod 메서드는 고객의 기본 결제 수단을 업데이트합니다. 인자로 Stripe 결제 수단 ID를 전달합니다.
$user->updateDefaultPaymentMethod($paymentMethodId);Stripe의 기본 결제 수단 정보와 애플리케이션 DB를 동기화하려면 updateDefaultPaymentMethodFromStripe 메서드를 사용합니다.
$user->updateDefaultPaymentMethodFromStripe();WARNING
고객의 기본 결제 수단은 구독 청구와 새 인보이스 발행에만 사용됩니다. Stripe의 제한으로 인해 단건 결제에는 기본 결제 수단이 사용되지 않습니다.
결제 수단 추가
이미 Stripe에 등록된 결제 수단 ID를 가지고 있다면 addPaymentMethod 메서드로 고객에게 결제 수단을 추가할 수 있습니다.
$user->addPaymentMethod($paymentMethodId);NOTE
결제 수단 ID를 얻는 방법은 결제 수단 저장 문서를 참고하세요.
결제 수단 삭제
결제 수단을 삭제하려면 Laravel\Cashier\PaymentMethod 인스턴스에서 delete 메서드를 호출합니다.
$paymentMethod->delete();deletePaymentMethod 메서드를 사용하면 특정 결제 수단을 Billable 모델에서 바로 삭제할 수 있습니다.
$user->deletePaymentMethod($paymentMethodId);deletePaymentMethods 메서드는 고객의 모든 결제 수단을 삭제합니다.
$user->deletePaymentMethods();특정 타입의 결제 수단만 삭제하려면 타입을 인자로 전달합니다.
$user->deletePaymentMethods('sepa_debit');WARNING
활성 구독이 있는 고객의 기본 결제 수단을 삭제하면 이후 자동 청구가 실패할 수 있습니다. 삭제 전에 고객에게 적절히 안내하세요.
구독
구독 기능은 Cashier의 핵심입니다. 아래에서 구독 생성부터 상태 관리, 변경, 취소까지 전반적인 과정을 살펴봅니다.
구독 생성
구독을 생성하려면 먼저 Billable 모델 인스턴스를 가져옵니다. 보통 인증된 사용자입니다. 모델 인스턴스를 얻은 후 newSubscription 메서드로 구독을 생성합니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});newSubscription의 첫 번째 인자는 구독의 내부 이름입니다. 단일 구독만 제공한다면 default나 primary 같은 이름을 사용하면 됩니다. 이 이름은 내부 식별자이며 사용자에게는 보이지 않습니다. 두 번째 인자는 Stripe에서 가입할 요금제의 가격 ID입니다.
create 메서드는 Stripe 결제 수단 ID 또는 Stripe PaymentMethod 객체를 인자로 받아 구독을 시작하고 DB의 고객 ID와 관련 정보를 업데이트합니다.
WARNING
결제 수단 ID를 create 메서드에 직접 전달하면, 해당 결제 수단이 고객의 저장된 결제 수단 목록에도 자동으로 추가됩니다.
인보이스 이메일을 통한 구독료 수집
자동 청구 대신, 구독 생성 시 Stripe가 고객에게 인보이스를 이메일로 발송하도록 설정할 수도 있습니다. 이 경우 고객은 인보이스를 받은 후 직접 결제합니다. 이메일 인보이스 청구 방식은 결제 수단을 미리 수집할 필요가 없습니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();인보이스 결제 기한은 days_until_due 옵션으로 지정합니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);수량(Quantity)
구독 시작 시 특정 가격에 대한 수량을 지정하려면 구독 빌더에서 quantity 메서드를 호출합니다.
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethodId);추가 정보 지정
Stripe가 지원하는 고객 생성과 구독 생성 옵션을 추가로 지정하려면 create 메서드의 두 번째, 세 번째 인자로 배열을 전달합니다.
$user->newSubscription('default', 'price_monthly')->create($paymentMethodId, [
'email' => $email,
], [
'metadata' => ['order_id' => 1234],
]);쿠폰 적용
구독 생성 시 쿠폰을 적용하려면 withCoupon 메서드를 사용합니다.
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethodId);또는 Stripe 프로모션 코드를 적용하려면 withPromotionCode 메서드를 사용합니다.
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethodId);프로모션 코드 ID는 Stripe 대시보드에 표시되는 API ID를 사용해야 합니다. 고객이 직접 입력하는 코드 문자열과는 다르므로 주의하세요. 코드 문자열로 프로모션 코드 ID를 조회하려면 findPromotionCode 메서드를 사용합니다.
// 코드 문자열로 프로모션 코드 찾기
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// 고객에게 활성화된 프로모션 코드만 조회
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');위 예시에서 반환된 $promotionCode 객체는 Laravel\Cashier\PromotionCode 인스턴스입니다. 이 클래스는 내부적으로 Stripe의 PromotionCode 객체를 래핑합니다. 프로모션 코드와 연관된 쿠폰을 확인하려면 coupon 메서드를 호출하세요.
$coupon = $promotionCode->coupon();구독 추가
이미 기본 결제 수단이 등록된 고객에게 구독을 추가하려면 구독 빌더에서 add 메서드를 호출합니다.
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();Stripe 대시보드에서 구독 생성
Stripe 대시보드에서 직접 구독을 생성할 수도 있습니다. 이 경우 Cashier는 새로 추가된 구독을 동기화하고 default라는 이름을 부여합니다. 대시보드에서 생성된 구독에 할당되는 이름을 변경하려면 웹훅 이벤트 핸들러를 확장하면 됩니다.
또한 Stripe 대시보드에서는 한 가지 타입의 구독만 생성할 수 있습니다. 애플리케이션에서 여러 구독을 제공하고 있다면 대시보드에서 구독을 추가하지 않도록 주의하세요.
마지막으로 Stripe 대시보드를 통해 새 구독이 추가되더라도 Cashier가 중복 처리하지 않도록 항상 고객당 활성 구독이 하나만 있는지 확인하세요.
구독 상태 확인
고객이 구독 중인지 확인하는 방법은 여러 가지입니다. 우선 subscribed 메서드는 고객이 활성 구독 또는 평가판을 사용 중이면 true를 반환합니다.
if ($user->subscribed('default')) {
// ...
}subscribed 메서드는 라우트 미들웨어로도 활용하기 좋습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* 요청을 처리합니다.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 구독하지 않은 사용자는 결제 페이지로 리다이렉트
return redirect('/billing');
}
return $next($request);
}
}고객이 평가판 기간을 포함하여 특정 요금제를 구독 중인지 확인하려면 subscribedToPrice 메서드를 사용합니다.
if ($user->subscribedToPrice('price_monthly', 'default')) {
// ...
}recurring 메서드는 고객이 현재 활성 구독 중이며 평가판 기간이 아님을 확인합니다.
if ($user->subscription('default')->recurring()) {
// ...
}WARNING
사용자가 같은 이름의 구독을 두 개 가지고 있을 경우, subscription 메서드는 항상 가장 최신 구독을 반환합니다. 예를 들어 default라는 이름의 구독 레코드가 두 개라면 더 최근에 생성된 것이 반환됩니다. 이런 상황을 방지하기 위해 구독 이름을 고유하게 관리하세요.
구독 취소 상태
고객이 구독을 취소했지만 아직 "유예 기간(grace period)" 내에 있는지 확인하려면 onGracePeriod 메서드를 사용합니다.
if ($user->subscription('default')->onGracePeriod()) {
// ...
}구독이 완전히 취소되었는지 확인하려면 canceled 메서드를 사용합니다.
if ($user->subscription('default')->canceled()) {
// ...
}미납 상태
구독이 결제 실패로 인해 미납 상태인지 확인하려면 hasIncompletePayment 메서드를 사용합니다.
if ($user->hasIncompletePayment('default')) {
// ...
}미납 상태인 경우 고객을 결제 확인 페이지로 안내하고 latestPayment 식별자를 URL에 포함시켜야 합니다. 다음은 예시입니다.
<a href="/subscription/{{ $user->subscription('default')->latestPayment()->id }}">
결제를 완료해 주세요.
</a>incomplete 상태와 past_due 상태에서도 구독이 활성 상태임을 보여주고 싶다면, Cashier에서 제공하는 keepPastDueSubscriptionsActive와 keepIncompleteSubscriptionsActive 메서드를 사용할 수 있습니다.
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}WARNING
구독이 incomplete 상태에 있을 때는 결제가 확인되기 전까지 요금제 변경이 불가능합니다. 따라서 incomplete 상태일 때 swap이나 updateQuantity 메서드를 호출하면 예외가 발생합니다.
구독 범위(Scope)
대부분의 구독 상태는 쿼리 스코프로도 사용할 수 있어, DB에서 특정 상태의 구독을 손쉽게 조회할 수 있습니다.
// 활성 구독 조회
$subscriptions = Subscription::query()->active()->get();
// 취소된 구독 조회
$subscriptions = Subscription::query()->canceled()->get();
// 유예 기간 내의 구독 조회
$subscriptions = Subscription::query()->onGracePeriod()->get();
// 완전히 취소되지 않은 구독 조회
$subscriptions = Subscription::query()->notCanceled()->get();
// 미납 상태의 구독 조회
$subscriptions = Subscription::query()->pastDue()->get();
// 미완료 상태의 구독 조회
$subscriptions = Subscription::query()->incomplete()->get();
// 평가판 구독 조회
$subscriptions = Subscription::query()->onTrial()->get();
// 평가판이 아닌 구독 조회
$subscriptions = Subscription::query()->notOnTrial()->get();
// 특정 요금제 구독 조회
$subscriptions = Subscription::query()->subscribedToPrice(['price_monthly', 'price_yearly'])->get();요금제 변경
구독 중인 고객의 요금제를 변경하려면 swap 메서드에 새 Stripe 가격 ID를 전달합니다.
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');고객이 평가판 기간 중일 때 swap을 호출하면 평가판 기간이 유지됩니다. 또한 "수량"이 설정되어 있다면 그 수량도 그대로 유지됩니다.
요금제 변경 시 현재 청구 주기의 인보이스를 즉시 발행하려면 swapAndInvoice 메서드를 사용합니다.
$user->subscription('default')->swapAndInvoice('price_yearly');비례 배분(Proration)
기본적으로 Stripe는 요금제 변경 시 금액을 일할 계산합니다. 일할 계산 없이 변경하려면 noProrate 메서드를 사용합니다.
$user->subscription('default')->noProrate()->swap('price_yearly');구독 일할 계산에 대한 자세한 내용은 Stripe 문서를 참고하세요.
WARNING
swapAndInvoice 전에 noProrate를 호출해도 일할 계산에는 영향을 주지 않습니다. 인보이스는 항상 발행됩니다.
구독 수량
일부 구독은 "수량(quantity)"의 영향을 받습니다. 예를 들어 프로젝트 관리 서비스라면 프로젝트 수에 따라 월 9,900원씩 청구될 수 있습니다. incrementQuantity와 decrementQuantity 메서드로 수량을 증감할 수 있습니다.
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 현재 수량에 5를 추가
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 현재 수량에서 5를 감소
$user->subscription('default')->decrementQuantity(5);수량을 특정 값으로 직접 설정하려면 updateQuantity 메서드를 사용합니다.
$user->subscription('default')->updateQuantity(10);일할 계산 없이 수량을 변경하려면 noProrate 메서드를 먼저 호출합니다.
$user->subscription('default')->noProrate()->updateQuantity(10);구독 수량에 대한 더 자세한 내용은 Stripe 문서를 참고하세요.
여러 상품이 포함된 구독
여러 상품이 포함된 구독을 사용하면 하나의 구독에 여러 결제 상품을 포함할 수 있습니다. 예를 들어 기본 요금 9,900원에 라이브 채팅 추가 기능을 7,700원에 제공하는 구독을 만들 수 있습니다. 구독 아이템은 subscription_items 테이블에 저장됩니다.
여러 상품이 포함된 구독을 생성하려면 newSubscription 메서드의 두 번째 인자로 가격 ID 배열을 전달합니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});위 예시에서 고객은 default 구독에 두 개의 가격이 포함된 형태로 구독됩니다. 두 가격은 각각의 청구 주기에 따라 청구됩니다.
필요하다면 price 메서드로 각 아이템의 수량도 지정할 수 있습니다.
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->price('price_chat', 5)
->create($paymentMethodId);기존 구독에 새 가격을 추가하려
Laravel Cashier (Stripe)
소개
Laravel Cashier Stripe는 Stripe의 구독 결제 서비스를 위한 표현력 있고 유창한 인터페이스를 제공합니다. 반복적으로 작성해야 하는 구독 결제 관련 보일러플레이트 코드를 대부분 처리해 줍니다. 기본적인 구독 관리 외에도, Cashier는 쿠폰 적용, 구독 변경, 구독 "수량(quantity)" 관리, 해지 유예 기간, 그리고 인보이스 PDF 생성까지 지원합니다.
Laravel Cashier (Stripe)
Cashier 업그레이드
새 버전의 Cashier로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 검토하시기 바랍니다.
WARNING
Cashier는 예상치 못한 호환성 문제를 방지하기 위해 고정된 Stripe API 버전을 사용합니다. Cashier 15는 Stripe API 버전 2023-10-16을 사용합니다. Stripe API 버전은 새로운 Stripe 기능과 개선 사항을 활용하기 위해 마이너 릴리즈 시점에 업데이트될 수 있습니다.
Laravel Cashier (Stripe)
설치
Composer 패키지 매니저를 사용해 Stripe용 Cashier 패키지를 설치합니다:
composer require laravel/cashier패키지 설치 후, vendor:publish Artisan 명령어로 Cashier의 마이그레이션 파일을 퍼블리시합니다:
php artisan vendor:publish --tag="cashier-migrations"그런 다음 데이터베이스 마이그레이션을 실행합니다:
php artisan migrate마이그레이션을 실행하면 users 테이블에 여러 컬럼이 추가됩니다. 또한 고객의 구독 정보를 저장하는 subscriptions 테이블과, 복수 가격(multiple prices)이 포함된 구독을 위한 subscription_items 테이블이 새로 생성됩니다.
필요하다면 아래 명령어로 Cashier 설정 파일도 퍼블리시할 수 있습니다:
php artisan vendor:publish --tag="cashier-config"마지막으로, Stripe에서 발생하는 모든 이벤트를 Cashier가 올바르게 처리할 수 있도록 Cashier 웹훅 설정을 반드시 완료하세요.
WARNING
Stripe는 Stripe 식별자를 저장하는 컬럼에 대소문자를 구분하는 콜레이션(case-sensitive collation) 사용을 권장합니다. MySQL을 사용하는 경우 stripe_id 컬럼의 콜레이션을 utf8_bin으로 설정해야 합니다. 자세한 내용은 Stripe 공식 문서를 참고하세요.
Laravel Cashier (Stripe)
설정
Billable 모델
Cashier를 사용하기 전에, 결제 대상 모델에 Billable 트레이트를 추가해야 합니다. 일반적으로는 App\Models\User 모델에 추가합니다. 이 트레이트는 구독 생성, 쿠폰 적용, 결제 수단 업데이트 등 자주 사용하는 결제 관련 메서드를 제공합니다:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}Cashier는 기본적으로 Laravel의 App\Models\User 클래스를 결제 모델로 간주합니다. 다른 모델을 사용하고 싶다면, AppServiceProvider의 boot 메서드에서 useCustomerModel 메서드를 호출해 변경할 수 있습니다:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}WARNING
App\Models\User 이외의 모델을 사용하는 경우, Cashier 마이그레이션을 퍼블리시한 뒤 해당 모델의 테이블명에 맞게 수정해야 합니다.
API 키 설정
다음으로, 애플리케이션의 .env 파일에 Stripe API 키를 설정합니다. API 키는 Stripe 대시보드에서 확인할 수 있습니다:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secretWARNING
STRIPE_WEBHOOK_SECRET 환경 변수는 반드시 설정해야 합니다. 이 값은 수신된 웹훅 요청이 실제로 Stripe에서 보낸 것인지 검증하는 데 사용됩니다.
통화 설정
Cashier의 기본 통화는 미국 달러(USD)입니다. .env 파일에서 CASHIER_CURRENCY 환경 변수를 설정해 기본 통화를 변경할 수 있습니다. 예를 들어 원화(KRW)를 사용하려면 다음과 같이 설정합니다:
CASHIER_CURRENCY=krw통화 외에도, 청구서에 금액을 표시할 때 사용할 로케일을 지정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용해 통화 형식을 처리합니다:
CASHIER_CURRENCY_LOCALE=ko_KRWARNING
en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 확장이 설치되어 있어야 합니다.
세금 설정
Stripe Tax를 활용하면 Stripe가 생성하는 모든 청구서에 세금을 자동으로 계산할 수 있습니다. AppServiceProvider의 boot 메서드에서 calculateTaxes 메서드를 호출해 자동 세금 계산을 활성화하세요:
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}자동 세금 계산을 활성화하면, 이후 생성되는 모든 신규 구독과 단건 청구서에 세금이 자동으로 적용됩니다.
이 기능이 올바르게 동작하려면 고객의 이름, 주소, 납세자 번호 등 결제 정보가 Stripe에 동기화되어 있어야 합니다. Cashier가 제공하는 고객 데이터 동기화 및 세금 ID 관련 메서드를 활용해 이 작업을 처리할 수 있습니다.
WARNING
단건 청구 및 단건 청구 체크아웃에는 세금이 자동 계산되지 않습니다.
로깅
Cashier에서 치명적인 Stripe 오류가 발생했을 때 사용할 로그 채널을 지정할 수 있습니다. .env 파일에서 CASHIER_LOGGER 환경 변수를 설정하면 됩니다:
CASHIER_LOGGER=stackStripe API 호출 중 발생하는 예외는 애플리케이션의 기본 로그 채널을 통해 기록됩니다.
커스텀 모델 사용
Cashier가 내부적으로 사용하는 모델을 직접 확장해 사용할 수 있습니다. 대응하는 Cashier 모델을 상속해 커스텀 모델을 정의하면 됩니다:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// 커스텀 로직 추가
}모델을 정의한 뒤, AppServiceProvider의 boot 메서드에서 Cashier에 커스텀 모델을 등록합니다:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}Laravel Cashier (Stripe)
빠른 시작
단일 상품 판매
NOTE
Stripe Checkout을 사용하기 전에, Stripe 대시보드에서 고정 가격의 상품(Product)을 먼저 등록해야 합니다. 또한 Cashier의 웹훅 처리도 반드시 설정해 두세요.
애플리케이션에 결제 기능을 직접 구축하는 일은 생각보다 복잡할 수 있습니다. 하지만 Cashier와 Stripe Checkout을 함께 사용하면, 검증된 결제 흐름을 비교적 간단하게 연동할 수 있습니다.
구독이 아닌 1회성 단건 결제 상품을 판매할 때는 Cashier의 checkout 메서드를 사용해 고객을 Stripe Checkout 페이지로 보냅니다. 고객이 결제를 완료하면 애플리케이션 내 지정한 성공 URL로 리디렉션됩니다.
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
$stripePriceId = 'price_deluxe_album';
$quantity = 1;
return $request->user()->checkout([$stripePriceId => $quantity], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->name('checkout');
Route::view('checkout.success')->name('checkout-success');
Route::view('checkout.cancel')->name('checkout-cancel');checkout 메서드의 첫 번째 인자로 Stripe 가격 식별자(price ID)와 수량을 배열로 전달합니다. Stripe에서 "가격(price)"은 특정 상품에 설정된 판매 단가를 의미합니다.
checkout 메서드는 해당 사용자가 Stripe에 아직 등록되지 않았다면 자동으로 Stripe 고객 레코드를 생성하고, 애플리케이션 DB의 사용자와 연결합니다. 결제 완료 또는 취소 후에는 각각 지정한 URL로 고객이 이동됩니다.
Stripe Checkout에 메타데이터 전달하기
상품을 판매할 때는 보통 애플리케이션 내부의 Cart(장바구니)나 Order(주문) 모델로 주문을 관리합니다. 고객이 결제를 마치고 돌아왔을 때 어떤 주문에 대한 결제인지 파악하려면, Stripe Checkout 세션에 주문 ID 같은 식별 정보를 함께 넘겨야 합니다.
이를 위해 checkout 메서드에 metadata 배열을 전달할 수 있습니다. 아래 예시에서 Cart와 Order 모델은 예시용이며 Cashier가 제공하는 것이 아닙니다. 실제 프로젝트 구조에 맞게 자유롭게 구현하세요.
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
$order = Order::create([
'cart_id' => $cart->id,
'price_ids' => $cart->price_ids,
'status' => 'incomplete',
]);
return $request->user()->checkout($order->price_ids, [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
'metadata' => ['order_id' => $order->id],
]);
})->name('checkout');success_url에 포함된 {CHECKOUT_SESSION_ID}는 Stripe가 자동으로 실제 세션 ID로 치환해 주는 템플릿 변수입니다. 이를 통해 성공 라우트에서 세션 정보를 조회할 수 있습니다.
이제 결제 성공 라우트를 작성합니다. 쿼리 파라미터로 전달된 session_id를 이용해 Stripe 세션을 조회하고, 메타데이터에서 주문 ID를 꺼내 주문 상태를 업데이트합니다.
use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
Route::get('/checkout/success', function (Request $request) {
$sessionId = $request->get('session_id');
if ($sessionId === null) {
return;
}
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
if ($session->payment_status !== 'paid') {
return;
}
$orderId = $session['metadata']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
return view('checkout-success', ['order' => $order]);
})->name('checkout-success');Stripe Checkout 세션 객체에 포함된 전체 데이터 구조는 Stripe 공식 문서를 참고하세요.
구독 서비스 판매
NOTE
Stripe Checkout을 사용하기 전에, Stripe 대시보드에서 고정 가격의 상품(Product)을 먼저 등록해야 합니다. 또한 Cashier의 웹훅 처리도 반드시 설정해 두세요.
구독 결제도 Cashier와 Stripe Checkout을 활용하면 비교적 간단하게 구현할 수 있습니다.
예를 들어, 월간(price_basic_monthly)과 연간(price_basic_yearly) 요금제를 제공하는 "Basic" 플랜(pro_basic)과 고급 플랜인 "Expert"(pro_expert)가 있다고 가정해 보겠습니다.
고객이 가격 안내 페이지에서 "구독하기" 버튼을 클릭하면, 아래 라우트로 이동해 Stripe Checkout 세션이 생성됩니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_basic_monthly')
->trialDays(5)
->allowPromotionCodes()
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});결제가 완료되거나 취소되면 고객은 checkout 메서드에 전달한 URL로 돌아옵니다. 일부 결제 수단은 처리에 수 초가 걸리기 때문에, 구독 시작 시점을 정확히 파악하려면 Cashier 웹훅 처리 설정이 필요합니다.
구독 여부에 따라 특정 기능을 제한하고 싶다면, Cashier의 Billable 트레이트가 제공하는 subscribed 메서드를 활용하면 됩니다.
@if ($user->subscribed())
<p>현재 구독 중입니다.</p>
@endif특정 상품이나 가격에 구독 중인지도 확인할 수 있습니다.
@if ($user->subscribedToProduct('pro_basic'))
<p>Basic 플랜을 구독 중입니다.</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>Basic 월간 요금제를 구독 중입니다.</p>
@endif구독 확인 미들웨어 만들기
구독 중인 사용자만 접근할 수 있는 라우트를 보호하려면, 전용 미들웨어를 만들어 두는 것이 편리합니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* 요청을 처리합니다.
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// 구독되지 않은 사용자를 결제 페이지로 이동
return redirect('/billing');
}
return $next($request);
}
}미들웨어를 정의한 뒤, 보호할 라우트에 적용합니다.
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);고객이 직접 구독 플랜을 관리할 수 있게 하기
고객이 구독 플랜을 변경하거나 결제 수단을 업데이트하고 싶을 때는 Stripe의 Customer Billing Portal을 활용하는 것이 가장 간단합니다. 이 포털은 Stripe가 호스팅하는 UI로, 고객이 청구서 다운로드, 결제 수단 변경, 구독 플랜 변경 등을 직접 처리할 수 있습니다.
먼저 Blade 템플릿에 포털로 이동하는 링크를 추가합니다.
<a href="{{ route('billing') }}">
결제 관리
</a>그런 다음, 포털 세션을 시작하고 고객을 리디렉션하는 라우트를 정의합니다. redirectToBillingPortal 메서드에는 포털 이용 후 돌아올 URL을 전달합니다.
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');NOTE
Cashier 웹훅 처리를 설정해 두면, Stripe에서 발생한 변경 사항이 애플리케이션 DB에 자동으로 반영됩니다. 예를 들어, 고객이 Stripe Customer Billing Portal에서 구독을 직접 취소하면, Cashier가 해당 웹훅을 수신해 DB의 구독 상태를 "cancelled"로 업데이트합니다.
Laravel Cashier (Stripe)
고객 관리
고객 조회
Stripe ID로 청구 가능한 고객을 조회하려면 Cashier::findBillable 메서드를 사용합니다. 이 메서드는 빌러블 모델 인스턴스를 반환합니다.
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);고객 생성
구독을 바로 시작하지 않고 Stripe에 고객 정보만 먼저 등록하고 싶을 때는 createAsStripeCustomer 메서드를 사용합니다.
$stripeCustomer = $user->createAsStripeCustomer();Stripe에 고객이 생성된 후에는 원하는 시점에 구독을 시작할 수 있습니다. Stripe API가 지원하는 고객 생성 파라미터를 추가로 전달하려면 $options 배열을 인자로 넘기면 됩니다.
$stripeCustomer = $user->createAsStripeCustomer($options);빌러블 모델에 해당하는 Stripe 고객 객체를 반환하려면 asStripeCustomer 메서드를 사용합니다.
$stripeCustomer = $user->asStripeCustomer();해당 모델이 이미 Stripe에 등록된 고객인지 불분명한 상황에서는 createOrGetStripeCustomer 메서드가 유용합니다. Stripe에 고객이 없으면 새로 생성하고, 이미 있으면 기존 객체를 반환합니다.
$stripeCustomer = $user->createOrGetStripeCustomer();고객 정보 수정
Stripe 고객 정보를 직접 수정하려면 updateStripeCustomer 메서드를 사용합니다. Stripe API가 지원하는 고객 수정 옵션 배열을 인자로 전달합니다.
$stripeCustomer = $user->updateStripeCustomer($options);잔액 관리
Stripe는 고객의 "잔액(balance)"을 증감할 수 있는 기능을 제공합니다. 이 잔액은 이후 생성되는 인보이스에 자동으로 반영됩니다. 현재 잔액을 확인하려면 빌러블 모델의 balance 메서드를 사용합니다. 반환값은 고객의 통화 단위에 맞게 포맷된 문자열입니다.
$balance = $user->balance();고객 잔액을 증가(크레딧)시키려면 creditBalance 메서드에 금액을 전달합니다. 선택적으로 설명도 추가할 수 있습니다.
$user->creditBalance(500, '프리미엄 고객 충전');잔액을 차감(데빗)하려면 debitBalance 메서드를 사용합니다.
$user->debitBalance(300, '이용 약관 위반 패널티');applyBalance 메서드는 내부적으로 고객 잔액 트랜잭션을 새로 생성합니다. balanceTransactions 메서드로 이 트랜잭션 기록을 조회하면, 고객에게 크레딧/데빗 내역을 보여주는 페이지를 구성하는 데 활용할 수 있습니다.
// 모든 트랜잭션 조회
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// 트랜잭션 금액
$amount = $transaction->amount(); // 예: $2.31
// 관련 인보이스가 있을 경우 조회
$invoice = $transaction->invoice();
}세금 ID 관리
Cashier는 고객의 세금 ID를 간편하게 관리할 수 있는 기능을 제공합니다. 예를 들어, taxIds 메서드로 고객에게 등록된 모든 세금 ID 목록을 컬렉션으로 가져올 수 있습니다.
$taxIds = $user->taxIds();식별자로 특정 세금 ID를 조회할 수도 있습니다.
$taxId = $user->findTaxId('txi_belgium');새 세금 ID를 등록하려면 유효한 타입과 값을 createTaxId 메서드에 전달합니다.
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');createTaxId 메서드를 호출하면 VAT ID가 즉시 고객 계정에 추가됩니다. VAT ID 검증은 Stripe에서 비동기로 처리됩니다. 검증 결과를 받으려면 customer.tax_id.updated 웹훅 이벤트를 구독하고 VAT ID의 verification 파라미터를 확인하면 됩니다. 웹훅 처리에 대한 자세한 내용은 웹훅 핸들러 정의 문서를 참고하세요.
세금 ID를 삭제하려면 deleteTaxId 메서드를 사용합니다.
$user->deleteTaxId('txi_belgium');Stripe와 고객 데이터 동기화
애플리케이션 사용자가 이름, 이메일 등 Stripe에도 저장된 정보를 변경하면, 변경 내용을 Stripe에도 반영해야 데이터가 일치하게 됩니다.
이를 자동화하려면 빌러블 모델에 updated 이벤트 리스너를 등록하고, 리스너 안에서 syncStripeCustomerDetails 메서드를 호출합니다.
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* 모델의 "booted" 메서드
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}이제 고객 모델이 변경될 때마다 Stripe에 자동으로 동기화됩니다. 참고로 Cashier는 고객이 최초 생성될 때도 자동으로 Stripe에 정보를 동기화합니다.
동기화에 사용할 컬럼은 Cashier가 제공하는 메서드를 오버라이드해서 커스터마이즈할 수 있습니다. 예를 들어 고객의 "이름"으로 사용할 속성을 변경하려면 stripeName 메서드를 오버라이드합니다.
/**
* Stripe에 동기화할 고객 이름을 반환합니다.
*/
public function stripeName(): string|null
{
return $this->company_name;
}같은 방식으로 stripeEmail, stripePhone, stripeAddress, stripePreferredLocales 메서드도 오버라이드할 수 있습니다. 각 메서드는 Stripe 고객 객체 수정 시 해당 파라미터에 매핑됩니다. 동기화 과정 전체를 직접 제어하고 싶다면 syncStripeCustomerDetails 메서드 자체를 오버라이드하세요.
빌링 포털
Stripe는 고객이 구독, 결제 수단, 청구 내역을 직접 관리할 수 있는 빌링 포털을 제공합니다. 컨트롤러나 라우트에서 빌러블 모델의 redirectToBillingPortal 메서드를 호출하면 사용자를 빌링 포털로 리다이렉트할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});기본적으로 사용자가 구독 관리를 마치면 Stripe 빌링 포털 내 링크를 통해 애플리케이션의 home 라우트로 돌아오게 됩니다. 돌아올 URL을 직접 지정하려면 redirectToBillingPortal 메서드에 URL을 인자로 전달합니다.
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});HTTP 리다이렉트 응답 없이 빌링 포털 URL만 생성하려면 billingPortalUrl 메서드를 사용합니다.
$url = $request->user()->billingPortalUrl(route('billing'));결제 수단
결제 수단 저장
구독을 생성하거나 단건 결제를 처리하려면 Stripe에서 결제 수단을 저장하고 해당 식별자를 가져와야 합니다. 구독용 결제 수단과 단건 결제용 결제 수단은 처리 방식이 다르므로, 각각 나누어 설명합니다.
구독용 결제 수단
고객의 카드 정보를 구독에 재사용하기 위해 저장할 때는 Stripe의 "Setup Intents" API를 사용해야 합니다. Setup Intent는 고객의 결제 수단을 나중에 청구하겠다는 의사를 Stripe에 알리는 역할을 합니다. Cashier의 Billable 트레이트에는 이를 쉽게 생성할 수 있는 createSetupIntent 메서드가 포함되어 있습니다. 결제 수단 입력 폼을 렌더링하는 라우트 또는 컨트롤러에서 이 메서드를 호출하세요:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);Setup Intent를 생성한 뒤 뷰에 전달했으면, 폼의 버튼 등 적절한 요소에 client_secret을 연결합니다. 예를 들어, 다음과 같이 "결제 수단 변경" 폼을 구성할 수 있습니다:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 위치 -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
결제 수단 변경
</button>다음으로 Stripe.js 라이브러리를 사용하여 폼에 Stripe Element를 연결하고 고객의 결제 정보를 안전하게 수집합니다:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>이제 Stripe의 confirmCardSetup 메서드를 사용하여 카드를 인증하고 안전한 "결제 수단 식별자"를 가져올 수 있습니다:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// 사용자에게 error.message를 표시합니다...
} else {
// 카드 인증이 성공했습니다...
}
});Stripe에서 카드 인증이 완료되면, 결과로 받은 setupIntent.payment_method 식별자를 Laravel 애플리케이션으로 전달합니다. 이 식별자는 새 결제 수단 추가, 기본 결제 수단 변경, 또는 새 구독 생성에 즉시 사용할 수 있습니다.
NOTE
Setup Intent 및 고객 결제 정보 수집에 대한 자세한 내용은 Stripe 공식 가이드를 참고하세요.
단건 결제용 결제 수단
단건 결제의 경우, 결제 수단 식별자를 일회성으로만 사용하면 됩니다. Stripe의 정책상 고객의 저장된 기본 결제 수단은 단건 결제에 사용할 수 없으므로, 반드시 Stripe.js 라이브러리를 통해 고객이 결제 정보를 직접 입력하도록 해야 합니다. 예를 들어 다음과 같은 폼을 사용할 수 있습니다:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 위치 -->
<div id="card-element"></div>
<button id="card-button">
결제 처리
</button>폼을 정의한 후, Stripe.js 라이브러리로 Stripe Element를 연결하여 결제 정보를 안전하게 수집합니다:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>이어서 Stripe의 createPaymentMethod 메서드로 카드를 인증하고 결제 수단 식별자를 가져옵니다:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// 사용자에게 error.message를 표시합니다...
} else {
// 카드 인증이 성공했습니다...
}
});카드 인증이 성공하면 paymentMethod.id를 Laravel 애플리케이션으로 전달하여 단건 결제를 처리할 수 있습니다.
결제 수단 조회
빌링 모델 인스턴스의 paymentMethods 메서드는 Laravel\Cashier\PaymentMethod 인스턴스의 컬렉션을 반환합니다:
$paymentMethods = $user->paymentMethods();기본적으로 이 메서드는 모든 유형의 결제 수단을 반환합니다. 특정 유형의 결제 수단만 조회하려면 type을 인수로 전달하세요:
$paymentMethods = $user->paymentMethods('sepa_debit');기본 결제 수단을 조회하려면 defaultPaymentMethod 메서드를 사용합니다:
$paymentMethod = $user->defaultPaymentMethod();빌링 모델에 연결된 특정 결제 수단을 조회하려면 findPaymentMethod 메서드를 사용합니다:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);결제 수단 존재 여부 확인
빌링 모델에 기본 결제 수단이 등록되어 있는지 확인하려면 hasDefaultPaymentMethod 메서드를 사용합니다:
if ($user->hasDefaultPaymentMethod()) {
// ...
}결제 수단이 하나라도 등록되어 있는지 확인하려면 hasPaymentMethod 메서드를 사용합니다:
if ($user->hasPaymentMethod()) {
// ...
}특정 유형의 결제 수단이 존재하는지 확인하려면 type을 인수로 전달할 수 있습니다:
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}기본 결제 수단 변경
updateDefaultPaymentMethod 메서드로 고객의 기본 결제 수단을 변경할 수 있습니다. Stripe 결제 수단 식별자를 전달하면 해당 결제 수단이 기본값으로 설정됩니다:
$user->updateDefaultPaymentMethod($paymentMethod);애플리케이션의 기본 결제 수단 정보를 Stripe의 최신 정보와 동기화하려면 updateDefaultPaymentMethodFromStripe 메서드를 사용합니다:
$user->updateDefaultPaymentMethodFromStripe();WARNING
고객의 기본 결제 수단은 인보이스 발행과 신규 구독 생성에만 사용할 수 있습니다. Stripe의 정책상 단건 결제에는 사용할 수 없습니다.
결제 수단 추가
빌링 모델에 새 결제 수단을 추가하려면 addPaymentMethod 메서드에 결제 수단 식별자를 전달합니다:
$user->addPaymentMethod($paymentMethod);NOTE
결제 수단 식별자를 가져오는 방법은 결제 수단 저장 문서를 참고하세요.
결제 수단 삭제
결제 수단을 삭제하려면 Laravel\Cashier\PaymentMethod 인스턴스의 delete 메서드를 호출합니다:
$paymentMethod->delete();특정 결제 수단을 빌링 모델에서 삭제하려면 deletePaymentMethod 메서드를 사용합니다:
$user->deletePaymentMethod('pm_visa');빌링 모델의 모든 결제 수단을 삭제하려면 deletePaymentMethods 메서드를 사용합니다:
$user->deletePaymentMethods();기본적으로 이 메서드는 모든 유형의 결제 수단을 삭제합니다. 특정 유형의 결제 수단만 삭제하려면 type을 인수로 전달하세요:
$user->deletePaymentMethods('sepa_debit');WARNING
사용자에게 활성 구독이 있는 경우, 기본 결제 수단을 삭제할 수 없도록 애플리케이션에서 반드시 방지해야 합니다.
구독 (Subscriptions)
구독은 고객에게 정기 결제를 설정하는 방법입니다. Cashier로 관리하는 Stripe 구독은 여러 구독 가격, 수량, 체험 기간 등 다양한 기능을 지원합니다.
구독 생성
구독을 생성하려면 먼저 청구 가능한 모델 인스턴스를 가져옵니다. 일반적으로 App\Models\User 인스턴스가 됩니다. 모델 인스턴스를 가져온 후, newSubscription 메서드를 사용해 구독을 생성할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});newSubscription 메서드의 첫 번째 인수는 구독의 내부 타입입니다. 애플리케이션에 구독이 하나뿐이라면 default 또는 primary로 지정하면 됩니다. 이 구독 타입은 애플리케이션 내부용이며, 사용자에게 노출되지 않습니다. 공백이 포함되어서는 안 되며, 구독 생성 이후에는 절대 변경하지 마십시오. 두 번째 인수는 사용자가 구독할 Stripe 가격의 식별자입니다.
create 메서드는 Stripe 결제 수단 식별자 또는 Stripe PaymentMethod 객체를 인수로 받아 구독을 시작하고, 청구 모델의 Stripe 고객 ID 및 관련 결제 정보를 데이터베이스에 저장합니다.
WARNING
결제 수단 식별자를 create 메서드에 직접 전달하면, 해당 결제 수단이 사용자의 저장된 결제 수단 목록에 자동으로 추가됩니다.
청구서 이메일을 통한 정기 결제 수집
자동으로 정기 결제를 수집하는 대신, 결제 기일마다 Stripe가 고객에게 청구서를 이메일로 발송하도록 설정할 수 있습니다. 고객은 청구서를 받은 후 직접 결제합니다. 이 방식에서는 구독 생성 시 결제 수단을 미리 등록할 필요가 없습니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();고객이 청구서를 납부하지 않아 구독이 취소되기까지의 기간은 days_until_due 옵션으로 설정합니다. 기본값은 30일이며, 필요에 따라 직접 지정할 수 있습니다.
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);수량 지정
구독 생성 시 특정 수량을 설정하려면, 구독을 생성하기 전에 빌더에서 quantity 메서드를 호출하십시오.
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);추가 옵션 지정
Stripe에서 지원하는 고객 또는 구독 옵션을 추가로 지정하려면, create 메서드의 두 번째와 세 번째 인수로 전달하십시오.
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => '추가 메모 정보'],
]);쿠폰 적용
구독 생성 시 쿠폰을 적용하려면 withCoupon 메서드를 사용하십시오.
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);Stripe 프로모션 코드를 적용하려면 withPromotionCode 메서드를 사용하십시오.
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);여기서 전달하는 프로모션 코드 ID는 Stripe API ID여야 하며, 고객에게 보여지는 프로모션 코드 문자열이 아닙니다. 고객용 코드 문자열로 API ID를 조회해야 한다면 findPromotionCode 메서드를 사용하십시오.
// 고객용 코드로 프로모션 코드 ID 조회...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// 활성 상태의 프로모션 코드만 조회...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');반환된 $promotionCode 객체는 Laravel\Cashier\PromotionCode 인스턴스로, 내부적으로 Stripe\PromotionCode 객체를 감쌉니다. coupon 메서드를 호출하면 해당 프로모션 코드에 연결된 쿠폰을 가져올 수 있습니다.
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();쿠폰 인스턴스를 통해 할인 금액의 유형(정률 또는 정액)을 확인할 수 있습니다.
if ($coupon->isPercentage()) {
return $coupon->percentOff().'%'; // 예: 21.5%
} else {
return $coupon->amountOff(); // 예: 5900원
}현재 고객 또는 구독에 적용된 할인 정보도 조회할 수 있습니다.
$discount = $billable->discount();
$discount = $subscription->discount();반환된 Laravel\Cashier\Discount 인스턴스는 Stripe\Discount 객체를 감쌉니다. coupon 메서드로 해당 할인에 연결된 쿠폰을 조회할 수 있습니다.
$coupon = $subscription->discount()->coupon();고객 또는 구독에 새 쿠폰이나 프로모션 코드를 적용하려면 applyCoupon 또는 applyPromotionCode 메서드를 사용하십시오.
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');고객용 코드 문자열이 아닌 Stripe API ID를 사용해야 한다는 점을 기억하십시오. 한 번에 하나의 쿠폰 또는 프로모션 코드만 적용할 수 있습니다.
자세한 내용은 Stripe의 쿠폰 및 프로모션 코드 문서를 참고하십시오.
기존 고객에게 구독 추가
기본 결제 수단이 이미 등록된 고객에게 구독을 추가하려면 구독 빌더에서 add 메서드를 호출하십시오.
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();Stripe 대시보드에서 구독 생성
Stripe 대시보드에서 직접 구독을 생성할 수도 있습니다. 이 경우 Cashier는 새로 추가된 구독을 동기화하고 default 타입을 부여합니다. 대시보드에서 생성된 구독에 다른 타입을 부여하려면 웹훅 이벤트 핸들러를 정의하십시오.
한 가지 주의할 점은, Stripe 대시보드에서는 한 번에 하나의 구독 타입만 생성할 수 있습니다. 애플리케이션이 여러 타입의 구독을 제공하는 경우, 대시보드를 통해서는 하나의 타입만 추가할 수 있습니다.
또한, 동일 타입의 구독이 여러 개 활성화되지 않도록 주의하십시오. 예를 들어, 고객에게 default 타입 구독이 두 개 있다면 Cashier는 가장 최근에 추가된 구독만 사용하며, 두 구독 모두 데이터베이스에 동기화됩니다.
구독 상태 확인
고객이 구독을 시작한 후에는 다양한 메서드를 통해 구독 상태를 손쉽게 확인할 수 있습니다.
subscribed 메서드는 고객이 활성 구독을 보유하고 있으면 true를 반환합니다. 체험 기간 중에도 true를 반환합니다. 첫 번째 인수로 구독 타입을 전달합니다.
if ($user->subscribed('default')) {
// ...
}subscribed 메서드는 라우트 미들웨어로도 유용하게 활용할 수 있습니다. 구독 여부에 따라 라우트 및 컨트롤러 접근을 제어할 수 있습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* 들어오는 요청을 처리합니다.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 유료 구독자가 아닌 경우...
return redirect('billing');
}
return $next($request);
}
}사용자가 체험 기간 중인지 확인하려면 onTrial 메서드를 사용하십시오. 체험 기간 안내 문구를 표시할 때 유용합니다.
if ($user->subscription('default')->onTrial()) {
// ...
}subscribedToProduct 메서드는 Stripe 상품 식별자를 기반으로 사용자가 특정 상품을 구독 중인지 확인합니다. Stripe에서 상품은 여러 가격의 묶음입니다. 아래 예시는 사용자의 default 구독이 "premium" 상품에 활성화되어 있는지 확인합니다.
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}배열을 전달하면 여러 상품 중 하나에 구독 중인지 확인할 수 있습니다.
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}subscribedToPrice 메서드는 특정 가격 ID에 해당하는 구독이 활성화되어 있는지 확인합니다.
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}recurring 메서드는 사용자가 현재 구독 중이며 체험 기간이 종료된 상태인지 확인합니다.
if ($user->subscription('default')->recurring()) {
// ...
}WARNING
동일한 타입의 구독이 여러 개인 경우, subscription 메서드는 항상 가장 최근 구독을 반환합니다. 예를 들어 default 타입 구독 레코드가 두 개라면, 하나는 만료된 이전 구독이고 다른 하나는 현재 활성 구독일 수 있습니다. 가장 최근 구독이 반환되며, 오래된 구독은 이력 조회를 위해 데이터베이스에 보존됩니다.
취소된 구독 상태
사용자가 구독을 취소했는지 확인하려면 canceled 메서드를 사용하십시오.
if ($user->subscription('default')->canceled()) {
// ...
}구독을 취소했지만 아직 "유예 기간(grace period)" 중인지도 확인할 수 있습니다. 예를 들어, 3월 5일에 만료 예정인 구독을 3월 1일에 취소한 경우, 3월 5일까지는 유예 기간이 적용됩니다. 이 기간 동안 subscribed 메서드는 여전히 true를 반환합니다.
if ($user->subscription('default')->onGracePeriod()) {
// ...
}구독이 완전히 종료되었는지(취소 후 유예 기간도 지난 상태) 확인하려면 ended 메서드를 사용하십시오.
if ($user->subscription('default')->ended()) {
// ...
}미완료(Incomplete) 및 연체(Past Due) 상태
구독 생성 후 추가적인 결제 인증이 필요한 경우, 구독은 incomplete 상태로 표시됩니다. 구독 상태는 Cashier의 subscriptions 테이블 stripe_status 컬럼에 저장됩니다.
마찬가지로, 가격 변경 시 추가 결제 인증이 필요한 경우에는 구독이 past_due 상태로 표시됩니다. 두 상태 모두 고객이 결제를 완료하기 전까지는 구독이 활성화되지 않습니다. 결제 미완료 여부는 hasIncompletePayment 메서드로 확인합니다.
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}결제 미완료 구독이 있을 경우, 사용자를 Cashier의 결제 확인 페이지로 안내해야 합니다. latestPayment 메서드로 해당 결제 ID를 가져올 수 있습니다.
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
결제를 확인해 주세요.
</a>past_due 또는 incomplete 상태의 구독도 활성 상태로 간주하려면, Cashier에서 제공하는 keepPastDueSubscriptionsActive 및 keepIncompleteSubscriptionsActive 메서드를 사용하십시오. 일반적으로 App\Providers\AppServiceProvider의 register 메서드에서 호출합니다.
use Laravel\Cashier\Cashier;
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}WARNING
구독이 incomplete 상태인 경우 결제가 확인되기 전까지 변경이 불가능합니다. 따라서 incomplete 상태에서 swap 또는 updateQuantity 메서드를 호출하면 예외가 발생합니다.
구독 쿼리 스코프
대부분의 구독 상태는 쿼리 스코프로도 제공되므로, 특정 상태의 구독을 데이터베이스에서 손쉽게 조회할 수 있습니다.
// 모든 활성 구독 조회...
$subscriptions = Subscription::query()->active()->get();
// 특정 사용자의 취소된 구독 조회...
$subscriptions = $user->subscriptions()->canceled()->get();사용 가능한 전체 스코프 목록은 다음과 같습니다.
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();구독 가격 변경
고객이 구독 가격을 변경하려는 경우, swap 메서드에 Stripe 가격 식별자를 전달하십시오. 이전에 취소된 구독이 있다면, 가격 변경 시 자동으로 재활성화됩니다.
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');체험 기간 중인 고객이 가격을 변경하면 체험 기간이 유지됩니다. 수량이 설정되어 있다면 해당 수량도 그대로 유지됩니다.
체험 기간을 건너뛰고 가격을 변경하려면 skipTrial 메서드를 함께 사용하십시오.
$user->subscription('default')
->skipTrial()
->swap('price_yearly');다음 청구 주기를 기다리지 않고 즉시 청구서를 발행하려면 swapAndInvoice 메서드를 사용하십시오.
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');일할 계산(Proration)
기본적으로 Stripe는 가격을 변경할 때 일할 계산을 적용합니다. 일할 계산 없이 가격을 변경하려면 noProrate 메서드를 사용하십시오.
$user->subscription('default')->noProrate()->swap('price_yearly');구독 일할 계산에 대한 자세한 내용은 Stripe 문서를 참고하십시오.
WARNING
swapAndInvoice 메서드 앞에 noProrate 메서드를 호출해도 일할 계산에는 영향을 주지 않습니다. 청구서는 항상 발행됩니다.
구독 수량
구독에 수량 개념이 필요한 경우가 있습니다. 예를 들어, 프로젝트 관리 서비스에서 프로젝트당 월 1만 원을 청구하는 방식입니다. incrementQuantity 및 decrementQuantity 메서드로 구독 수량을 쉽게 조정할 수 있습니다.
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 구독 수량을 5 증가...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 구독 수량을 5 감소...
$user->subscription('default')->decrementQuantity(5);특정 수량으로 직접 설정하려면 updateQuantity 메서드를 사용하십시오.
$user->subscription('default')->updateQuantity(10);일할 계산 없이 수량을 변경하려면 noProrate 메서드를 함께 사용하십시오.
$user->subscription('default')->noProrate()->updateQuantity(10);구독 수량에 대한 자세한 내용은 Stripe 문서를 참고하십시오.
다중 상품 구독의 수량
다중 상품 구독에서 특정 가격의 수량을 증감할 때는, 가격 ID를 두 번째 인수로 전달하십시오.
$user->subscription('default')->incrementQuantity(1, 'price_chat');다중 상품 구독
다중 상품 구독을 사용하면 하나의 구독에 여러 결제 상품을 연결할 수 있습니다. 예를 들어, 월 1만 원의 기본 구독과 추가로 월 1만 5천 원의 라이브 채팅 부가 상품을 함께 제공하는 고객 서비스 헬프데스크 애플리케이션을 구축할 수 있습니다. 다중 상품 구독 정보는 Cashier의 subscription_items 테이블에 저장됩니다.
newSubscription 메서드의 두 번째 인수로 가격 배열을 전달하여 다중 상품 구독을 생성할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});위 예시에서 고객은 default 구독에 두 가지 가격이 연결되며, 각각의 청구 주기에 따라 청구됩니다. 필요한 경우 quantity 메서드로 각 가격에 대한 수량을 지정할 수 있습니다.
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);기존 구독에 새 가격을 추가하려면 addPrice 메서드를 사용하십시오.
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');위 예시에서는 다음 청구 주기에 새 가격이 청구됩니다. 즉시 청구하려면 addPriceAndInvoice 메서드를 사용하십시오.
$user->subscription('default')->addPriceAndInvoice('price_chat');수량을 지정하여 가격을 추가할 수도 있습니다.
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);구독에서 특정 가격을 제거하려면 removePrice 메서드를 사용하십시오.
$user->subscription('default')->removePrice('price_chat');WARNING
구독의 마지막 남은 가격은 제거할 수 없습니다. 이 경우에는 구독 자체를 취소하십시오.
가격 교체
다중 상품 구독에서 연결된 가격을 변경할 수도 있습니다. 예를 들어, price_basic 구독에 price_chat 부가 상품이 있는 고객을 price_pro로 업그레이드하는 경우를 살펴보겠습니다.
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);위 예시를 실행하면, price_basic에 해당하는 구독 항목이 삭제되고 price_chat은 유지되며, price_pro에 대한 새 구독 항목이 생성됩니다.
swap 메서드에 키/값 배열을 전달하여 구독 항목 옵션(예: 수량)을 함께 지정할 수도 있습니다.
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);구독의 단일 가격만 교체하려면 구독 항목 자체에서 swap 메서드를 사용하십시오. 이 방법은 다른 가격의 메타데이터를 그대로 유지하고 싶을 때 특히 유용합니다.
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');일할 계산
기본적으로 Stripe는 다중 상품 구독에서 가격을 추가하거나 제거할 때 일할 계산을 적용합니다. 일할 계산 없이 가격을 조정하려면 noProrate 메서드를 체이닝하십시오.
$user->subscription('default')->noProrate()->removePrice('price_chat');개별 가격 수량 변경
다중 상품 구독에서 특정 가격의 수량을 변경하려면, 기존 수량 메서드에 가격 ID를 추가 인수로 전달하십시오.
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'price_chat');
$user->subscription('default')->decrementQuantity(3, 'price_chat');
$user->subscription('default')->updateQuantity(10, 'price_chat');WARNING
구독에 여러 가격이 있는 경우, Subscription 모델의 stripe_price 및 quantity 속성은 null이 됩니다. 개별 가격 속성에 접근하려면 Subscription 모델의 items 관계를 사용하십시오.
구독 항목(Subscription Items)
구독에 여러 가격이 있는 경우, 데이터베이스의 subscription_items 테이블에 여러 구독 "항목"이 저장됩니다. items 관계를 통해 접근할 수 있습니다.
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// 특정 항목의 Stripe 가격 및 수량 조회...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;findItemOrFail 메서드로 특정 가격의 항목을 조회할 수도 있습니다.
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');다중 구독
Stripe는 고객이 여러 구독을 동시에 보유할 수 있도록 지원합니다. 예를 들어, 헬스장 애플리케이션에서 수영 구독과 웨이트 구독을 별도로 제공하고 각각 다른 가격을 책정할 수 있습니다. 고객은 원하는 구독을 하나 또는 모두 선택할 수 있습니다.
구독을 생성할 때 newSubscription 메서드에 구독 타입을 전달하십시오. 타입은 구독 성격을 나타내는 임의의 문자열로 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});이후 swimming 구독의 가격을 연간 요금제로 변경하거나 취소하는 것도 간단합니다.
$user->subscription('swimming')->swap('price_swimming_yearly');
$user->subscription('swimming')->cancel();사용량 기반 청구 (Metered Billing)
사용량 기반 청구는 청구 주기 동안 고객의 실제 사용량에 따라 요금을 청구합니다. 예를 들어, 월별로 발송한 SMS 또는 이메일 수에 따라 요금을 청구하는 방식입니다.
사용량 기반 청구를 시작하려면 먼저 Stripe 대시보드에서 사용량 기반 가격이 설정된 상품을 생성하고, meteredPrice 메서드로 해당 가격 ID를 구독에 추가하십시오.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});Stripe Checkout을 통해 사용량 기반 구독을 시작할 수도 있습니다.
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);사용량 보고
고객이 애플리케이션을 사용하면 그에 맞게 Stripe에 사용량을 보고해야 정확한 청구가 이루어집니다. reportUsage 메서드로 사용량을 보고하십시오.
$user = User::find(1);
$user->subscription('default')->reportUsage();기본적으로 1 단위의 사용량이 청구 기간에 추가됩니다. 특정 사용량을 지정하려면 숫자를 인수로 전달하십시오.
$user = User::find(1);
$user->subscription('default')->reportUsage(15);구독에 여러 가격이 있다면 reportUsageFor 메서드로 사용량을 보고할 대상 가격을 지정하십시오.
$user = User::find(1);
$user->subscription('default')->reportUsageFor('price_metered', 15);이전에 보고한 사용량을 수정해야 한다면, reportUsage의 두 번째 인수로 타임스탬프 또는 DateTimeInterface 인스턴스를 전달하십시오. Stripe는 해당 시점에 보고된 사용량을 업데이트합니다. 지정한 날짜와 시간이 현재 청구 기간 내에 있는 경우에만 이전 사용량 레코드를 계속 수정할 수 있습니다.
$user = User::find(1);
$user->subscription('default')->reportUsage(5, $timestamp);사용량 레코드 조회
고객의 과거 사용량을 조회하려면 구독 인스턴스의 usageRecords 메서드를 사용하십시오.
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecords();여러 가격이 있는 구독에서 특정 가격의 사용량 레코드를 조회하려면 usageRecordsFor 메서드를 사용하십시오.
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');두 메서드 모두 사용량 레코드의 연관 배열을 담은 컬렉션 인스턴스를 반환합니다. 이를 순회하여 고객의 총 사용량을 표시할 수 있습니다.
@foreach ($usageRecords as $usageRecord)
- 기간 시작: {{ $usageRecord['period']['start'] }}
- 기간 종료: {{ $usageRecord['period']['end'] }}
- 총 사용량: {{ $usageRecord['total_usage'] }}
@endforeach반환되는 전체 사용량 데이터 구조와 Stripe의 커서 기반 페이지네이션 사용법은 Stripe API 공식 문서를 참고하십시오.
구독 세금
WARNING
세율을 직접 계산하는 대신, Stripe Tax를 통한 자동 세금 계산을 사용하는 것을 권장합니다.
구독에 적용할 세율을 지정하려면 청구 가능한 모델에 taxRates 메서드를 구현하고 Stripe 세율 ID 배열을 반환하십시오. 세율은 Stripe 대시보드에서 정의할 수 있습니다.
/**
* 고객 구독에 적용할 세율을 반환합니다.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}taxRates 메서드는 고객별로 세율을 다르게 적용할 수 있어, 여러 국가의 사용자를 대상으로 하는 서비스에 유용합니다.
다중 상품 구독을 제공하는 경우, 청구 모델에 priceTaxRates 메서드를 구현하여 가격별로 다른 세율을 정의할 수 있습니다.
/**
* 고객 구독에 적용할 세율을 반환합니다.
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}WARNING
taxRates 메서드는 구독 청구에만 적용됩니다. 일회성 청구에 Cashier를 사용하는 경우에는 해당 시점에 세율을 직접 지정해야 합니다.
세율 동기화
taxRates 메서드에서 반환하는 세율 ID를 변경해도, 기존 구독의 세금 설정은 그대로 유지됩니다. 기존 구독의 세율도 새 값으로 업데이트하려면 syncTaxRates 메서드를 호출하십시오.
$user->subscription('default')->syncTaxRates();다중 상품 구독의 경우 각 항목 세율도 함께 동기화됩니다. 다중 상품 구독을 제공한다면 청구 모델에 앞서 설명한 priceTaxRates 메서드가 구현되어 있는지 확인하십시오.
세금 면제
Cashier는 고객의 세금 면제 여부를 확인하기 위한 isNotTaxExempt, isTaxExempt, reverseChargeApplies 메서드를 제공합니다. 이 메서드들은 Stripe API를 호출하여 고객의 세금 면제 상태를 조회합니다.
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();WARNING
이 메서드들은 Laravel\Cashier\Invoice 객체에서도 사용할 수 있습니다. 단, Invoice 객체에서 호출하면 청구서가 생성된 시점의 세금 면제 상태를 기준으로 판단합니다.
구독 기준일 (Anchor Date)
기본적으로 청구 주기의 기준일은 구독이 생성된 날짜이며, 체험 기간이 있는 경우에는 체험이 종료되는 날짜가 기준일이 됩니다. 청구 기준일을 변경하려면 anchorBillingCycleOn 메서드를 사용하십시오.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});구독 청구 주기 관리에 대한 자세한 내용은 Stripe 청구 주기 문서를 참고하십시오.
구독 취소
구독을 취소하려면 사용자 구독에서 cancel 메서드를 호출하십시오.
$user->subscription('default')->cancel();구독이 취소되면 Cashier는 subscriptions 테이블의 ends_at 컬럼을 자동으로 설정합니다. 이 컬럼을 기준으로 subscribed 메서드가 false를 반환할 시점이 결정됩니다.
예를 들어, 3월 5일에 종료 예정인 구독을 3월 1일에 취소한 경우, subscribed 메서드는 3월 5일까지 true를 반환합니다. 일반적으로 사용자는 청구 주기가 끝날 때까지 서비스를 계속 이용할 수 있기 때문입니다.
유예 기간 중인지 확인하려면 onGracePeriod 메서드를 사용하십시오.
if ($user->subscription('default')->onGracePeriod()) {
// ...
}즉시 구독을 취소하려면 cancelNow 메서드를 사용하십시오.
$user->subscription('default')->cancelNow();미청구된 사용량 기반 사용량이나 보류 중인 일할 계산 항목에 대한 청구서를 즉시 발행하고 취소하려면 cancelNowAndInvoice 메서드를 사용하십시오.
$user->subscription('default')->cancelNowAndInvoice();특정 시점에 구독을 취소하도록 예약할 수도 있습니다.
$user->subscription('default')->cancelAt(
now()->addDays(10)
);마지막으로, 사용자 모델을 삭제하기 전에 반드시 구독을 취소해야 합니다.
$user->subscription('default')->cancelNow();
$user->delete();구독 재개
취소된 구독을 재개하려면 구독에서 resume 메서드를 호출하십시오. 구독을 재개하려면 고객이 아직 유예 기간 내에 있어야 합니다.
$user->subscription('default')->resume();고객이 구독을 취소했다가 완전히 만료되기 전에 재개하면 즉시 청구되지 않고, 원래 청구 주기에 따라 다음 결제가 이루어집니다.
Laravel Cashier (Stripe)
구독 체험판 (Trial)
결제 수단을 미리 수집하는 체험판
체험 기간을 제공하면서도 결제 수단 정보를 미리 등록받고 싶다면, 구독 생성 시 trialDays 메서드를 사용하세요:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});이 메서드는 데이터베이스의 구독 레코드에 체험 종료일을 기록하고, 해당 날짜까지 Stripe가 고객에게 청구하지 않도록 지시합니다. trialDays를 사용하면 Stripe 대시보드에서 해당 요금제(price)에 설정한 기본 체험 기간은 Cashier가 덮어씁니다.
WARNING
체험 기간이 끝나기 전에 구독을 해지하지 않으면 체험 만료 즉시 청구가 발생합니다. 반드시 사용자에게 체험 종료일을 사전에 안내하세요.
특정 날짜와 시간을 직접 지정하려면 trialUntil 메서드에 DateTime 인스턴스를 전달하세요:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);사용자가 현재 체험 기간 중인지 확인하려면 사용자 인스턴스나 구독 인스턴스의 onTrial 메서드를 사용하면 됩니다. 아래 두 예시는 동일하게 동작합니다:
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}체험 기간을 즉시 종료하려면 endTrial 메서드를 사용하세요:
$user->subscription('default')->endTrial();체험 기간이 이미 만료되었는지 확인하려면 hasExpiredTrial 메서드를 사용하세요:
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}Stripe / Cashier에서 체험 기간 설정하기
체험 기간은 Stripe 대시보드에서 요금제(price)별로 설정하거나, Cashier 코드에서 직접 명시적으로 전달할 수 있습니다.
Stripe 대시보드에서 설정한 경우 한 가지 주의할 점이 있습니다. 과거에 구독 이력이 있는 고객을 포함해, 신규 구독을 생성할 때마다 항상 체험 기간이 적용됩니다. 이를 원하지 않는 경우에는 구독 생성 시 skipTrial() 메서드를 명시적으로 호출해야 합니다.
결제 수단 없이 제공하는 체험판
결제 수단 정보를 먼저 받지 않고 체험 기간을 제공하려면, 사용자 레코드의 trial_ends_at 컬럼에 체험 종료일을 직접 설정하면 됩니다. 보통 회원 가입 시점에 처리합니다:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);WARNING
Billable 모델 클래스 정의에서 trial_ends_at 속성에 반드시 날짜 캐스트를 추가하세요.
Cashier는 이 방식의 체험판을 특정 구독에 연결되지 않은 "범용 체험판(generic trial)" 이라고 부릅니다. Billable 모델 인스턴스의 onTrial 메서드는 현재 날짜가 trial_ends_at 값을 넘지 않았다면 true를 반환합니다:
if ($user->onTrial()) {
// 사용자가 체험 기간 중입니다...
}실제 구독을 생성할 준비가 되면 평소처럼 newSubscription 메서드를 사용하면 됩니다:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);사용자의 체험 종료일을 가져오려면 trialEndsAt 메서드를 사용하세요. 체험 중이라면 Carbon 인스턴스를, 체험 중이 아니라면 null을 반환합니다. 기본 구독이 아닌 특정 구독의 체험 종료일을 조회하려면 구독 타입을 인수로 전달할 수 있습니다:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}실제 구독은 아직 생성하지 않고 범용 체험판 기간 중인지 정확히 확인하고 싶다면 onGenericTrial 메서드를 사용하세요:
if ($user->onGenericTrial()) {
// 사용자가 범용 체험판 기간 중입니다...
}체험 기간 연장
extendTrial 메서드를 사용하면 구독 생성 후에도 체험 기간을 연장할 수 있습니다. 체험이 이미 만료되어 청구가 시작된 고객에게도 연장된 체험 기간을 제공할 수 있으며, 연장된 체험 기간에 해당하는 금액은 다음 청구서에서 차감됩니다:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// 지금으로부터 7일 후에 체험 종료...
$subscription->extendTrial(
now()->addDays(7)
);
// 현재 체험 종료일에서 5일 추가 연장...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);Laravel Cashier (Stripe)
Stripe 웹훅 처리
NOTE
로컬 개발 환경에서 웹훅을 테스트할 때는 Stripe CLI를 활용하면 편리합니다.
Stripe는 다양한 이벤트가 발생할 때 웹훅을 통해 여러분의 애플리케이션에 알림을 전송합니다. Cashier 서비스 프로바이더는 기본적으로 Cashier의 웹훅 컨트롤러를 가리키는 라우트를 자동으로 등록하며, 이 컨트롤러가 모든 수신 웹훅 요청을 처리합니다.
기본적으로 이 컨트롤러는 다음과 같은 일반적인 Stripe 웹훅 이벤트를 자동으로 처리합니다.
- 결제 실패 횟수 초과로 인한 구독 취소 (Stripe 설정에서 정의한 기준에 따름)
- 고객 정보 업데이트 및 삭제
- 구독 변경
- 결제 수단 변경
물론 이 컨트롤러를 확장하면 원하는 어떤 Stripe 웹훅 이벤트도 직접 처리할 수 있습니다.
웹훅이 정상적으로 동작하려면 Stripe 대시보드에서 웹훅 URL을 반드시 설정해야 합니다. Cashier의 웹훅 컨트롤러는 기본적으로 /stripe/webhook 경로로 요청을 수신합니다. Stripe 대시보드에서 활성화해야 할 웹훅 이벤트 목록은 다음과 같습니다.
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
편의를 위해 Cashier는 cashier:webhook Artisan 명령어를 제공합니다. 이 명령어를 실행하면 Cashier가 필요로 하는 모든 이벤트를 수신하는 웹훅이 Stripe에 자동으로 생성됩니다.
php artisan cashier:webhook생성된 웹훅은 기본적으로 .env 파일의 APP_URL 환경 변수와 Cashier가 제공하는 cashier.webhook 라우트를 조합한 URL을 사용합니다. 다른 URL을 사용하려면 --url 옵션을 지정하세요.
php artisan cashier:webhook --url "https://example.com/stripe/webhook"생성되는 웹훅은 현재 Cashier 버전과 호환되는 Stripe API 버전을 사용합니다. 다른 Stripe API 버전을 사용하려면 --api-version 옵션을 지정할 수 있습니다.
php artisan cashier:webhook --api-version="2019-12-03"웹훅은 생성 즉시 활성화됩니다. 생성은 하되 준비가 될 때까지 비활성 상태로 두려면 --disabled 옵션을 사용하세요.
php artisan cashier:webhook --disabledWARNING
외부에서 들어오는 Stripe 웹훅 요청은 반드시 Cashier가 제공하는 웹훅 서명 검증 미들웨어로 보호해야 합니다.
웹훅과 CSRF 보호
Stripe 웹훅 요청은 Laravel의 CSRF 보호를 우회해야 합니다. 이를 위해 App\Http\Middleware\VerifyCsrfToken 미들웨어의 예외 목록에 해당 URI를 추가하거나, 해당 라우트를 web 미들웨어 그룹 바깥에 정의하세요.
protected $except = [
'stripe/*',
];웹훅 이벤트 핸들러 정의
Cashier는 결제 실패로 인한 구독 취소 등 자주 발생하는 Stripe 웹훅 이벤트를 자동으로 처리합니다. 그 외에 직접 처리하고 싶은 이벤트가 있다면, Cashier가 디스패치하는 다음 두 이벤트에 리스너를 등록하면 됩니다.
Laravel\Cashier\Events\WebhookReceived— 웹훅 수신 시Laravel\Cashier\Events\WebhookHandled— 웹훅 처리 완료 시
두 이벤트 모두 Stripe 웹훅의 전체 페이로드를 포함합니다. 예를 들어 invoice.payment_succeeded 웹훅을 처리하려면 다음과 같이 리스너를 정의합니다.
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* 수신된 Stripe 웹훅을 처리합니다.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// 이벤트 처리 로직 작성...
}
}
}리스너를 정의한 후에는 애플리케이션의 EventServiceProvider에 등록합니다.
<?php
namespace App\Providers;
use App\Listeners\StripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
WebhookReceived::class => [
StripeEventListener::class,
],
];
}웹훅 서명 검증
웹훅 요청의 보안을 강화하려면 Stripe의 웹훅 서명 기능을 사용하세요. Cashier는 수신된 Stripe 웹훅 요청의 유효성을 자동으로 검증하는 미들웨어를 기본으로 제공합니다.
웹훅 서명 검증을 활성화하려면 애플리케이션의 .env 파일에 STRIPE_WEBHOOK_SECRET 환경 변수를 설정하세요. 웹훅 시크릿 값은 Stripe 계정 대시보드에서 확인할 수 있습니다.
Laravel Cashier (Stripe)
단건 결제 (Single Charges)
단순 청구
Billable 모델 인스턴스에서 charge 메서드를 사용하면 고객에게 일회성 결제를 요청할 수 있습니다. 두 번째 인수로 결제 수단 식별자를 전달해야 합니다.
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});charge 메서드의 세 번째 인수로 배열을 넘기면, Stripe 결제 생성 시 사용할 추가 옵션을 자유롭게 지정할 수 있습니다. 사용 가능한 옵션 목록은 Stripe 공식 문서를 참고하세요.
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);기존 고객 또는 사용자 없이도 charge 메서드를 사용할 수 있습니다. 이 경우 Billable 모델의 새 인스턴스를 생성한 뒤 메서드를 호출하면 됩니다.
use App\Models\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);결제에 실패하면 charge 메서드는 예외를 던집니다. 결제가 성공하면 Laravel\Cashier\Payment 인스턴스가 반환됩니다.
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}WARNING
charge 메서드의 금액은 애플리케이션에서 사용하는 통화의 최소 단위로 지정해야 합니다. 예를 들어 원화(KRW)는 보조 단위가 없으므로 금액을 그대로 입력하면 되지만, 미국 달러(USD)를 사용한다면 센트(cent) 단위로 입력해야 합니다.
인보이스와 함께 청구
일회성 결제를 처리하면서 고객에게 PDF 인보이스도 함께 제공해야 하는 경우, invoicePrice 메서드를 사용하면 됩니다. 예를 들어 티셔츠 5벌에 대한 인보이스를 발행하는 코드는 다음과 같습니다.
$user->invoicePrice('price_tshirt', 5);인보이스는 즉시 고객의 기본 결제 수단으로 청구됩니다. invoicePrice 메서드의 세 번째 인수로 인보이스 항목에 대한 청구 옵션 배열을, 네 번째 인수로 인보이스 자체에 대한 옵션 배열을 전달할 수 있습니다.
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);invoicePrice와 유사하게, tabPrice 메서드를 사용하면 여러 항목(인보이스당 최대 250개)을 고객의 "탭(tab)"에 추가한 뒤 한 번에 인보이스를 발행할 수 있습니다. 예를 들어 티셔츠 5벌과 머그컵 2개에 대한 인보이스는 다음과 같이 발행합니다.
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();또는 invoiceFor 메서드를 사용해 고객의 기본 결제 수단으로 일회성 청구를 할 수도 있습니다.
$user->invoiceFor('One Time Fee', 500);invoiceFor 메서드도 사용할 수 있지만, 가능하면 Stripe 대시보드에서 미리 정의한 가격(price)을 활용하는 invoicePrice와 tabPrice 메서드 사용을 권장합니다. 미리 정의된 가격을 사용하면 Stripe 대시보드에서 상품별 매출 분석 데이터를 더 효과적으로 활용할 수 있습니다.
WARNING
invoice, invoicePrice, invoiceFor 메서드는 Stripe 인보이스를 생성하며, 결제 실패 시 자동으로 재시도합니다. 재시도를 원하지 않는다면, 첫 번째 결제 실패 후 Stripe API를 통해 인보이스를 직접 닫아야 합니다.
Payment Intent 생성
Billable 모델 인스턴스에서 pay 메서드를 호출하면 새로운 Stripe Payment Intent를 생성할 수 있습니다. 이 메서드는 Laravel\Cashier\Payment 인스턴스로 래핑된 Payment Intent를 반환합니다.
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->pay(
$request->get('amount')
);
return $payment->client_secret;
});Payment Intent를 생성한 후, client_secret 값을 프론트엔드로 반환하면 사용자가 브라우저에서 결제를 완료할 수 있습니다. Stripe Payment Intent를 활용한 전체 결제 흐름 구현 방법은 Stripe 공식 문서를 참고하세요.
pay 메서드를 사용하면 Stripe 대시보드에서 활성화된 기본 결제 수단이 모두 고객에게 제공됩니다. 특정 결제 수단만 허용하고 싶다면 payWith 메서드를 사용하세요.
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->payWith(
$request->get('amount'), ['card', 'bancontact']
);
return $payment->client_secret;
});WARNING
pay 및 payWith 메서드도 마찬가지로 금액을 통화의 최소 단위로 지정해야 합니다. 미국 달러 사용 시에는 센트 단위로 입력하세요.
결제 환불
Stripe 결제를 환불해야 할 경우 refund 메서드를 사용합니다. 첫 번째 인수로 Stripe Payment Intent ID를 전달하면 됩니다.
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);Laravel Cashier (Stripe)
인보이스
인보이스 조회
invoices 메서드를 사용하면 청구 가능한 모델의 인보이스 목록을 손쉽게 조회할 수 있습니다. 이 메서드는 Laravel\Cashier\Invoice 인스턴스의 컬렉션을 반환합니다:
$invoices = $user->invoices();처리 대기 중인 인보이스도 결과에 포함하려면 invoicesIncludingPending 메서드를 사용하세요:
$invoices = $user->invoicesIncludingPending();특정 인보이스를 ID로 조회하려면 findInvoice 메서드를 사용합니다:
$invoice = $user->findInvoice($invoiceId);인보이스 정보 출력
고객에게 인보이스 목록을 보여줄 때는 인보이스 객체의 메서드를 활용해 필요한 정보를 표시할 수 있습니다. 예를 들어, 아래와 같이 테이블로 목록을 구성하고 각 항목에 다운로드 링크를 제공할 수 있습니다:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">다운로드</a></td>
</tr>
@endforeach
</table>예정된 인보이스 조회
고객의 다음 결제 예정 인보이스를 미리 조회하려면 upcomingInvoice 메서드를 사용합니다:
$invoice = $user->upcomingInvoice();고객이 여러 구독을 보유하고 있다면, 특정 구독에 대한 예정 인보이스만 따로 조회할 수도 있습니다:
$invoice = $user->subscription('default')->upcomingInvoice();구독 인보이스 미리보기
가격 변경을 적용하기 전에 인보이스가 어떻게 생성될지 미리 확인하려면 previewInvoice 메서드를 활용하세요. 이를 통해 특정 가격으로 변경했을 때 고객에게 청구될 금액을 사전에 파악할 수 있습니다:
$invoice = $user->subscription('default')->previewInvoice('price_yearly');여러 가격을 동시에 변경하는 경우, 배열로 전달하면 복수의 새 가격이 반영된 인보이스를 미리볼 수 있습니다:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);인보이스 PDF 생성
인보이스 PDF를 생성하려면 먼저 Cashier의 기본 PDF 렌더러인 Dompdf 라이브러리를 Composer로 설치해야 합니다:
composer require dompdf/dompdf설치 후 라우트나 컨트롤러에서 downloadInvoice 메서드를 호출하면 해당 인보이스의 PDF 파일을 다운로드할 수 있습니다. 이 메서드는 파일 다운로드에 필요한 HTTP 응답을 자동으로 생성합니다:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
return $request->user()->downloadInvoice($invoiceId);
});기본적으로 인보이스의 모든 데이터는 Stripe에 저장된 고객 및 인보이스 정보를 기반으로 구성되며, 파일명은 app.name 설정값을 따릅니다. downloadInvoice의 두 번째 인수로 배열을 전달하면 회사 정보나 제품 정보 등을 커스터마이징할 수 있습니다:
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => '주식회사 예시',
'product' => '프리미엄 플랜',
'street' => '테헤란로 123',
'location' => '서울특별시 강남구 06134',
'phone' => '+82 02-1234-5678',
'email' => 'info@example.com',
'url' => 'https://example.com',
'vendorVat' => '123-45-67890',
]);세 번째 인수를 통해 파일명을 직접 지정할 수도 있습니다. .pdf 확장자는 자동으로 붙습니다:
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');커스텀 인보이스 렌더러
Cashier는 기본적으로 dompdf 라이브러리를 사용하는 DompdfInvoiceRenderer로 인보이스를 렌더링합니다. 하지만 Laravel\Cashier\Contracts\InvoiceRenderer 인터페이스를 직접 구현하면 원하는 렌더러로 교체할 수 있습니다.
예를 들어, 외부 PDF 변환 API를 호출해 인보이스를 생성하는 커스텀 렌더러는 다음과 같이 작성할 수 있습니다:
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* 인보이스를 렌더링하고 PDF 바이너리 데이터를 반환합니다.
*/
public function render(Invoice $invoice, array $data = [], array $options = []): string
{
$html = $invoice->view($data)->render();
return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
}
}커스텀 렌더러 구현이 완료되면, config/cashier.php 설정 파일의 cashier.invoices.renderer 값을 해당 클래스명으로 변경하세요.
Laravel Cashier (Stripe)
Checkout
Cashier Stripe는 Stripe Checkout을 지원합니다. Stripe Checkout은 미리 구축된 호스팅 결제 페이지를 제공하므로, 결제 화면을 직접 구현하는 수고를 덜 수 있습니다.
이 문서에서는 Cashier와 함께 Stripe Checkout을 시작하는 방법을 설명합니다. 더 자세한 내용은 Stripe 공식 Checkout 문서를 함께 참고하세요.
- 상품 결제 (Product Checkouts)
- 단건 결제 (Single Charge Checkouts)
- 구독 결제 (Subscription Checkouts)
- 세금 ID 수집
- 비회원 결제 (Guest Checkouts)
상품 결제 (Product Checkouts)
Stripe 대시보드에 미리 등록된 상품에 대해 결제를 시작하려면 빌링 가능한 모델의 checkout 메서드를 사용하세요. 이 메서드는 새로운 Stripe Checkout 세션을 시작합니다. 기본적으로 Stripe Price ID를 전달해야 합니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});수량을 지정하려면 배열 형태로 전달합니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});이 라우트에 접근하면 사용자는 Stripe Checkout 페이지로 리디렉션됩니다. 결제를 완료하거나 취소하면 기본적으로 home 라우트로 이동합니다. success_url과 cancel_url 옵션으로 이동할 URL을 직접 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});결제 완료 후 Stripe가 Checkout 세션 ID를 쿼리 파라미터로 전달하도록 설정하려면, success_url에 {CHECKOUT_SESSION_ID} 플레이스홀더를 추가합니다. Stripe가 이 값을 실제 세션 ID로 자동으로 교체합니다.
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');프로모션 코드
Stripe Checkout은 기본적으로 사용자가 직접 입력하는 프로모션 코드를 허용하지 않습니다. 이를 활성화하려면 allowPromotionCodes 메서드를 호출하세요.
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});단건 결제 (Single Charge Checkouts)
Stripe 대시보드에 등록되지 않은 임시 상품에 대해 바로 결제를 진행하려면 checkoutCharge 메서드를 사용합니다. 금액(센트 단위), 상품명, 수량을 전달하면 됩니다.
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
// 금액은 센트 단위: 1200 = 12.00달러
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});WARNING
checkoutCharge 메서드를 사용하면 Stripe 대시보드에 항상 새로운 상품과 가격이 생성됩니다. 따라서 Stripe 대시보드에 상품을 미리 등록해두고 checkout 메서드를 사용하는 방식을 권장합니다.
구독 결제 (Subscription Checkouts)
WARNING
Stripe Checkout으로 구독을 처리하려면 Stripe 대시보드에서 customer.subscription.created 웹훅을 활성화해야 합니다. 이 웹훅이 데이터베이스에 구독 레코드를 생성하고 관련 구독 아이템을 저장합니다.
Cashier의 구독 빌더 메서드로 구독을 정의한 뒤 checkout 메서드를 호출하면 Stripe Checkout으로 구독을 시작할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});상품 결제와 마찬가지로 성공 및 취소 URL을 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});구독 결제에서도 프로모션 코드를 활성화할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});WARNING
Stripe Checkout은 구독의 모든 결제 옵션을 지원하지는 않습니다. 구독 빌더에서 anchorBillingCycleOn 메서드 사용, 비례 배분(proration) 동작 설정, 결제 동작 설정은 Stripe Checkout 세션에서 효과가 없습니다. 사용 가능한 파라미터는 Stripe Checkout Session API 문서를 참고하세요.
체험 기간 (Trial Periods)
Stripe Checkout으로 구독을 시작할 때도 체험 기간을 설정할 수 있습니다.
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();단, Stripe Checkout이 지원하는 최소 체험 기간은 48시간입니다. 3일(72시간)처럼 48시간 이상으로 설정해야 합니다.
구독과 웹훅
Stripe와 Cashier는 웹훅을 통해 구독 상태를 업데이트합니다. 따라서 고객이 결제 정보를 입력하고 애플리케이션으로 돌아왔을 때, 구독이 아직 활성화되지 않은 상태일 수 있습니다. 이런 경우를 대비해 결제 또는 구독이 처리 중임을 사용자에게 안내하는 메시지를 표시하는 것이 좋습니다.
세금 ID 수집
Checkout 세션에서 고객의 세금 ID를 수집할 수 있습니다. collectTaxIds 메서드를 호출하면 됩니다.
$checkout = $user->collectTaxIds()->checkout('price_tshirt');이 메서드를 호출하면 결제 페이지에 체크박스가 추가되어, 고객이 법인으로 구매하는지 여부를 선택할 수 있습니다. 법인 구매를 선택하면 세금 ID를 직접 입력할 수 있습니다.
WARNING
이미 서비스 프로바이더에서 자동 세금 수집을 설정했다면, 이 기능은 자동으로 활성화되므로 collectTaxIds 메서드를 별도로 호출할 필요가 없습니다.
비회원 결제 (Guest Checkouts)
계정이 없는 비회원 사용자를 위한 결제 세션을 시작하려면 Checkout::guest 메서드를 사용하세요.
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});기존 사용자 대상 결제 세션과 마찬가지로, Laravel\Cashier\CheckoutBuilder 인스턴스의 다양한 메서드를 활용해 비회원 결제 세션을 커스터마이징할 수 있습니다.
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()
->withPromotionCode('promo-code')
->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});비회원 결제가 완료되면 Stripe는 checkout.session.completed 웹훅 이벤트를 발송합니다. Stripe 대시보드에서 웹훅을 설정하여 이 이벤트가 애플리케이션으로 전송되도록 해야 합니다. 웹훅을 활성화했다면 Cashier로 웹훅을 처리할 수 있습니다. 웹훅 페이로드에 포함된 객체는 checkout 객체이며, 이를 통해 고객의 주문을 처리할 수 있습니다.
결제 실패 처리
구독 또는 단건 결제가 실패하는 경우가 있습니다. 이런 상황이 발생하면 Cashier는 Laravel\Cashier\Exceptions\IncompletePayment 예외를 던져 결제가 완료되지 않았음을 알립니다. 이 예외를 잡은 후에는 두 가지 방식으로 처리할 수 있습니다.
첫 번째 방법은 Cashier에 내장된 결제 확인 페이지로 고객을 리디렉션하는 것입니다. 이 페이지는 Cashier의 서비스 프로바이더를 통해 이미 이름이 지정된 라우트로 등록되어 있습니다. IncompletePayment 예외를 잡아 다음과 같이 리디렉션할 수 있습니다:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}결제 확인 페이지에서 고객은 신용카드 정보를 다시 입력하거나, Stripe에서 요구하는 추가 인증(예: "3D Secure" 확인)을 수행하게 됩니다. 결제 확인이 완료되면 위의 redirect 파라미터로 지정한 URL로 리디렉션되며, 이때 message(문자열)와 success(정수) 쿼리 스트링 변수가 URL에 함께 추가됩니다. 현재 결제 확인 페이지에서 지원하는 결제 수단 유형은 다음과 같습니다:
- 신용카드 (Credit Cards)
- Alipay
- Bancontact
- BECS Direct Debit
- EPS
- Giropay
- iDEAL
- SEPA Direct Debit
두 번째 방법은 Stripe의 자동 결제 이메일 기능을 활용하는 것입니다. 별도의 결제 확인 페이지로 리디렉션하는 대신, Stripe 대시보드에서 자동 청구 이메일을 설정해두면 Stripe가 고객에게 직접 안내 이메일을 발송합니다. 단, 이 경우에도 IncompletePayment 예외가 발생했다면, 고객이 추가 결제 안내 이메일을 받게 된다는 사실을 화면에 표시해 주어야 합니다.
IncompletePayment 예외가 발생할 수 있는 메서드는 다음과 같습니다. Billable 트레이트를 사용하는 모델의 charge, invoiceFor, invoice 메서드, 구독과 관련해서는 SubscriptionBuilder의 create 메서드, 그리고 Subscription 및 SubscriptionItem 모델의 incrementAndInvoice, swapAndInvoice 메서드입니다.
기존 구독에 미완료 결제가 있는지 확인하려면 청구 가능 모델 또는 구독 인스턴스의 hasIncompletePayment 메서드를 사용하세요:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}미완료 결제의 구체적인 상태는 예외 인스턴스의 payment 프로퍼티를 통해 확인할 수 있습니다:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// Payment Intent 상태 확인...
$exception->payment->status;
// 세부 조건 확인...
if ($exception->payment->requiresPaymentMethod()) {
// 결제 수단이 필요한 경우...
} elseif ($exception->payment->requiresConfirmation()) {
// 추가 확인이 필요한 경우...
}
}결제 확인
일부 결제 수단은 결제를 확인하기 위해 추가 데이터를 필요로 합니다. 예를 들어, SEPA 결제 수단은 결제 과정에서 "위임(mandate)" 데이터를 추가로 요구합니다. withPaymentConfirmationOptions 메서드를 사용하여 이 데이터를 Cashier에 전달할 수 있습니다:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');결제 확인 시 사용할 수 있는 모든 옵션은 Stripe API 문서를 참고하세요.
Laravel Cashier (Stripe)
강력한 고객 인증 (Strong Customer Authentication)
유럽에 기반을 둔 비즈니스이거나 유럽 고객을 대상으로 서비스를 운영한다면, EU의 강력한 고객 인증(SCA, Strong Customer Authentication) 규정을 준수해야 합니다. 이 규정은 결제 사기를 방지하기 위해 2019년 9월 유럽연합이 도입했습니다. Stripe와 Cashier는 SCA를 준수하는 애플리케이션을 구축할 수 있도록 이미 대응되어 있습니다.
WARNING
시작하기 전에 Stripe의 PSD2 및 SCA 가이드와 새로운 SCA API 문서를 먼저 검토하시기 바랍니다.
추가 확인이 필요한 결제
SCA 규정에 따라 결제를 승인하고 처리하려면 추가 인증이 필요한 경우가 있습니다. 이런 상황이 발생하면 Cashier는 Laravel\Cashier\Exceptions\IncompletePayment 예외를 발생시켜 추가 인증이 필요함을 알립니다. 이 예외를 처리하는 방법은 결제 실패 처리 문서를 참고하세요.
Stripe 또는 Cashier가 표시하는 결제 확인 화면은 특정 은행이나 카드사의 결제 흐름에 맞게 구성될 수 있으며, 추가 카드 확인, 소액 임시 청구, 별도 기기 인증 등 다양한 방식의 인증 절차가 포함될 수 있습니다.
Incomplete 및 Past Due 상태
결제에 추가 확인이 필요한 경우, 구독은 stripe_status 데이터베이스 컬럼 값이 incomplete 또는 past_due 상태로 유지됩니다. 결제 확인이 완료되고 Stripe가 웹훅을 통해 애플리케이션에 완료 사실을 알리면, Cashier가 자동으로 해당 고객의 구독을 활성화합니다.
incomplete 및 past_due 상태에 대한 자세한 내용은 해당 상태에 대한 추가 문서를 참고하세요.
오프세션 결제 알림
SCA 규정에 따라 고객은 구독이 활성 상태인 경우에도 결제 정보를 간헐적으로 재인증해야 할 수 있습니다. Cashier는 오프세션(off-session) 결제 확인이 필요할 때 고객에게 알림을 보낼 수 있습니다. 예를 들어, 구독 갱신 시 이런 상황이 발생할 수 있습니다.
Cashier의 결제 알림을 활성화하려면 CASHIER_PAYMENT_NOTIFICATION 환경 변수에 알림 클래스를 지정하면 됩니다. 기본값은 비활성화 상태입니다. Cashier에서 기본 제공하는 알림 클래스를 사용해도 되고, 필요에 따라 직접 만든 알림 클래스를 지정할 수도 있습니다.
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment오프세션 결제 확인 알림이 정상적으로 전송되려면 다음 두 가지를 확인해야 합니다.
- 애플리케이션에 Stripe 웹훅이 설정되어 있고, Stripe 대시보드에서
invoice.payment_action_required웹훅 이벤트가 활성화되어 있어야 합니다. Billable모델에 Laravel의Illuminate\Notifications\Notifiable트레이트가 적용되어 있어야 합니다.
WARNING
고객이 추가 확인이 필요한 결제를 직접(수동으로) 진행하는 경우에도 알림이 발송됩니다. 안타깝게도 Stripe는 해당 결제가 수동으로 처리되었는지 오프세션으로 처리되었는지 구분할 방법이 없습니다. 다만, 고객이 이미 결제를 확인한 후 결제 페이지를 다시 방문하면 "결제 완료" 메시지만 표시됩니다. 동일한 결제를 실수로 두 번 확인하거나 중복 청구가 발생하는 일은 없으니 안심하셔도 됩니다.
Stripe SDK
Cashier의 많은 객체는 Stripe SDK 객체를 래핑한 형태입니다. Stripe 객체를 직접 다루고 싶다면 asStripe 메서드를 사용해 간편하게 가져올 수 있습니다.
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();updateStripeSubscription 메서드를 사용하면 Stripe 구독을 직접 업데이트할 수도 있습니다.
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);Stripe\StripeClient 클라이언트를 직접 사용하고 싶다면 Cashier 클래스의 stripe 메서드를 호출하세요. 예를 들어, 아래와 같이 Stripe 계정에 등록된 가격 목록을 조회할 수 있습니다.
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();Laravel Cashier (Stripe)
테스트
테스트
Cashier를 사용하는 애플리케이션을 테스트할 때, Stripe API로 향하는 실제 HTTP 요청을 모킹(mocking)하는 방법을 떠올릴 수 있습니다. 그러나 이 방식은 Cashier 내부 동작의 일부를 직접 재구현해야 하므로 권장하지 않습니다. 대신, 실제 Stripe API에 요청을 보내는 방식으로 테스트하는 것을 권장합니다. 속도는 다소 느리지만, 애플리케이션이 실제로 올바르게 동작하는지 훨씬 높은 신뢰도로 확인할 수 있습니다. 시간이 오래 걸리는 테스트는 별도의 PHPUnit 테스트 그룹으로 분리해 두면 관리하기 편합니다.
테스트를 작성할 때, Cashier 자체에는 이미 충분한 테스트 스위트가 갖춰져 있다는 점을 기억하세요. 따라서 Cashier 내부 동작을 하나하나 검증하기보다는, 여러분의 애플리케이션에서 구현한 구독 및 결제 흐름에만 집중하면 됩니다.
시작하려면 phpunit.xml 파일에 Stripe 테스트용 시크릿 키를 추가하세요:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>이렇게 설정하면 테스트 실행 중 Cashier와 상호작용할 때마다 Stripe 테스트 환경으로 실제 API 요청이 전송됩니다. 테스트를 원활하게 진행하려면, Stripe 테스트 계정에 테스트에서 사용할 구독 플랜과 가격(Price)을 미리 등록해 두는 것이 좋습니다.
NOTE
카드 거절, 결제 실패 등 다양한 결제 시나리오를 테스트하려면 Stripe에서 제공하는 테스트용 카드 번호 및 토큰을 활용하세요.