본문 바로가기

결제(Cashier)

업데이트됨

번역일: 2026년 9월 17일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 17일
번역 갱신
2026년 9월 17일

결제(Cashier)

소개

Laravel Cashier Stripe는 Stripe의 구독 결제 서비스를 직관적이고 유연한 인터페이스로 다룰 수 있게 해주는 패키지입니다. 반복적으로 작성해야 하는 구독 결제 관련 코드를 대부분 대신 처리해주기 때문에, 개발자는 이 귀찮은 작업에서 벗어날 수 있습니다. 기본적인 구독 관리 기능 외에도 Cashier는 쿠폰 적용, 구독 변경, 구독 "수량" 관리, 취소 유예 기간 처리, 심지어 인보이스 PDF 생성까지 다양한 기능을 지원합니다.

NOTE

Cashier를 사용하기 전에 먼저 알아두어야 할 것들이 있습니다. Cashier는 Stripe API를 최신 버전으로 유지하는 데 신경을 많이 쓰고 있기 때문에, 실제 프로덕션 서비스에 적용하기 전 반드시 Stripe API 업그레이드 가이드를 확인해야 합니다. 또한 이 패키지는 Stripe의 API 버전 변경에 맞춰 지속적으로 업데이트되므로, 항상 최신 버전을 사용하는 것을 권장합니다.

WARNING

이 문서에서는 Cashier Stripe를 다룹니다. Paddle 결제를 연동하려는 경우 Cashier Paddle 문서를 참고하세요.

Cashier 업그레이드

Cashier의 새 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 확인해야 합니다.

WARNING

호환성이 깨지는 변경(breaking change)을 방지하기 위해, Cashier는 고정된 Stripe API 버전을 사용합니다. Cashier 15는 Stripe API 버전 2023-10-16을 사용합니다. Stripe의 새로운 기능 및 개선 사항을 반영하기 위해 마이너(minor) 릴리즈에서 Stripe API 버전이 업데이트될 수 있습니다.

설치

먼저, Composer 패키지 매니저를 사용해 Stripe용 Cashier 패키지를 설치합니다.

composer require laravel/cashier

패키지 설치 후, vendor:publish Artisan 명령어를 사용해 Cashier의 마이그레이션 파일을 게시(publish)합니다.

artisan vendor:publish --tag="cashier-migrations"

그다음 데이터베이스 마이그레이션을 실행합니다.

artisan migrate

Cashier의 마이그레이션은 users 테이블에 여러 컬럼을 추가합니다. 또한 고객의 모든 구독 정보를 담을 새로운 subscriptions 테이블과, 여러 가격(price)이 적용된 구독을 위한 subscription_items 테이블도 함께 생성합니다.

원한다면 Cashier의 설정 파일도 다음 명령어로 게시할 수 있습니다.

artisan vendor:publish --tag="cashier-config"

마지막으로, Cashier가 Stripe에서 발생하는 이벤트를 정상적으로 처리하도록 하려면 Cashier의 웹훅 처리 설정을 반드시 진행해야 합니다.

WARNING

Stripe에서는 Stripe 관련 객체 ID를 저장하는 모든 컬럼이 대소문자를 구분(case-sensitive)해야 한다고 권장합니다. 따라서 MySQL을 사용한다면 stripe_id 컬럼의 collation을 반드시 utf8_bin으로 설정해야 합니다. 자세한 내용은 Stripe 공식 문서에서 확인할 수 있습니다.

설정

결제 대상 모델 (Billable Model)

Cashier를 사용하기 전, Billable 트레이트를 자신의 결제 대상 모델 정의에 추가합니다. 보통은 App\Models\User 모델이 해당됩니다. 이 트레이트는 구독 생성, 쿠폰 적용, 결제 수단 정보 업데이트 등 흔히 사용하는 다양한 결제 관련 작업을 수행할 수 있는 메서드들을 제공합니다.

use Laravel\Cashier\Billable; class User extends Authenticatable { use Billable; }

User 모델이 아닌 다른 모델을 결제 대상 엔티티로 사용하고 싶다면, 아래처럼 해당 모델에도 트레이트를 추가할 수 있습니다.

use Illuminate\Database\Eloquent\Model; use Laravel\Cashier\Billable; class Team extends Model { use Billable; }

이렇게 Team처럼 User가 아닌 모델을 결제 대상 엔티티로 지정하려는 경우, Cashier가 제공하는 Stripe 웹훅을 자동으로 처리하는 기능을 사용할 수 있도록 하려면 Cashier가 지원하는 이벤트들을 리스닝하여 데이터베이스에 구독 정보를 직접 추가·수정·삭제해야 합니다.

NOTE

User 모델이 아닌 모델을 사용하는 경우, 해당 예제 코드에서 User 모델을 여러분이 사용하는 모델 타입으로 적절히 바꿔서 작성해야 합니다.

API 키

다음으로, 애플리케이션의 .env 파일에서 Stripe API 키를 설정해야 합니다. Stripe 관리자 콘솔에서 API 키를 발급받을 수 있습니다.

STRIPE_KEY=your-stripe-key STRIPE_SECRET=your-stripe-secret STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

WARNING

애플리케이션의 .env 파일에 STRIPE_WEBHOOK_SECRET 환경 변수가 반드시 정의되어 있어야 합니다. 이 변수는 실제 Stripe에서 받은 요청인지를 검증하는 웹훅에 사용됩니다.

통화(Currency) 설정

Cashier의 기본 통화는 미국 달러(USD)입니다. 애플리케이션의 .env 파일에서 CASHIER_CURRENCY 환경 변수를 설정하면 기본 통화를 변경할 수 있습니다.

CASHIER_CURRENCY=eur

통화 설정 외에도, 인보이스에 금액을 표시할 때 사용할 로케일(locale)도 지정할 수 있습니다. Cashier는 내부적으로 PHP NumberFormatter 클래스를 사용해 통화 로케일을 설정합니다.

CASHIER_CURRENCY_LOCALE=nl_BE

WARNING

en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 확장 모듈이 설치되어 있고 설정되어 있어야 합니다.

세금(Tax) 설정

Stripe Tax 덕분에 Stripe가 생성하는 모든 인보이스에 대한 세금을 자동으로 계산할 수 있습니다. App\Providers\AppServiceProvider 클래스의 boot 메서드에서 calculateTaxes 메서드를 호출하여 자동 세금 계산 기능을 활성화할 수 있습니다.

use Laravel\Cashier\Cashier; /** * 애플리케이션의 모든 서비스를 부트스트랩합니다. */ public function boot(): void { Cashier::calculateTaxes(); }

자동 세금 계산 기능이 활성화되면, 새롭게 생성되는 모든 구독과 일회성 인보이스에 대해 세금이 자동으로 계산됩니다.

이 기능이 정상적으로 동작하려면 고객의 이름, 주소, 세금 식별 번호(Tax ID)와 같은 청구 정보가 Stripe와 동기화되어 있어야 합니다. 이를 위해 Cashier가 제공하는 고객 데이터 동기화 및 Tax ID 관련 메서드를 사용할 수 있습니다.

NOTE

Stripe Tax에 대해 더 알아보고 싶다면 Stripe Tax 공식 문서를 참고하세요.

로깅

Cashier는 Stripe 관련 치명적인 오류가 발생할 때 사용할 로그 채널을 지정할 수 있습니다. 애플리케이션의 .env 파일에서 CASHIER_LOGGER 환경 변수를 정의하면 해당 로그 채널을 사용하도록 설정할 수 있습니다.

CASHIER_LOGGER=stack

Stripe API 호출 중 발생하는 예외는 애플리케이션의 기본 로그 채널을 통해 기록됩니다.

커스텀 모델 사용

Cashier 내부에서 사용되는 모델을 자유롭게 확장하여, 자신만의 모델을 정의하고 필요한 메서드나 속성을 추가할 수도 있습니다.

use Laravel\Cashier\Subscription as CashierSubscription; class Subscription extends CashierSubscription { // ... }

모델을 정의한 후에는, Laravel\Cashier\Cashier 클래스를 통해 Cashier가 커스텀 모델을 사용하도록 지정할 수 있습니다. 보통 이 설정은 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 처리합니다.

use App\Models\Cashier\Subscription; use App\Models\Cashier\SubscriptionItem; /** * 모든 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Cashier::useSubscriptionModel(Subscription::class); Cashier::useSubscriptionItemModel(SubscriptionItem::class); }

결제(Cashier)

소개

Laravel Cashier Stripe는 Stripe의 구독 결제 서비스를 표현력 있고 유려한 인터페이스로 다룰 수 있게 해주는 패키지입니다. 구독 결제 기능을 직접 구현하려면 지루하고 반복적인 코드를 작성해야 하는데, Cashier를 사용하면 이런 보일러플레이트 코드를 대부분 신경 쓰지 않아도 됩니다.

기본적인 구독 관리 기능은 물론이고, 쿠폰 적용, 구독 플랜 변경(swap), 구독 수량(quantity) 관리, 해지 유예 기간(grace period), 인보이스 PDF 생성까지 Cashier 하나로 처리할 수 있습니다.

NOTE

Stripe를 위한 Cashier와 별개로, Laravel은 Paddle 결제 연동을 위한 Laravel Cashier Paddle도 함께 제공합니다. Paddle을 사용하고 있다면 Cashier Paddle 문서를 참고하시기 바랍니다.

결제(Cashier)

Cashier 업그레이드

Cashier의 새 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 확인하시기 바랍니다.

WARNING

예기치 않은 변경으로 인한 문제를 방지하기 위해, Cashier는 고정된 Stripe API 버전을 사용합니다. Cashier 16은 Stripe API 버전 2025-06-30.basil을 사용합니다. Stripe의 새로운 기능과 개선 사항을 반영하기 위해, Stripe API 버전은 마이너 릴리스마다 업데이트될 수 있습니다.

설치

먼저 Composer 패키지 매니저를 사용해 Stripe용 Cashier 패키지를 설치합니다:

composer require laravel/cashier

패키지 설치 후, vendor:publish Artisan 명령어로 Cashier의 마이그레이션 파일을 퍼블리시합니다:

php artisan vendor:publish --tag="cashier-migrations"

그 다음, 데이터베이스를 마이그레이션합니다:

php artisan migrate

Cashier의 마이그레이션은 users 테이블에 몇 개의 컬럼을 추가합니다. 또한 고객의 구독 정보를 저장할 subscriptions 테이블과, 여러 개의 가격(price)을 가진 구독을 위한 subscription_items 테이블도 함께 생성합니다.

필요하다면 vendor:publish Artisan 명령어로 Cashier의 설정 파일도 퍼블리시할 수 있습니다:

php artisan vendor:publish --tag="cashier-config"

마지막으로, Cashier가 모든 Stripe 이벤트를 제대로 처리할 수 있도록 Cashier의 웹훅 처리 설정을 반드시 진행하세요.

WARNING

Stripe는 Stripe 식별자를 저장하는 컬럼에 대해 대소문자를 구분하도록 설정할 것을 권장합니다. 따라서 MySQL을 사용하는 경우 stripe_id 컬럼의 콜레이션(collation)을 utf8_bin으로 설정해야 합니다. 자세한 내용은 Stripe 문서를 참고하세요.

NOTE

이 옵션은 신규 컬럼을 대소문자 구분 방식으로 저장하기 위한 것으로, 예를 들어 cus_ABC123과 cus_abc123처럼 대소문자만 다른 두 식별자를 서로 다른 값으로 인식하게 해줍니다. 기존 프로젝트에 Cashier를 새로 추가하는 경우라면 마이그레이션을 실행하기 전에 이 콜레이션 설정을 미리 확인해 두는 것이 좋습니다.

결제(Cashier)

설정

결제 대상 모델 (Billable Model)

Cashier를 사용하기 전에, 결제 대상이 되는 모델에 Billable 트레이트를 추가해야 합니다. 일반적으로는 App\Models\User 모델이 그 대상이 됩니다. 이 트레이트는 구독 생성, 쿠폰 적용, 결제 수단 정보 업데이트 등 흔히 사용되는 결제 관련 작업을 수행할 수 있는 다양한 메서드를 제공합니다.

use Laravel\Cashier\Billable; class User extends Authenticatable { use Billable; }

Cashier는 기본적으로 결제 대상 모델이 Laravel에 기본 포함된 App\Models\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_KEY=your-stripe-key STRIPE_SECRET=your-stripe-secret STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

WARNING

STRIPE_WEBHOOK_SECRET 환경 변수는 반드시 애플리케이션의 .env 파일에 정의되어 있어야 합니다. 이 값은 들어오는 웹훅이 실제로 Stripe로부터 전송된 것인지 검증하는 데 사용됩니다.

통화 설정

Cashier의 기본 통화는 미국 달러(USD)입니다. 애플리케이션의 .env 파일에서 CASHIER_CURRENCY 환경 변수를 설정하면 기본 통화를 변경할 수 있습니다.

CASHIER_CURRENCY=eur

통화 설정 외에도, 인보이스에 표시되는 금액 형식을 지정하기 위한 로케일(locale)을 설정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용하여 통화 로케일을 설정합니다.

CASHIER_CURRENCY_LOCALE=nl_BE

WARNING

en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 확장 모듈이 설치되어 있고 올바르게 설정되어 있어야 합니다. 예를 들어 한국어 환경이라면 ko_KR 로케일을 사용할 수 있습니다.

세금 설정

Stripe Tax 덕분에 Stripe에서 생성되는 모든 인보이스에 대해 세금을 자동으로 계산할 수 있습니다. 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 calculateTaxes 메서드를 호출하면 자동 세금 계산 기능을 활성화할 수 있습니다.

use Laravel\Cashier\Cashier; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Cashier::calculateTaxes(); }

세금 계산 기능을 활성화하면, 이후 생성되는 모든 신규 구독과 단건 인보이스에 자동으로 세금 계산이 적용됩니다.

이 기능이 올바르게 동작하려면 고객의 이름, 주소, 사업자등록번호(Tax ID) 등 청구 정보가 Stripe와 동기화되어 있어야 합니다. Cashier가 제공하는 고객 데이터 동기화 및 Tax ID 관련 메서드를 활용하면 이를 손쉽게 처리할 수 있습니다.

NOTE

한국에서 서비스를 운영 중이라면 부가가치세(VAT) 관련 규정을 확인하시고, Stripe Tax의 지원 국가 및 세율 정책을 사전에 검토하는 것을 권장합니다.

로깅

Cashier에서는 치명적인 Stripe 오류가 발생했을 때 사용할 로그 채널을 지정할 수 있습니다. 애플리케이션의 .env 파일에 CASHIER_LOGGER 환경 변수를 정의하여 로그 채널을 지정하세요.

CASHIER_LOGGER=stack

Stripe API 호출로 인해 발생하는 예외는 애플리케이션의 기본 로그 채널을 통해 기록됩니다.

커스텀 모델 사용하기

Cashier가 내부적으로 사용하는 모델을 직접 정의한 모델로 확장하여 사용할 수 있습니다. 이를 위해서는 해당 Cashier 모델을 상속받는 모델을 정의하면 됩니다.

use Laravel\Cashier\Subscription as CashierSubscription; class Subscription extends CashierSubscription { // ... }

커스텀 모델을 정의한 후에는 Laravel\Cashier\Cashier 클래스를 통해 Cashier가 해당 모델을 사용하도록 지정할 수 있습니다. 보통은 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 커스텀 모델을 등록합니다.

use App\Models\Cashier\Subscription; use App\Models\Cashier\SubscriptionItem; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Cashier::useSubscriptionModel(Subscription::class); Cashier::useSubscriptionItemModel(SubscriptionItem::class); }

결제(Cashier)

퀵스타트

상품 판매하기

NOTE

Stripe Checkout을 사용하기 전에, Stripe 대시보드에서 고정 가격을 가진 상품(Product)을 먼저 정의해두어야 합니다. 또한 Cashier의 웹훅 처리 설정도 함께 진행해야 합니다.

애플리케이션을 통해 상품이나 구독 결제를 제공하는 일은 처음 접하면 부담스럽게 느껴질 수 있습니다. 하지만 Cashier와 Stripe Checkout을 함께 사용하면, 견고하고 현대적인 결제 연동을 놀랍도록 쉽게 구현할 수 있습니다.

반복 결제가 아닌 단건 결제 상품에 대해 요금을 청구하려면, Cashier를 통해 고객을 Stripe Checkout 페이지로 안내하면 됩니다. 고객은 이 페이지에서 결제 정보를 입력하고 구매를 확정합니다. 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', 'checkout.success')->name('checkout-success'); Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');

위 예제에서 볼 수 있듯이, Cashier가 제공하는 checkout 메서드를 사용하면 지정한 "가격(price) 식별자"를 기준으로 고객을 Stripe Checkout 페이지로 리디렉션할 수 있습니다. Stripe에서 "가격(price)"이란 특정 상품에 대해 정의된 가격 정보를 의미합니다.

필요한 경우 checkout 메서드는 Stripe에 고객을 자동으로 생성하고, 해당 Stripe 고객 레코드를 애플리케이션 데이터베이스의 사용자와 연결해줍니다. 결제 세션이 완료되면 고객은 별도로 지정한 성공 또는 취소 페이지로 리디렉션되며, 여기서 안내 메시지를 표시해줄 수 있습니다.

Stripe Checkout에 메타데이터 전달하기

상품을 판매할 때는 보통 애플리케이션에서 직접 정의한 Cart, Order 모델을 통해 완료된 주문과 구매한 상품을 추적하게 됩니다. 고객을 Stripe Checkout으로 리디렉션해서 구매를 진행할 때, 고객이 애플리케이션으로 다시 돌아왔을 때 완료된 결제를 해당 주문과 연결할 수 있도록 기존 주문 식별자를 함께 전달해야 하는 경우가 많습니다.

이를 위해 checkout 메서드에 metadata 배열을 전달할 수 있습니다. 사용자가 결제 절차를 시작할 때 애플리케이션 내에서 대기 상태의 Order가 생성되는 상황을 가정해보겠습니다. 여기서 사용하는 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');

위 예제를 보면, 사용자가 결제 절차를 시작할 때 장바구니(주문)에 포함된 모든 Stripe 가격 식별자를 checkout 메서드에 전달하고 있습니다. 물론 사용자가 상품을 담을 때마다 이 항목들을 "장바구니"나 주문과 연결하는 로직은 애플리케이션에서 직접 구현해야 합니다. 또한 metadata 배열을 통해 주문의 ID를 Stripe Checkout 세션에 함께 전달했습니다. 마지막으로 Checkout 성공 라우트에 CHECKOUT_SESSION_ID 템플릿 변수를 추가했습니다. Stripe가 고객을 애플리케이션으로 다시 리디렉션할 때, 이 템플릿 변수는 실제 Checkout 세션 ID 값으로 자동으로 채워집니다.

이제 Checkout 성공 라우트를 만들어보겠습니다. 이 라우트는 Stripe Checkout을 통해 결제가 완료된 후 사용자가 리디렉션되는 라우트입니다. 이 라우트 내에서 Stripe Checkout 세션 ID와 관련 Checkout 인스턴스를 조회하여, 앞서 전달한 메타데이터에 접근하고 그에 따라 주문 상태를 갱신할 수 있습니다:

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');

Checkout 세션 객체에 포함된 데이터에 대한 더 자세한 내용은 Stripe 공식 문서를 참고하세요.

구독 판매하기

NOTE

Stripe Checkout을 사용하기 전에, Stripe 대시보드에서 고정 가격을 가진 상품(Product)을 먼저 정의해두어야 합니다. 또한 Cashier의 웹훅 처리 설정도 함께 진행해야 합니다.

애플리케이션을 통해 상품이나 구독 결제를 제공하는 일은 처음에는 막막하게 느껴질 수 있습니다. 하지만 Cashier와 Stripe Checkout을 함께 사용하면 현대적이고 견고한 결제 연동을 쉽게 구축할 수 있습니다.

Cashier와 Stripe Checkout을 사용해 구독을 판매하는 방법을 알아보기 위해, 월간 요금제(price_basic_monthly)와 연간 요금제(price_basic_yearly)를 제공하는 기본 구독 서비스를 예로 들어보겠습니다. 이 두 가격은 Stripe 대시보드에서 "Basic" 상품(pro_basic) 아래로 그룹화되어 있을 수 있습니다. 또한 이 구독 서비스는 pro_expert라는 이름의 Expert 요금제도 함께 제공할 수 있습니다.

먼저, 고객이 서비스에 구독하는 흐름을 살펴보겠습니다. 애플리케이션의 요금제 안내 페이지에서 고객이 Basic 플랜의 "구독하기" 버튼을 클릭하는 상황을 떠올려볼 수 있습니다. 이 버튼(또는 링크)은 사용자가 선택한 요금제에 대한 Stripe Checkout 세션을 생성하는 Laravel 라우트로 연결되어야 합니다:

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'), ]); });

위 예제와 같이, Basic 플랜을 구독할 수 있는 Stripe Checkout 세션으로 고객을 리디렉션합니다. 결제가 성공하거나 취소되면 고객은 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]);

고객이 직접 요금제를 관리할 수 있도록 하기

고객이 자신의 구독 요금제를 다른 상품이나 "등급(tier)"으로 변경하고 싶어하는 경우도 당연히 있습니다. 이를 지원하는 가장 쉬운 방법은 Stripe의 고객 결제 포털(Customer Billing Portal)로 고객을 안내하는 것입니다. 이 포털은 Stripe가 직접 제공하는 호스팅 UI로, 고객이 인보이스를 다운로드하거나 결제 수단을 변경하고, 구독 요금제를 바꿀 수 있게 해줍니다.

먼저, 애플리케이션 내에 사용자를 Billing Portal 세션을 시작하는 Laravel 라우트로 연결하는 링크나 버튼을 정의합니다:

<a href="{{ route('billing') }}"> 결제 관리 </a>

다음으로, Stripe 고객 결제 포털 세션을 시작하고 사용자를 포털로 리디렉션하는 라우트를 정의합니다. 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로부터 들어오는 웹훅을 자동으로 분석하여 애플리케이션의 Cashier 관련 데이터베이스 테이블을 항상 최신 상태로 동기화합니다. 예를 들어 사용자가 Stripe 고객 결제 포털에서 구독을 취소하면, Cashier는 이에 해당하는 웹훅을 수신하여 애플리케이션 데이터베이스에서 해당 구독을 "취소됨" 상태로 표시합니다.

고객 (Customers)

고객 조회

Cashier::findBillable 메서드를 사용하면 Stripe ID로 고객을 조회할 수 있습니다. 이 메서드는 청구 가능(billable) 모델의 인스턴스를 반환합니다:

use Laravel\Cashier\Cashier; $user = Cashier::findBillable($stripeId);

고객 생성

구독을 시작하지 않고 Stripe 고객만 먼저 생성해야 할 때가 있습니다. 이런 경우 createAsStripeCustomer 메서드를 사용하면 됩니다:

$stripeCustomer = $user->createAsStripeCustomer();

Stripe에 고객이 생성되고 나면, 이후 원하는 시점에 구독을 시작할 수 있습니다. $options 배열을 선택적으로 전달하여 Stripe API가 지원하는 고객 생성 추가 옵션을 지정할 수도 있습니다:

$stripeCustomer = $user->createAsStripeCustomer($options);

청구 가능 모델에 해당하는 Stripe 고객 객체를 가져오려면 asStripeCustomer 메서드를 사용하세요:

$stripeCustomer = $user->asStripeCustomer();

createOrGetStripeCustomer 메서드는 특정 청구 가능 모델의 Stripe 고객 객체를 가져오되, 해당 모델이 이미 Stripe 고객으로 등록되어 있는지 확실하지 않을 때 사용합니다. 이 메서드는 고객이 아직 존재하지 않으면 Stripe에 새로 생성합니다:

$stripeCustomer = $user->createOrGetStripeCustomer();

고객 정보 업데이트

Stripe 고객 정보를 직접 업데이트해야 하는 경우 updateStripeCustomer 메서드를 사용할 수 있습니다. 이 메서드는 Stripe API가 지원하는 고객 업데이트 옵션 배열을 인자로 받습니다:

$stripeCustomer = $user->updateStripeCustomer($options);

잔액(Balance)

Stripe에서는 고객의 "잔액"에 금액을 적립(credit)하거나 차감(debit)할 수 있으며, 이 잔액은 이후 새로 발행되는 인보이스에 반영됩니다. 청구 가능 모델의 balance 메서드를 사용하면 고객의 총 잔액을 확인할 수 있습니다. balance 메서드는 고객의 통화 단위로 포맷된 문자열을 반환합니다:

$balance = $user->balance();

고객 잔액에 금액을 적립하려면 creditBalance 메서드에 값을 전달하면 됩니다. 필요하다면 설명(description)도 함께 전달할 수 있습니다:

$user->creditBalance(500, '프리미엄 고객 충전.');

반대로 debitBalance 메서드에 값을 전달하면 고객 잔액에서 금액이 차감됩니다:

$user->debitBalance(300, '부정 사용 페널티.');

applyBalance 메서드는 고객에 대한 새로운 잔액 거래(balance transaction) 내역을 생성합니다. 이렇게 생성된 거래 내역은 balanceTransactions 메서드로 조회할 수 있으며, 고객에게 적립/차감 내역을 확인시켜줄 로그를 제공하고자 할 때 유용합니다:

// 모든 거래 내역 조회... $transactions = $user->balanceTransactions(); foreach ($transactions as $transaction) { // 거래 금액... $amount = $transaction->amount(); // $2.31 // 관련 인보이스가 있다면 함께 조회... $invoice = $transaction->invoice(); }

세금 ID (Tax IDs)

Cashier는 고객의 세금 ID를 손쉽게 관리할 수 있는 기능을 제공합니다. 예를 들어 taxIds 메서드를 사용하면 고객에게 등록된 모든 세금 ID를 컬렉션 형태로 조회할 수 있습니다:

$taxIds = $user->taxIds();

특정 식별자를 사용해 고객의 특정 세금 ID를 조회할 수도 있습니다:

$taxId = $user->findTaxId('txi_belgium');

createTaxId 메서드에 유효한 type과 값을 전달하면 새로운 세금 ID를 생성할 수 있습니다:

$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에도 알려주어야 합니다. 이렇게 하면 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가 제공하는 여러 메서드를 오버라이드하여 Stripe로 동기화되는 컬럼을 원하는 대로 커스터마이징할 수도 있습니다. 예를 들어, stripeName 메서드를 오버라이드하면 Cashier가 Stripe로 고객 정보를 동기화할 때 어떤 속성을 고객의 "이름"으로 취급할지 지정할 수 있습니다:

/** * Stripe로 동기화할 고객 이름을 가져옵니다. */ public function stripeName(): string|null { return $this->company_name; }

마찬가지로 stripeEmail, stripePhone(최대 20자), stripeAddress, stripePreferredLocales 메서드도 오버라이드할 수 있습니다. 이 메서드들은 Stripe 고객 객체를 업데이트할 때 그에 대응하는 고객 파라미터로 정보를 동기화합니다. 동기화 프로세스 전체를 완전히 직접 제어하고 싶다면 syncStripeCustomerDetails 메서드 자체를 오버라이드하면 됩니다.

빌링 포털 (Billing Portal)

Stripe는 고객이 자신의 구독, 결제 수단을 관리하고 청구 내역을 확인할 수 있는 빌링 포털을 손쉽게 구성하는 기능을 제공합니다. 컨트롤러나 라우트에서 청구 가능 모델의 redirectToBillingPortal 메서드를 호출하면 사용자를 빌링 포털로 리다이렉트할 수 있습니다:

use Illuminate\Http\Request; Route::get('/billing-portal', function (Request $request) { return $request->user()->redirectToBillingPortal(); });

기본적으로 사용자가 구독 관리를 마치면, Stripe 빌링 포털 내의 링크를 통해 애플리케이션의 home 라우트로 돌아올 수 있습니다. redirectToBillingPortal 메서드에 URL을 인자로 전달하면 사용자가 돌아올 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'));

결제(Cashier)

결제 수단(Payment Methods)

결제 수단 저장하기

Stripe로 구독을 생성하거나 "일회성" 결제를 처리하려면, 애플리케이션이 고객의 결제 정보를 안전하게 수집할 수 있어야 합니다. 이때 결제 수단을 향후 구독을 위해 저장해 둘 것인지, 아니면 즉시 단건 결제를 처리할 것인지에 따라 접근 방식이 달라지므로, 두 가지 경우를 모두 살펴보겠습니다.

Stripe의 Payment Element를 사용하면 카드, Apple Pay, Google Pay, iDEAL 등 다양한 결제 수단을 한 번에 지원할 수 있습니다.

구독을 위한 Payment Element

먼저 Setup Intent를 생성하여 뷰에 전달합니다:

return view('subscribe', [ 'intent' => $user->createSetupIntent() ]);

Setup Intent의 client_secret을 이용해 Payment Element를 마운트합니다:

<div id="payment-element"></div> <button id="submit">구독하기</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) { // 사용자에게 "error.message"를 표시합니다... } }); </script>

Stripe가 return_url로 리다이렉트한 후에는 쿼리 문자열 파라미터로 setup_intent ID를 받을 수 있습니다. 이 값을 사용해 결제 수단을 조회하고 구독을 생성할 수 있습니다:

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');

만약 Payment Element를 구독 생성이 아니라 고객의 기본 결제 수단을 변경하는 용도로 사용하고 있다면, 결제 수단 식별자를 updateDefaultPaymentMethod 메서드에 전달하면 됩니다.

단건 결제를 위한 Payment Element

일회성 결제의 경우, Cashier의 pay 메서드를 사용해 Payment Intent를 생성합니다. 이때 생성된 Payment Intent ID는 애플리케이션의 주문(order) 레코드에 함께 저장해 두는 것이 좋습니다. 이렇게 하면 Stripe가 고객을 애플리케이션으로 다시 리다이렉트한 후 해당 주문을 조회할 수 있습니다. 아래 예제에서는 애플리케이션에 user_id, amount, status, stripe_payment_intent_id 컬럼을 가진 Order 모델이 있다고 가정합니다:

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를 마운트하고 결제를 확인(confirm)합니다:

<div id="payment-element"></div> <button id="submit">결제하기</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) { // 사용자에게 "error.message"를 표시합니다... } }); </script>

리다이렉트 이후에는 payment_intent 쿼리 문자열 파라미터를 사용해 해당 주문과 Payment Intent를 조회할 수 있습니다. 주문을 처리(fulfill)하기 전에는 반드시 해당 주문이 현재 인증된 고객의 것인지, 그리고 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');

NOTE

결제 성공 여부를 확인하는 로직은 반드시 서버 사이드에서 한 번 더 검증해야 합니다. 클라이언트 측 리다이렉트만으로 주문을 확정하면, 사용자가 임의로 URL을 조작해 결제되지 않은 주문을 처리 완료 상태로 바꿀 위험이 있습니다.

결제 수단 조회하기

billable 모델 인스턴스의 paymentMethods 메서드는 Laravel\Cashier\PaymentMethod 인스턴스의 컬렉션을 반환합니다:

$paymentMethods = $user->paymentMethods();

기본적으로 이 메서드는 모든 유형의 결제 수단을 반환합니다. 특정 유형의 결제 수단만 조회하려면 type 인자를 전달하면 됩니다:

$paymentMethods = $user->paymentMethods('sepa_debit');

고객의 기본 결제 수단을 조회하려면 defaultPaymentMethod 메서드를 사용합니다:

$paymentMethod = $user->defaultPaymentMethod();

billable 모델에 연결된 특정 결제 수단은 findPaymentMethod 메서드로 조회할 수 있습니다:

$paymentMethod = $user->findPaymentMethod($paymentMethodId);

결제 수단 존재 여부 확인하기

billable 모델 계정에 기본 결제 수단이 등록되어 있는지 확인하려면 hasDefaultPaymentMethod 메서드를 호출합니다:

if ($user->hasDefaultPaymentMethod()) { // ... }

billable 모델 계정에 결제 수단이 하나라도 등록되어 있는지 확인하려면 hasPaymentMethod 메서드를 사용합니다:

if ($user->hasPaymentMethod()) { // ... }

이 메서드는 유형과 관계없이 결제 수단이 존재하는지만 확인합니다. 특정 유형의 결제 수단이 존재하는지 확인하려면 type 인자를 전달하세요:

if ($user->hasPaymentMethod('sepa_debit')) { // ... }

기본 결제 수단 변경하기

updateDefaultPaymentMethod 메서드는 고객의 기본 결제 수단 정보를 업데이트할 때 사용합니다. 이 메서드는 Stripe 결제 수단 식별자를 인자로 받아 새 결제 수단을 기본 결제 수단으로 지정합니다:

$user->updateDefaultPaymentMethod($paymentMethod);

애플리케이션에 저장된 기본 결제 수단 정보를 Stripe 상의 실제 기본 결제 수단 정보와 동기화하려면 updateDefaultPaymentMethodFromStripe 메서드를 사용할 수 있습니다:

$user->updateDefaultPaymentMethodFromStripe();

WARNING

고객의 기본 결제 수단은 청구서(invoice) 발행과 신규 구독 생성에만 사용할 수 있습니다. Stripe 측 제약으로 인해 단건 결제(single charge)에는 사용할 수 없습니다.

결제 수단 추가하기

새 결제 수단을 추가하려면, billable 모델의 addPaymentMethod 메서드에 결제 수단 식별자를 전달하여 호출합니다:

$user->addPaymentMethod($paymentMethod);

NOTE

결제 수단 식별자를 조회하는 방법은 결제 수단 저장하기 문서를 참고하세요.

결제 수단 삭제하기

결제 수단을 삭제하려면, 삭제하려는 Laravel\Cashier\PaymentMethod 인스턴스의 delete 메서드를 호출합니다:

$paymentMethod->delete();

deletePaymentMethod 메서드는 billable 모델에서 특정 결제 수단을 삭제합니다:

$user->deletePaymentMethod('pm_visa');

deletePaymentMethods 메서드는 billable 모델에 등록된 모든 결제 수단 정보를 삭제합니다:

$user->deletePaymentMethods();

기본적으로 이 메서드는 모든 유형의 결제 수단을 삭제합니다. 특정 유형의 결제 수단만 삭제하려면 type 인자를 전달하세요:

$user->deletePaymentMethods('sepa_debit');

WARNING

사용자가 활성 상태의 구독을 보유하고 있다면, 애플리케이션에서 해당 사용자가 기본 결제 수단을 삭제하지 못하도록 막아야 합니다.

결제(Cashier)

구독(Subscriptions)

구독 기능은 고객에게 정기 결제를 설정할 수 있는 방법을 제공합니다. Cashier가 관리하는 Stripe 구독은 여러 개의 구독 가격(price), 수량(quantity), 체험 기간(trial) 등 다양한 기능을 지원합니다.

구독 생성하기

구독을 생성하려면, 먼저 청구 가능 모델(billable model)의 인스턴스를 가져와야 합니다. 보통은 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 같은 이름을 사용하면 됩니다. 이 타입 값은 애플리케이션 내부에서만 사용되며 사용자에게 노출되는 값이 아닙니다. 또한 공백을 포함해서는 안 되며, 구독을 생성한 이후에는 절대 변경해서는 안 됩니다. 두 번째 인자는 사용자가 구독할 특정 가격(price)이며, 이 값은 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 ]);

수량(Quantities)

구독을 생성할 때 가격에 대한 특정 수량을 지정하고 싶다면, 구독 빌더에서 create 메서드를 호출하기 전에 quantity 메서드를 호출하면 됩니다.

$user->newSubscription('default', 'price_monthly') ->quantity(5) ->create($paymentMethod);

추가 정보 지정하기

Stripe가 지원하는 추가적인 고객(customer) 또는 구독(subscription) 옵션을 지정하고 싶다면, create 메서드의 두 번째와 세 번째 인자로 전달하면 됩니다.

$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [ 'email' => $email, ], [ 'metadata' => ['note' => 'Some extra information.'], ]);

쿠폰(Coupons)

구독을 생성할 때 쿠폰을 적용하고 싶다면 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여야 합니다. 고객이 입력한 프로모션 코드로부터 실제 ID를 찾아야 한다면 findPromotionCode 메서드를 사용하세요.

// 고객용 코드로 프로모션 코드 ID를 조회 $promotionCode = $user->findPromotionCode('SUMMERSALE'); // 고객용 코드로 활성화된 프로모션 코드 ID를 조회 $promotionCode = $user->findActivePromotionCode('SUMMERSALE');

위 예제에서 반환된 $promotionCode 객체는 Laravel\Cashier\PromotionCode의 인스턴스입니다. 이 클래스는 내부적으로 Stripe\PromotionCode 객체를 감싸고(decorate) 있습니다. coupon 메서드를 호출하면 해당 프로모션 코드에 연결된 쿠폰을 조회할 수 있습니다.

$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();

쿠폰 인스턴스를 통해 할인 금액을 확인하거나, 정액 할인인지 정률 할인인지 판단할 수 있습니다.

if ($coupon->isPercentage()) { return $coupon->percentOff().'%'; // 21.5% } else { return $coupon->amountOff(); // $5.99 }

현재 고객이나 구독에 적용 중인 할인 정보도 조회할 수 있습니다.

$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를 반환합니다. subscribed 메서드는 첫 번째 인자로 구독 타입을 받습니다.

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 상품(product) ID에 해당하는 상품을 구독 중인지 확인할 때 사용합니다. Stripe에서 상품은 여러 가격의 묶음입니다. 아래 예제에서는 사용자의 default 구독이 애플리케이션의 "premium" 상품을 구독 중인지 확인합니다. 여기서 전달하는 Stripe 상품 ID는 Stripe 대시보드에 등록된 상품 식별자와 일치해야 합니다.

if ($user->subscribedToProduct('prod_premium', 'default')) { // ... }

subscribedToProduct 메서드에 배열을 전달하면, 사용자의 default 구독이 "basic" 또는 "premium" 상품 중 하나를 구독 중인지도 확인할 수 있습니다.

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월 10일이었다면, 사용자는 3월 10일까지 유예 기간 상태입니다. 이 기간 동안에는 subscribed 메서드가 여전히 true를 반환한다는 점에 유의하세요.

if ($user->subscription('default')->onGracePeriod()) { // ... }

사용자가 구독을 취소했고 유예 기간도 끝났는지 확인하려면 ended 메서드를 사용하세요.

if ($user->subscription('default')->ended()) { // ... }

미완료(Incomplete) 및 연체(Past Due) 상태

구독 생성 후 추가 결제 인증이 필요한 경우, 해당 구독은 incomplete 상태로 표시됩니다. 구독 상태는 Cashier의 subscriptions 테이블에 있는 stripe_status 컬럼에 저장됩니다.

마찬가지로, 가격을 변경(swap)할 때 추가 결제 인증이 필요하면 구독은 past_due 상태로 표시됩니다. 구독이 이 두 상태 중 하나인 동안에는 고객이 결제를 확인하기 전까지 구독이 활성화되지 않습니다. 청구 가능 모델이나 구독 인스턴스에서 hasIncompletePayment 메서드를 사용하면 미완료 결제 여부를 확인할 수 있습니다.

if ($user->hasIncompletePayment('default')) { // ... } if ($user->subscription('default')->hasIncompletePayment()) { // ... }

구독에 미완료 결제가 있는 경우, latestPayment 식별자를 전달해서 사용자를 Cashier의 결제 확인 페이지로 안내해야 합니다. 구독 인스턴스의 latestPayment 메서드를 사용하면 이 식별자를 조회할 수 있습니다.

<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 메서드를 호출하면 예외가 발생합니다.

구독 스코프(Scopes)

대부분의 구독 상태는 쿼리 스코프로도 제공되므로, 특정 상태의 구독을 데이터베이스에서 손쉽게 조회할 수 있습니다.

// 모든 활성 구독 조회... $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 가격 식별자를 전달하면 됩니다. 가격을 변경할 때, 만약 구독이 이전에 취소된 상태였다면 사용자가 구독을 다시 활성화하려는 것으로 간주됩니다. 전달하는 가격 식별자는 Stripe 대시보드에 등록된 가격 식별자와 일치해야 합니다.

use App\Models\User; $user = App\Models\User::find(1); $user->subscription('default')->swap('price_yearly');

고객이 체험 기간 중이라면 체험 기간은 그대로 유지됩니다. 또한 구독에 "수량(quantity)"이 설정되어 있었다면 그 수량도 유지됩니다.

가격을 변경하면서 현재 진행 중인 체험 기간도 함께 종료하고 싶다면 skipTrial 메서드를 사용하세요.

$user->subscription('default') ->skipTrial() ->swap('price_yearly');

가격을 변경하면서 다음 결제 주기를 기다리지 않고 즉시 청구하고 싶다면 swapAndInvoice 메서드를 사용하세요.

$user = User::find(1); $user->subscription('default')->swapAndInvoice('price_yearly');

일할 계산(Proration)

기본적으로 Stripe는 가격을 변경할 때 요금을 일할 계산(proration)합니다. noProrate 메서드를 사용하면 일할 계산 없이 구독 가격을 변경할 수 있습니다.

$user->subscription('default')->noProrate()->swap('price_yearly');

구독 일할 계산에 대한 더 자세한 내용은 Stripe 문서를 참고하세요.

WARNING

swapAndInvoice 메서드보다 앞서 noProrate 메서드를 실행해도 일할 계산에는 영향을 주지 않습니다. 인보이스는 항상 발행됩니다.

구독 수량(Quantity)

일부 구독은 "수량"의 영향을 받습니다. 예를 들어 프로젝트 관리 애플리케이션이 프로젝트 하나당 월 10달러를 청구하는 경우가 있습니다. 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');

다중 상품 구독

다중 상품 구독을 사용하면 하나의 구독에 여러 결제 상품을 연결할 수 있습니다. 예를 들어 월 10달러의 기본 구독 요금을 받는 고객 지원 "헬프데스크" 애플리케이션을 만든다고 가정해봅시다. 여기에 월 15달러의 실시간 채팅 애드온 상품을 추가로 제공할 수 있습니다. 다중 상품 구독 정보는 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');

특정 수량으로 가격을 추가하고 싶다면, addPrice나 addPriceAndInvoice 메서드의 두 번째 인자로 수량을 전달하면 됩니다.

$user = User::find(1); $user->subscription('default')->addPrice('price_chat', 5);

removePrice 메서드를 사용하면 구독에서 가격을 제거할 수 있습니다.

$user->subscription('default')->removePrice('price_chat');

WARNING

구독의 마지막 남은 가격은 제거할 수 없습니다. 이 경우에는 구독 자체를 취소해야 합니다.

다중 상품 구독에서 가격 변경하기

다중 상품 구독에 연결된 가격들을 변경할 수도 있습니다. 예를 들어 고객이 price_chat 애드온이 포함된 price_basic 구독을 가지고 있는데, 이를 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');

일할 계산(Proration)

기본적으로 Stripe는 다중 상품 구독에서 가격을 추가하거나 제거할 때 요금을 일할 계산합니다. 일할 계산 없이 가격을 조정하고 싶다면, 가격 조작 메서드에 noProrate 메서드를 체이닝하세요.

$user->subscription('default')->noProrate()->removePrice('price_chat');

수량(Quantities)

개별 구독 가격의 수량을 변경하고 싶다면, 기존 수량 관련 메서드에 가격 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');

다중 구독(Multiple Subscriptions)

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 구독의 가격을 변경(swap)하면 됩니다.

$user->subscription('swimming')->swap('price_swimming_yearly');

물론 구독을 완전히 취소할 수도 있습니다.

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

사용량 기반 청구(Usage Based Billing)

사용량 기반 청구를 사용하면 결제 주기 동안의 상품 사용량에 따라 고객에게 요금을 청구할 수 있습니다. 예를 들어 한 달 동안 발송한 문자 메시지나 이메일 개수를 기준으로 요금을 청구할 수 있습니다.

사용량 기반 청구를 시작하려면, 먼저 Stripe 대시보드에서 사용량 기반 청구 모델과 미터(meter)를 사용하는 새 상품을 만들어야 합니다. 미터를 생성한 후, 사용량을 보고하고 조회할 때 필요한 이벤트 이름과 미터 ID를 저장해두세요. 그런 다음 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에 보고해야 합니다. 미터링 이벤트의 사용량을 보고하려면 Billable 모델의 reportMeterEvent 메서드를 사용하세요.

$user = User::find(1); $user->reportMeterEvent('emails-sent');

기본적으로 해당 결제 주기에 사용량(usage quantity) 1이 추가됩니다. 원한다면 특정 사용량 값을 전달해서 고객의 결제 주기 사용량에 반영할 수도 있습니다.

$user = User::find(1); $user->reportMeterEvent('emails-sent', quantity: 15);

특정 미터에 대한 고객의 이벤트 요약을 조회하려면 Billable 인스턴스의 meterEventSummaries 메서드를 사용하세요.

$user = User::find(1); $meterUsage = $user->meterEventSummaries($meterId); $meterUsage->first()->aggregated_value; // 10

미터 이벤트 요약에 대한 더 자세한 내용은 Stripe의 Meter Event Summary object 문서를 참고하세요.

모든 미터를 나열하려면 Billable 인스턴스의 meters 메서드를 사용하세요.

$user = User::find(1); $user->meters();

구독 세금(Subscription Taxes)

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를 변경해도, 기존 구독에 설정된 세금 값은 그대로 유지됩니다. 새로운 taxRates 값으로 기존 구독의 세금 값을 갱신하고 싶다면, 사용자 구독 인스턴스에서 syncTaxRates 메서드를 호출하세요.

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

이 메서드는 다중 상품 구독의 각 아이템별 세율도 함께 동기화합니다. 애플리케이션이 다중 상품 구독을 제공한다면, 청구 가능 모델이 앞서 설명한 priceTaxRates 메서드를 구현하고 있는지 확인해야 합니다.

세금 면제(Tax Exemption)

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)

기본적으로 결제 주기의 기준일(billing cycle anchor)은 구독이 생성된 날짜이며, 체험 기간을 사용하는 경우에는 체험 기간이 끝나는 날짜가 됩니다. 결제 기준일을 직접 조정하고 싶다면 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월 1일에 구독을 취소했지만, 원래 구독 종료 예정일이 3월 5일이었다면 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()->plus(days: 10) );

마지막으로, 사용자 모델을 삭제하기 전에는 항상 해당 사용자의 구독을 먼저 취소해야 합니다.

$user->subscription('default')->cancelNow(); $user->delete();

구독 재개하기

고객이 구독을 취소했다가 다시 재개하고 싶어한다면, 구독의 resume 메서드를 호출하면 됩니다. 구독을 재개하려면 고객이 여전히 "유예 기간" 안에 있어야 합니다.

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

고객이 구독을 취소한 뒤 구독이 완전히 만료되기 전에 다시 재개하면, 고객에게 즉시 요금이 청구되지 않습니다. 대신 구독이 다시 활성화되고, 원래의 결제 주기에 맞춰 요금이 청구됩니다.

구독 체험 기간

결제 수단을 미리 입력받는 경우

체험 기간을 제공하면서도 결제 수단 정보는 미리 받아두고 싶다면, 구독을 생성할 때 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)의 기본 체험 기간 값은 무시되고 덮어써집니다.

WARNING

체험 기간 종료일 이전에 고객이 구독을 취소하지 않으면 체험 기간이 끝나는 즉시 요금이 청구됩니다. 따라서 체험 기간 종료일을 사용자에게 반드시 안내해야 합니다.

trialUntil 메서드를 사용하면 체험 기간이 종료되는 시점을 DateTime 인스턴스로 직접 지정할 수도 있습니다:

use Illuminate\Support\Carbon; $user->newSubscription('default', 'price_monthly') ->trialUntil(Carbon::now()->plus(days: 10)) ->create($paymentMethod);

사용자가 현재 체험 기간 중인지 확인하려면 사용자 인스턴스의 onTrial 메서드나 구독 인스턴스의 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 중 어디서 체험 기간을 정의할지 선택하기

가격(price)별로 몇 일의 체험 기간을 제공할지는 Stripe 대시보드에서 설정하거나, Cashier를 통해 매번 명시적으로 전달하는 방식 중 하나를 선택할 수 있습니다. 만약 Stripe 대시보드에서 가격의 체험 기간을 설정하는 방식을 선택했다면 한 가지 주의할 점이 있습니다. skipTrial() 메서드를 명시적으로 호출하지 않는 한, 새로운 구독은 — 과거에 이미 구독했던 적이 있는 고객의 신규 구독이라 할지라도 — 항상 체험 기간을 적용받게 됩니다.

결제 수단을 미리 입력받지 않는 경우

사용자의 결제 수단 정보를 미리 받지 않고도 체험 기간을 제공하고 싶다면, 사용자 레코드의 trial_ends_at 컬럼에 원하는 체험 종료일을 직접 저장하면 됩니다. 보통 회원가입 시점에 이 값을 설정합니다:

use App\Models\User; $user = User::create([ // ... 'trial_ends_at' => now()->plus(days: 10), ]);

WARNING

청구 대상 모델(billable model) 클래스에 trial_ends_at 속성에 대한 날짜 캐스팅을 반드시 추가해야 합니다.

Cashier에서는 이처럼 특정 구독에 종속되지 않은 체험 방식을 "일반 체험(generic trial)"이라고 부릅니다. 청구 대상 모델 인스턴스의 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()->plus(days: 7) ); // 체험 기간에 5일을 추가합니다... $subscription->extendTrial( $subscription->trial_ends_at->plus(days: 5) );

Stripe 웹훅 처리하기

NOTE

로컬 개발 환경에서 웹훅을 테스트할 때는 Stripe CLI를 활용하면 편리합니다.

Stripe는 다양한 이벤트가 발생할 때 웹훅을 통해 애플리케이션에 알려줍니다. Cashier 서비스 프로바이더는 기본적으로 Cashier의 웹훅 컨트롤러를 가리키는 라우트를 자동으로 등록하며, 이 컨트롤러가 들어오는 모든 웹훅 요청을 처리합니다.

기본적으로 Cashier 웹훅 컨트롤러는 (Stripe 설정에 정의된 대로) 결제 실패가 반복된 구독의 자동 취소, 고객 정보 업데이트, 고객 삭제, 구독 정보 업데이트, 결제 수단 변경 등을 자동으로 처리합니다. 하지만 뒤에서 살펴보겠지만, 원한다면 이 컨트롤러를 확장해서 다른 Stripe 웹훅 이벤트도 원하는 대로 처리할 수 있습니다.

애플리케이션이 Stripe 웹훅을 제대로 처리하려면, Stripe 관리자 패널에서 웹훅 URL을 설정해야 합니다. Cashier의 웹훅 컨트롤러는 기본적으로 /stripe/webhook 경로에 대한 요청을 처리합니다. Stripe 관리자 패널에서 활성화해야 하는 전체 웹훅 목록은 다음과 같습니다.

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • payment_method.automatically_updated
  • invoice.payment_action_required
  • invoice.payment_succeeded

편의를 위해 Cashier는 cashier:webhook Artisan 명령어를 제공합니다. 이 명령어를 실행하면 Cashier에 필요한 모든 이벤트를 수신하는 웹훅이 Stripe에 자동으로 생성됩니다.

php artisan cashier:webhook

생성된 웹훅은 기본적으로 APP_URL 환경 변수 값과 Cashier에 포함된 cashier.webhook 라우트를 조합한 URL을 가리킵니다. 다른 URL을 사용하고 싶다면 명령어 실행 시 --url 옵션을 지정하면 됩니다.

php artisan cashier:webhook --url "https://example.com/stripe/webhook"

생성되는 웹훅은 현재 설치된 Cashier 버전과 호환되는 Stripe API 버전을 사용합니다. 다른 버전을 사용하고 싶다면 --api-version 옵션을 지정하세요.

php artisan cashier:webhook --api-version="2019-12-03"

웹훅은 생성 즉시 활성화됩니다. 만약 웹훅을 생성만 해두고 준비가 될 때까지 비활성화 상태로 두고 싶다면, 명령어 실행 시 --disabled 옵션을 사용하세요.

php artisan cashier:webhook --disabled

WARNING

반드시 Cashier에 포함된 웹훅 서명 검증 미들웨어로 들어오는 Stripe 웹훅 요청을 보호하세요.

웹훅과 CSRF 보호

Stripe 웹훅은 Laravel의 CSRF 보호를 우회해야 하므로, Stripe 웹훅 요청에 대해서는 Laravel이 CSRF 토큰 검증을 시도하지 않도록 설정해야 합니다. 이를 위해 애플리케이션의 bootstrap/app.php 파일에서 stripe/* 경로를 CSRF 보호 대상에서 제외하세요.

->withMiddleware(function (Middleware $middleware): void { $middleware->preventRequestForgery(except: [ 'stripe/*', ]); })

웹훅 이벤트 핸들러 정의하기

Cashier는 결제 실패로 인한 구독 취소를 비롯한 흔히 발생하는 Stripe 웹훅 이벤트들을 자동으로 처리합니다. 하지만 추가로 처리하고 싶은 웹훅 이벤트가 있다면, Cashier가 발생시키는 다음 이벤트를 리스닝해서 원하는 로직을 구현할 수 있습니다.

  • Laravel\Cashier\Events\WebhookReceived
  • Laravel\Cashier\Events\WebhookHandled

두 이벤트 모두 Stripe 웹훅의 전체 페이로드(payload)를 포함하고 있습니다. 예를 들어 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') { // 들어온 이벤트를 처리합니다... } } }

웹훅 서명 검증하기

웹훅을 안전하게 보호하려면 Stripe의 웹훅 서명 기능을 활용할 수 있습니다. Cashier는 편의를 위해 들어오는 Stripe 웹훅 요청이 유효한지 자동으로 검증하는 미들웨어를 기본으로 포함하고 있습니다.

웹훅 검증을 활성화하려면, 애플리케이션의 .env 파일에 STRIPE_WEBHOOK_SECRET 환경 변수가 설정되어 있는지 확인하세요. 웹훅 secret 값은 Stripe 계정 대시보드에서 확인할 수 있습니다.

단건 결제

단순 결제

결제 수단 식별자를 사용해서 고객에게 일회성 청구를 하고 싶다면, 청구 가능한(billable) 모델 인스턴스의 charge 메서드를 사용하면 됩니다. 만약 일회성 결제를 처리하기 전에 고객으로부터 결제 정보를 먼저 수집해야 한다면 단건 결제용 Payment Element 문서를 참고하세요.

use Illuminate\Http\Request; Route::post('/purchase', function (Request $request) { $payment = $request->user()->charge( 100, $request->paymentMethodId ); // ... });

charge 메서드는 세 번째 인수로 배열을 받을 수 있는데, 이를 통해 Stripe Payment Intent를 생성할 때 사용할 수 있는 다양한 옵션을 전달할 수 있습니다. Payment Intent 생성 시 사용 가능한 옵션에 대한 자세한 내용은 Stripe 공식 문서를 참고하세요.

$user->charge(100, $paymentMethod, [ 'custom_option' => $value, ]);

특정 고객/사용자에 종속되지 않은 상태에서도 charge 메서드를 사용할 수 있습니다. 이를 위해서는 애플리케이션의 청구 가능한 모델의 새 인스턴스에서 charge 메서드를 호출하면 됩니다.

use App\Models\User; $payment = (new User)->charge(100, $paymentMethod);

결제가 실패하면 charge 메서드는 예외를 발생시킵니다. 결제가 성공하면 Laravel\Cashier\Payment 인스턴스가 반환됩니다.

try { $payment = $user->charge(100, $paymentMethod); } catch (Exception $e) { // ... }

WARNING

charge 메서드는 결제 금액을 애플리케이션에서 사용하는 통화의 최소 단위로 전달받습니다. 예를 들어 고객이 미국 달러(USD)로 결제한다면 금액은 센트 단위로 지정해야 합니다. 원화(KRW)처럼 최소 단위가 없는 통화를 사용한다면 금액을 그대로(예: 1,000원 → 1000) 전달하면 됩니다.

인보이스와 함께 결제하기

일회성 결제를 진행하면서 고객에게 PDF 인보이스(청구서)를 함께 제공해야 할 때가 있습니다. 이럴 때는 invoicePrice 메서드를 사용하면 됩니다. 예를 들어 고객에게 새 티셔츠 5개에 대한 청구서를 발행해 보겠습니다.

$user->invoicePrice('price_tshirt', 5);

이 인보이스는 즉시 사용자의 기본 결제 수단으로 청구됩니다. invoicePrice 메서드의 세 번째 인수 역시 배열을 받을 수 있으며, 이 배열에는 해당 인보이스 항목(invoice item)에 대한 청구 옵션이 들어갑니다. 네 번째 인수도 배열이며, 인보이스 자체에 대한 청구 옵션을 담습니다.

$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 메서드도 사용할 수 있지만, 미리 정의된 가격(price)과 함께 invoicePrice, tabPrice 메서드를 사용하는 것을 권장합니다. 이렇게 하면 Stripe 대시보드에서 상품별 판매 데이터와 더 나은 분석 정보를 확인할 수 있습니다.

WARNING

invoice, invoicePrice, invoiceFor 메서드는 결제 실패 시 재시도를 수행하는 Stripe 인보이스를 생성합니다. 결제 실패 시 재시도를 원하지 않는다면, 첫 결제 실패 이후 Stripe API를 사용해서 해당 인보이스를 직접 닫아(close) 주어야 합니다.

Payment Intent 생성하기

청구 가능한 모델 인스턴스에서 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 메서드는 결제 금액을 애플리케이션에서 사용하는 통화의 최소 단위로 전달받습니다. 예를 들어 고객이 미국 달러(USD)로 결제한다면 금액은 센트 단위로 지정해야 합니다.

결제 환불하기

Stripe 결제를 환불해야 한다면 refund 메서드를 사용할 수 있습니다. 이 메서드는 첫 번째 인수로 Stripe Payment Intent ID를 받습니다.

$payment = $user->charge(100, $paymentMethodId); $user->refund($payment->id);

인보이스

인보이스 조회하기

invoices 메서드를 사용하면 결제 가능한 모델(billable model)의 인보이스 목록을 손쉽게 조회할 수 있습니다. invoices 메서드는 Laravel\Cashier\Invoice 인스턴스로 구성된 컬렉션을 반환합니다.

$invoices = $user->invoices();

미확정(pending) 인보이스까지 함께 조회하고 싶다면 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');

여러 개의 새 요금제를 동시에 적용하는 경우를 미리 확인하고 싶다면, previewInvoice 메서드에 요금제 배열을 전달하면 됩니다.

$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

인보이스 PDF 생성하기

인보이스 PDF를 생성하기 전에, Cashier의 기본 인보이스 렌더러인 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' => 'Your Company', 'product' => 'Your Product', 'street' => 'Main Str. 1', 'location' => '2000 Antwerp, Belgium', 'phone' => '+32 499 00 00 00', 'email' => 'info@example.com', 'url' => 'https://example.com', 'vendorVat' => 'BE123456789', ]);

NOTE

실제 서비스에서는 vendor, street, location 등의 값을 여러분 회사의 실제 사업자 정보(예: 상호명, 사업장 주소, 대표 연락처)로 채워 넣으면 됩니다. 이 데이터는 Stripe 데이터와 무관하게 PDF 렌더링 시에만 사용됩니다.

downloadInvoice 메서드는 세 번째 인자로 파일명을 지정할 수도 있습니다. 이때 지정한 파일명 뒤에는 자동으로 .pdf 확장자가 붙습니다.

return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');

커스텀 인보이스 렌더러

Cashier에서는 인보이스 렌더러를 원하는 방식으로 직접 구현해서 사용할 수도 있습니다. 기본적으로 Cashier는 dompdf PHP 라이브러리를 활용하는 DompdfInvoiceRenderer 구현체를 사용합니다. 하지만 Laravel\Cashier\Contracts\InvoiceRenderer 인터페이스를 구현하면 원하는 어떤 렌더러든 사용할 수 있습니다. 예를 들어, 서드파티 PDF 변환 API를 호출하여 인보이스 PDF를 생성하고 싶다면 다음과 같이 작성할 수 있습니다.

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 설정값을 방금 만든 커스텀 렌더러 클래스명으로 변경해주면 됩니다.

Checkout

Cashier Stripe는 Stripe Checkout도 지원합니다. Stripe Checkout은 결제를 받기 위한 페이지를 직접 구현하는 번거로움을 없애주는, 미리 만들어진 호스팅 결제 페이지입니다.

아래 내용은 Cashier와 함께 Stripe Checkout을 사용하는 방법을 설명합니다. Stripe Checkout에 대해 더 자세히 알고 싶다면 Stripe 공식 Checkout 문서도 함께 참고하시기 바랍니다.

상품 Checkout

Stripe 대시보드에 이미 등록되어 있는 상품에 대해 결제를 진행하려면, billable 모델의 checkout 메서드를 사용하면 됩니다. 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'), ]); });

success_url 옵션을 정의할 때, Checkout 세션 ID를 쿼리 문자열 파라미터로 URL에 추가하도록 Stripe에 지시할 수 있습니다. 이를 위해서는 success_url의 쿼리 문자열에 {CHECKOUT_SESSION_ID}라는 리터럴 문자열을 추가하면 됩니다. Stripe가 이 플레이스홀더를 실제 Checkout 세션 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은 기본적으로 사용자가 직접 입력하는 프로모션 코드를 허용하지 않습니다. 다행히 Checkout 페이지에서 이 기능을 손쉽게 활성화할 수 있는 방법이 있습니다. allowPromotionCodes 메서드를 호출하면 됩니다.

use Illuminate\Http\Request; Route::get('/product-checkout', function (Request $request) { return $request->user() ->allowPromotionCodes() ->checkout('price_tshirt'); });

단건 결제 Checkout

Stripe 대시보드에 등록되어 있지 않은 임시(ad-hoc) 상품에 대해서도 간단하게 결제를 진행할 수 있습니다. billable 모델의 checkoutCharge 메서드에 결제 금액, 상품명, 그리고 선택적으로 수량을 전달하면 됩니다. 고객이 이 라우트에 접속하면 Stripe의 Checkout 페이지로 리다이렉트됩니다.

use Illuminate\Http\Request; Route::get('/charge-checkout', function (Request $request) { return $request->user()->checkoutCharge(1200, 'T-Shirt', 5); });

WARNING

checkoutCharge 메서드를 사용하면 Stripe는 항상 대시보드에 새로운 상품과 가격을 새로 생성합니다. 따라서 가능하다면 Stripe 대시보드에서 상품을 미리 만들어두고 checkout 메서드를 사용하는 것을 권장합니다.

구독 Checkout

WARNING

구독에 Stripe Checkout을 사용하려면 Stripe 대시보드에서 customer.subscription.created 웹훅을 반드시 활성화해야 합니다. 이 웹훅은 데이터베이스에 구독 레코드를 생성하고 관련된 구독 아이템 정보를 저장하는 역할을 합니다.

Stripe Checkout을 사용해 구독을 시작할 수도 있습니다. Cashier의 구독 빌더 메서드로 구독 내용을 정의한 뒤 checkout 메서드를 호출하면 됩니다. 고객이 이 라우트에 접속하면 Stripe의 Checkout 페이지로 리다이렉트됩니다.

use Illuminate\Http\Request; Route::get('/subscription-checkout', function (Request $request) { return $request->user() ->newSubscription('default', 'price_monthly') ->checkout(); });

상품 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'), ]); });

물론 구독 Checkout에서도 프로모션 코드를 활성화할 수 있습니다.

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) 동작, 결제 동작(payment behavior)을 설정해도 Stripe Checkout 세션에서는 아무런 효과가 없습니다. 사용 가능한 파라미터 목록은 Stripe Checkout Session API 문서를 참고하시기 바랍니다.

Stripe Checkout과 체험 기간

Stripe Checkout으로 완료되는 구독을 만들 때도 체험 기간을 정의할 수 있습니다.

$checkout = Auth::user()->newSubscription('default', 'price_monthly') ->trialDays(3) ->checkout();

단, 체험 기간은 최소 48시간 이상이어야 합니다. 이는 Stripe Checkout이 지원하는 최소 체험 기간입니다.

구독과 웹훅

Stripe와 Cashier는 웹훅을 통해 구독 상태를 업데이트한다는 점을 기억하세요. 따라서 고객이 결제 정보를 입력하고 애플리케이션으로 돌아왔을 때, 구독이 아직 활성화되지 않았을 가능성이 있습니다. 이런 상황을 대비해 결제나 구독이 아직 처리 중이라는 안내 메시지를 보여주는 것이 좋습니다.

세금 ID 수집

Checkout에서는 고객의 세금 ID(Tax ID)를 수집하는 기능도 지원합니다. Checkout 세션을 생성할 때 collectTaxIds 메서드를 호출하면 이 기능을 활성화할 수 있습니다.

$checkout = $user->collectTaxIds()->checkout('price_tshirt');

이 메서드를 호출하면, 고객이 회사 자격으로 구매하는지 여부를 표시할 수 있는 체크박스가 화면에 추가로 표시됩니다. 체크박스를 선택하면 고객은 자신의 세금 ID 번호를 입력할 수 있습니다.

WARNING

애플리케이션의 서비스 프로바이더에서 이미 자동 세금 계산을 설정해두었다면 이 기능은 자동으로 활성화되므로 collectTaxIds 메서드를 별도로 호출할 필요가 없습니다.

게스트 Checkout

Checkout::guest 메서드를 사용하면 "계정"이 없는 게스트 사용자를 위한 Checkout 세션도 시작할 수 있습니다.

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'), ]); });

기존 사용자를 위한 Checkout 세션을 생성할 때와 마찬가지로, Laravel\Cashier\CheckoutBuilder 인스턴스에서 제공하는 추가 메서드를 활용해 게스트 Checkout 세션을 커스터마이징할 수 있습니다.

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'), ]); });

게스트 Checkout이 완료되면 Stripe는 checkout.session.completed 웹훅 이벤트를 발송할 수 있으므로, Stripe 웹훅 설정에서 이 이벤트가 애플리케이션으로 실제로 전송되도록 반드시 설정해두어야 합니다. Stripe 대시보드에서 웹훅을 활성화한 뒤에는 Cashier로 웹훅 처리하기 섹션에 따라 이를 처리하면 됩니다. 이때 웹훅 페이로드에 담긴 객체는 checkout 객체이며, 이 객체의 내용을 확인해 고객의 주문을 처리(fulfill)하면 됩니다.

결제 실패 처리하기

구독이나 단건 결제를 진행하다 보면 결제가 실패하는 상황이 생길 수 있습니다. 이런 경우 Cashier는 Laravel\Cashier\Exceptions\IncompletePayment 예외를 발생시켜 문제가 발생했음을 알려줍니다. 이 예외를 잡은(catch) 후에는 두 가지 방법으로 대응할 수 있습니다.

첫 번째 방법은 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로 다시 리다이렉트됩니다. 이때 URL에는 message(문자열)와 success(정수) 쿼리 스트링 변수가 자동으로 추가됩니다. 현재 이 결제 페이지는 다음과 같은 결제 수단들을 지원합니다:

  • 신용카드
  • Alipay
  • Bancontact
  • BECS Direct Debit
  • EPS
  • Giropay
  • iDEAL
  • SEPA Direct Debit

두 번째 방법은 결제 확인 과정을 Stripe에 맡기는 것입니다. 이 경우 결제 확인 페이지로 리다이렉트하는 대신, Stripe 대시보드에서 Stripe의 자동 청구 이메일 기능을 설정하면 됩니다. 다만 이 방식을 사용하더라도 IncompletePayment 예외가 발생한 경우, 사용자에게 추가 결제 확인 안내가 담긴 이메일을 받게 될 것이라는 사실을 알려주는 것이 좋습니다.

결제 예외는 Billable 트레이트를 사용하는 모델의 charge, invoiceFor, invoice 메서드에서 발생할 수 있습니다. 구독을 다룰 때는 SubscriptionBuilder의 create 메서드, 그리고 Subscription과 SubscriptionItem 모델의 incrementAndInvoice, swapAndInvoice 메서드에서도 결제 미완료 예외가 발생할 수 있습니다.

기존 구독에 결제 미완료 상태가 있는지 확인하려면, 청구 가능 모델(billable model) 또는 구독 인스턴스에서 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 문서를 참고하시기 바랍니다.

결제(Cashier)

Strong Customer Authentication (SCA)

여러분의 비즈니스나 고객이 유럽에 기반을 두고 있다면, EU의 강력한 고객 인증(Strong Customer Authentication, SCA) 규정을 준수해야 합니다. 이 규정은 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 상태에 대한 더 자세한 내용은 해당 상태에 관한 추가 문서를 참고하세요.

세션 외(Off-Session) 결제 알림

SCA 규정에서는 구독이 활성 상태인 동안에도 고객이 이따금 결제 정보를 다시 검증해야 하는 경우가 있습니다. 이런 이유로 Cashier는 세션 외(off-session) 상황에서 결제 확인이 필요할 때 고객에게 알림을 보낼 수 있습니다. 예를 들어 구독이 갱신되는 시점에 이런 상황이 발생할 수 있습니다. 이 알림 기능은 CASHIER_PAYMENT_NOTIFICATION 환경 변수에 알림(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();

Stripe 구독을 바로 업데이트하려면 updateStripeSubscription 메서드를 사용할 수도 있습니다:

$subscription->updateStripeSubscription(['application_fee_percent' => 5]);

Stripe\StripeClient 클라이언트를 직접 사용하고 싶다면 Cashier 클래스의 stripe 메서드를 호출하면 됩니다. 예를 들어, 이 메서드를 사용해 StripeClient 인스턴스에 접근한 뒤 Stripe 계정에 등록된 가격(price) 목록을 조회할 수 있습니다:

use Laravel\Cashier\Cashier; $prices = Cashier::stripe()->prices->all();

NOTE

Stripe SDK 객체를 직접 다룰 때는 Stripe 공식 문서를 함께 참고하는 것이 좋습니다. Cashier가 제공하지 않는 세부적인 옵션이나 필드를 다뤄야 할 때 특히 유용합니다.

결제 (Cashier)

테스트

Cashier를 사용하는 애플리케이션을 테스트할 때, Stripe API로 나가는 실제 HTTP 요청을 목(mock)으로 대체할 수도 있습니다. 하지만 이렇게 하려면 Cashier 내부 동작을 어느 정도 다시 구현해야 하는 번거로움이 있습니다. 따라서 저희는 테스트가 실제 Stripe API를 호출하도록 두는 방식을 권장합니다. 속도는 다소 느려지지만, 애플리케이션이 실제로 의도한 대로 동작하는지에 대해 훨씬 더 높은 확신을 얻을 수 있습니다. 실행 속도가 느린 테스트들은 별도의 Pest / PHPUnit 테스트 그룹으로 분리해서 관리하면 됩니다.

테스트를 작성할 때 한 가지 기억해야 할 점은, Cashier 자체는 이미 훌륭한 테스트 스위트를 갖추고 있다는 것입니다. 따라서 여러분은 Cashier의 내부 동작 하나하나를 검증하려 하기보다, 자신의 애플리케이션에서 구현한 구독 및 결제 흐름에 집중해서 테스트를 작성하는 것이 좋습니다.

시작하려면, phpunit.xml 파일에 Stripe의 테스트용 시크릿 키를 추가하세요:

<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>

이렇게 설정하면 테스트 중에 Cashier와 상호작용할 때마다 실제로 Stripe 테스트 환경으로 API 요청이 전송됩니다. 테스트를 원활하게 진행하려면, Stripe 테스트 계정에 테스트에서 사용할 구독(subscription)과 가격(price) 정보를 미리 등록해 두는 것이 편리합니다.

NOTE

카드 거절이나 결제 실패 등 다양한 결제 시나리오를 테스트하려면, Stripe에서 제공하는 다양한 테스트용 카드 번호와 토큰을 활용할 수 있습니다.

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 9월 17일