Laravel Cashier (Paddle)
번역일: 2026년 7월 2일
Laravel Cashier (Paddle)
소개
Laravel Cashier Paddle은 Paddle의 구독 결제 서비스를 Laravel 애플리케이션에서 손쉽게 사용할 수 있도록 도와주는 공식 패키지입니다. 반복적으로 작성하게 되는 구독 관련 보일러플레이트 코드 대부분을 처리해 주며, 구독 생성부터 쿠폰 적용, 플랜 변경, 구독 수량 관리까지 다양한 기능을 제공합니다.
Cashier를 사용하기 전에 Paddle의 개념 가이드와 API 문서도 함께 살펴보시길 권장합니다.
Cashier 업그레이드
Cashier를 새 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 확인하세요.
설치
Composer로 Cashier Paddle 패키지를 설치합니다.
composer require laravel/cashier-paddle그다음, vendor:publish Artisan 명령어로 Cashier의 마이그레이션 파일을 퍼블리시합니다.
php artisan vendor:publish --tag="cashier-migrations"그런 다음 마이그레이션을 실행하세요. Cashier는 customers, subscriptions, subscription_items, transactions 테이블을 생성합니다.
php artisan migratePaddle 샌드박스
개발 및 스테이징 환경에서는 Paddle 샌드박스 계정을 사용하세요. 샌드박스 계정은 실제 결제 없이 다양한 결제 시나리오를 안전하게 테스트할 수 있는 격리된 환경을 제공합니다. Paddle의 테스트용 카드 번호를 활용해 다양한 결제 시나리오를 테스트할 수 있습니다.
샌드박스 환경을 사용할 때는 애플리케이션의 .env 파일에 PADDLE_SANDBOX 환경 변수를 true로 설정하세요.
PADDLE_SANDBOX=true애플리케이션 개발이 완료되어 실서비스 배포를 준비할 때는 Paddle 벤더 계정을 신청하고, PADDLE_SANDBOX를 false로 변경하면 됩니다.
설정
청구 가능 모델
Cashier를 사용하려면 결제 대상이 되는 모델(일반적으로 User 모델)에 Billable 트레이트를 추가해야 합니다. 이 트레이트는 구독 생성, 체크아웃 세션 시작 등 다양한 결제 관련 메서드를 제공합니다.
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}User 이외의 모델을 청구 대상으로 사용하고 싶다면, 해당 모델에도 동일하게 트레이트를 추가하면 됩니다. 예를 들어 팀 단위로 구독을 관리하는 서비스라면 Team 모델에 추가할 수 있습니다.
API 키
다음으로 .env 파일에 Paddle API 키를 설정합니다. Paddle 대시보드에서 API 키를 확인할 수 있습니다.
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
PADDLE_API_KEY=your-paddle-api-key
PADDLE_RETAIN_KEY=your-paddle-retain-key
PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"
PADDLE_SANDBOX=truePADDLE_SANDBOX 환경 변수는 Paddle 샌드박스 환경 사용 여부를 제어합니다. 실서비스 배포 시에는 이 값을 false로 설정하세요.
WARNING
PADDLE_RETAIN_KEY는 선택 사항이며, Paddle의 Retain 기능(구독 이탈 방지)을 사용하는 경우에만 설정합니다.
Paddle JS
Paddle은 자체 JavaScript 라이브러리를 통해 체크아웃 위젯을 초기화합니다. 애플리케이션의 레이아웃 Blade 파일 </head> 태그 바로 앞에 @paddleJS Blade 디렉티브를 추가하세요.
<head>
...
@paddleJS
</head>통화 설정
인보이스 등에 금액을 표시할 때 사용할 로케일을 지정할 수 있습니다. Cashier 내부적으로는 PHP의 NumberFormatter 클래스를 사용해 통화를 포맷합니다.
CASHIER_CURRENCY_LOCALE=ko_KRWARNING
en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 확장이 설치되어 있어야 합니다.
기본 모델 재정의
Cashier가 내부적으로 사용하는 모델을 직접 확장할 수 있습니다. 커스텀 모델을 만들고 Cashier의 기본 모델을 상속받으면 됩니다.
use Laravel\Paddle\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}모델을 정의한 후에는 Laravel\Paddle\Cashier 클래스를 통해 Cashier에 커스텀 모델을 등록합니다. 보통 애플리케이션의 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 설정합니다.
use App\Models\Cashier\Subscription;
use App\Models\Cashier\Transaction;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useTransactionModel(Transaction::class);
}빠른 시작
단일 상품 판매
NOTE
체크아웃을 시작하기 전에 Paddle 대시보드에서 고정 가격이 설정된 상품을 먼저 등록해 두어야 합니다.
애플리케이션에서 상품 및 구독 결제를 제공하는 방법은 다양합니다. 아래는 가장 일반적인 방식입니다. 여기서는 단일 상품 일회성 결제를 위한 Paddle 체크아웃 버튼을 화면에 렌더링합니다.
먼저 라우트에서 checkout 메서드로 체크아웃 세션을 생성하고, 해당 세션을 paddle-button Blade 컴포넌트에 전달합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout('pri_deluxe_base')
->returnTo(route('dashboard'));
return view('buy', ['checkout' => $checkout]);
});checkout 메서드의 첫 번째 인수는 Paddle 대시보드에서 확인할 수 있는 상품의 가격 ID(pri_... 형식)입니다. returnTo 메서드에는 결제 완료 후 사용자가 이동할 URL을 지정합니다.
뷰 파일에서는 paddle-button Blade 컴포넌트를 사용해 체크아웃 버튼을 렌더링합니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구매하기
</x-paddle-button>버튼 클릭 시 Paddle의 체크아웃 오버레이가 표시됩니다. 오버레이 없이 인라인 체크아웃 방식을 원한다면 paddle-checkout 컴포넌트를 사용하면 됩니다.
<x-paddle-checkout :checkout="$checkout" class="w-full" />비회원 체크아웃
로그인하지 않은 사용자에게도 결제 기능을 제공해야 할 때가 있습니다. 이 경우 Cashier 파사드의 guest 메서드를 사용하면 됩니다.
use Illuminate\Http\Request;
use Laravel\Paddle\Cashier;
Route::get('/buy', function (Request $request) {
$checkout = Cashier::guest()->checkout('pri_deluxe_base')
->returnTo(route('home'));
return view('buy', ['checkout' => $checkout]);
});이후 뷰에서 버튼이나 인라인 체크아웃 컴포넌트를 동일하게 사용하면 됩니다.
구독 판매
NOTE
체크아웃을 시작하기 전에 Paddle 대시보드에서 고정 가격이 설정된 상품을 먼저 등록해 두어야 합니다.
애플리케이션에서 구독 판매를 제공하는 방법도 단일 상품과 유사합니다. 먼저 라우트에서 체크아웃 세션을 생성합니다.
use Illuminate\Http\Request;
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->checkout('pri_monthly_basic')
->returnTo(route('dashboard'));
return view('subscribe', ['checkout' => $checkout]);
});뷰 파일에서는 paddle-button 컴포넌트를 통해 체크아웃 버튼을 렌더링합니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독 시작하기
</x-paddle-button>구독 상태 확인
결제가 완료되면 Paddle은 웹훅을 통해 애플리케이션에 상태를 전달합니다. 웹훅 설정 방법은 웹훅 처리 섹션을 참고하세요.
Paddle에서 전달되는 웹훅을 처리하면 데이터베이스에 구독 정보가 업데이트됩니다. 이후 subscribed 메서드로 사용자의 구독 여부를 확인할 수 있습니다.
@if ($user->subscribed())
<p>구독 중입니다.</p>
@endif구독 중인 사용자인지 확인하는 미들웨어도 쉽게 만들 수 있습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* 들어오는 요청을 처리합니다.
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed()) {
// 구독하지 않은 사용자는 결제 페이지로 리디렉션합니다.
return redirect('/subscribe');
}
return $next($request);
}
}특정 플랜을 구독 중인지 확인하고 싶다면 subscribedToPrice 메서드를 사용하세요.
@if ($user->subscribedToPrice('pri_monthly_basic'))
<p>월간 기본 플랜 구독 중입니다.</p>
@endif구독 완료 페이지 표시
Paddle 체크아웃 완료 후에는 "구독이 완료되었습니다" 등의 안내 메시지를 보여주는 것이 좋습니다. 다만 웹훅이 전달되어 구독 정보가 데이터베이스에 반영되는 데 약간의 시간이 걸릴 수 있습니다.
Cashier가 발행하는 Laravel\Paddle\Events\WebhookReceived 이벤트를 통해 웹훅을 처리하거나, Cashier의 paddle-receipt Blade 컴포넌트를 활용하면 Paddle의 거래 정보가 동기화될 때까지 안내 메시지를 표시할 수 있습니다.
<x-paddle-receipt :checkout="$checkout" />paddle-receipt 컴포넌트는 내부적으로 Paddle API를 폴링하여 구독 정보가 준비되면 자동으로 화면을 업데이트합니다. 이 컴포넌트는 returnTo로 지정한 완료 페이지에서 사용하면 됩니다.
체크아웃 세션
Cashier Paddle에서 결제는 대부분 "체크아웃 세션"을 통해 이루어집니다. 체크아웃 세션은 Paddle에 어떤 상품을 얼마에 판매할지 알려주고, 사용자가 결제를 완료할 수 있는 UI를 제공합니다.
오버레이 체크아웃
오버레이 체크아웃은 Paddle의 기본 결제 방식으로, 현재 페이지 위에 오버레이 형태의 결제창이 뜹니다. paddle-button Blade 컴포넌트를 사용하면 됩니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독 시작하기
</x-paddle-button>버튼 컴포넌트 대신 링크 형태로 오버레이를 띄울 수도 있습니다.
<x-paddle-button tag="a" :checkout="$checkout">
구독 시작하기
</x-paddle-button>NOTE
오버레이 체크아웃이 제대로 동작하려면 레이아웃에 @paddleJS 디렉티브가 포함되어 있어야 합니다.
오버레이 커스터마이징
오버레이 체크아웃에 추가 설정을 적용하고 싶다면 체크아웃 세션 생성 메서드에 다양한 옵션을 체이닝할 수 있습니다. 예를 들어 customData 메서드로 커스텀 데이터를 함께 전달할 수 있습니다.
$checkout = $user->checkout('pri_monthly_basic')
->customData(['user_id' => $user->id]);사용 가능한 모든 설정 옵션은 Paddle 개발자 문서를 참고하세요.
인라인 체크아웃
오버레이 방식 대신 결제 UI를 페이지 내에 직접 삽입하는 인라인 체크아웃 방식을 사용할 수도 있습니다. paddle-checkout Blade 컴포넌트를 사용하면 됩니다.
<x-paddle-checkout :checkout="$checkout" class="w-full" />인라인 체크아웃 컴포넌트의 높이는 자동으로 조절되지 않으므로, 필요에 따라 height 속성으로 높이를 지정할 수 있습니다.
<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />인라인 체크아웃의 세부 커스터마이징 방법은 Paddle의 인라인 체크아웃 빌드 가이드를 참고하세요.
비회원 체크아웃
로그인하지 않은 비회원 사용자를 위해 결제 기능을 제공할 때는 Cashier 파사드의 guest 메서드를 사용합니다.
use Illuminate\Http\Request;
use Laravel\Paddle\Cashier;
Route::get('/buy', function (Request $request) {
$checkout = Cashier::guest()->checkout('pri_deluxe_base')
->returnTo(route('home'));
return view('buy', ['checkout' => $checkout]);
});이후 오버레이 또는 인라인 체크아웃 컴포넌트를 동일하게 사용하면 됩니다.
비회원 체크아웃은 기존 Paddle 고객 정보와 연결되지 않습니다. 결제 완료 후 사용자 계정과 결제 내역을 연결하려면 별도의 처리가 필요합니다.
가격 미리보기
Paddle은 통화별로 다른 가격을 설정할 수 있습니다. Cashier의 previewPrices 메서드를 사용하면 특정 가격 ID들의 금액을 조회할 수 있습니다. 이를 통해 사용자의 국가에 맞는 가격을 미리 표시할 수 있습니다.
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_standard', 'pri_premium']);반환된 가격 정보는 뷰에서 다음과 같이 표시할 수 있습니다.
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>소계와 세금을 분리해서 표시할 수도 있습니다.
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->subtotal() }} + {{ $price->tax() }} 세금</li>
@endforeach
</ul>자세한 내용은 Paddle 가격 미리보기 API 문서를 참고하세요.
고객 가격 미리보기
이미 Paddle에 등록된 고객이라면 해당 고객에게 적용되는 실제 가격을 조회할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/price-preview', function (Request $request) {
$prices = $request->user()->previewPrices(['pri_standard', 'pri_premium']);
// ...
});할인
할인된 가격을 미리보기로 표시할 수도 있습니다. previewPrices 메서드 호출 시 discount_id 옵션으로 할인 ID를 전달하면 됩니다.
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_standard', 'pri_premium'], [
'discount_id' => 'dsc_early_bird',
]);조회된 가격은 동일한 방식으로 표시하면 됩니다.
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>고객
고객 기본값
Cashier를 사용하면 체크아웃 세션 생성 시 고객 정보(이름, 이메일 등)를 자동으로 채워줄 수 있습니다. Billable 트레이트가 적용된 모델에 다음 메서드를 재정의하면 됩니다.
/**
* 고객의 이름을 반환합니다.
*/
public function paddleName(): string|null
{
return $this->name;
}
/**
* 고객의 이메일 주소를 반환합니다.
*/
public function paddleEmail(): string|null
{
return $this->email;
}이렇게 설정하면 체크아웃 시 고객 정보가 자동으로 입력됩니다.
고객 조회
Cashier::findBillable 메서드로 Paddle 고객 ID를 사용해 청구 가능 모델 인스턴스를 조회할 수 있습니다.
use Laravel\Paddle\Cashier;
$user = Cashier::findBillable($customerId);고객 생성
체크아웃 없이 Paddle 고객을 먼저 생성해야 하는 경우 createAsCustomer 메서드를 사용합니다.
$customer = $user->createAsCustomer();Paddle\Customer 인스턴스가 반환됩니다. Paddle에 고객이 생성된 후에는 언제든지 구독을 시작할 수 있습니다. Paddle API에서 지원하는 추가 파라미터를 배열로 전달할 수도 있습니다.
$customer = $user->createAsCustomer($options);구독
구독 생성
구독을 생성하려면 먼저 데이터베이스에서 청구 가능 모델 인스턴스를 조회합니다. 보통 User 모델입니다. 그다음 subscribe 메서드로 체크아웃 세션을 생성합니다.
use Illuminate\Http\Request;
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($premium = 'pri_monthly_premium', 'default')
->returnTo(route('dashboard'));
return view('subscribe', ['checkout' => $checkout]);
});subscribe 메서드의 첫 번째 인수는 가격 ID, 두 번째 인수는 구독의 내부 이름(type)입니다. 내부 이름은 애플리케이션에서 구독을 구분할 때 사용하는 임의의 문자열입니다. 애플리케이션이 단일 구독만 제공한다면 default나 primary 같은 값을 사용하면 됩니다. 이 구독 타입은 내부 관리 목적으로만 사용되며 사용자에게 표시되지 않습니다.
구독이 현재 활성 상태인지 확인하고 싶다면 subscribed 메서드를 사용합니다.
if ($user->subscribed()) {
// ...
}특정 타입의 구독 여부를 확인할 때는 타입을 인수로 전달합니다.
if ($user->subscribed('default')) {
// ...
}사용자가 체크아웃을 완료하면 Paddle 웹훅을 통해 데이터베이스에 구독 정보가 저장됩니다. subscribed 메서드는 데이터베이스의 구독 레코드를 기준으로 동작하므로, 웹훅이 처리되기 전에는 아직 false를 반환할 수 있습니다.
구독 상태 확인
사용자가 구독 중인지 확인하는 데 사용할 수 있는 다양한 메서드가 있습니다.
// 유효한 구독 중인지 확인 (체험 기간 포함)
$user->subscribed();
// 특정 타입의 구독 중인지 확인
$user->subscribed('default');
// 특정 가격으로 구독 중인지 확인
$user->subscribedToPrice('pri_monthly_basic', 'default');
// 구독 중이고 체험 기간이 아닌지 확인
$user->subscribedAndNotOnTrial('default');
// 구독이 만료 예정인지 확인 (취소했지만 아직 유효한 경우)
$user->onGracePeriod('default');
// 구독이 완전히 취소되었는지 확인
$user->canceled('default');구독 인스턴스를 직접 가져와서 상태를 확인할 수도 있습니다.
$subscription = $user->subscription('default');
// 구독이 활성 상태인지 확인
$subscription->active();
// 구독이 체험 기간인지 확인
$subscription->onTrial();
// 구독이 일시정지 상태인지 확인
$subscription->paused();
// 구독이 취소되었는지 확인
$subscription->canceled();Blade 템플릿에서 사용
미들웨어나 뷰에서 구독 상태를 확인하는 방법은 다음과 같습니다.
@if ($user->subscribed())
<p>구독 중입니다.</p>
@elseif ($user->onGracePeriod())
<p>구독이 취소되었지만 유효 기간이 남아있습니다.</p>
@else
<p>구독 중이 아닙니다.</p>
@endif라우트 미들웨어
구독하지 않은 사용자의 접근을 차단하는 미들웨어를 만들 수 있습니다.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed()) {
return redirect('/subscribe');
}
return $next($request);
}
}구독 단건 청구
구독 중인 사용자에게 추가 금액을 일회성으로 청구할 수 있습니다.
$response = $user->subscription('default')->charge('pri_extra_feature');결제 정보 업데이트
Paddle은 항상 구독별 결제 수단을 저장합니다. 구독의 기본 결제 수단을 변경하려면 구독 모델의 updateUrl 메서드로 결제 수단 업데이트 URL을 가져와 사용자에게 안내하면 됩니다.
use Illuminate\Http\Request;
Route::get('/update-payment-method', function (Request $request) {
$user = $request->user();
return view('update-payment-method', [
'updateUrl' => $user->subscription('default')->updateUrl(),
]);
});<a href="{{ $updateUrl }}">결제 수단 업데이트</a>플랜 변경
구독 중인 사용자가 더 높은 플랜이나 낮은 플랜으로 변경하려면 구독 인스턴스의 swap 메서드를 사용합니다.
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap('pri_yearly_premium');바로 청구하지 않고 다음 갱신 시점에 플랜을 변경하려면 swapNextBillingPeriod 메서드를 사용합니다.
$user->subscription('default')->swapNextBillingPeriod('pri_yearly_premium');구독 수량
일부 구독은 "수량"을 기반으로 청구됩니다. 예를 들어 프로젝트 관리 도구가 프로젝트당 월 10,000원을 청구하는 경우입니다. incrementQuantity, decrementQuantity, updateQuantity 메서드로 구독 수량을 조절할 수 있습니다.
$user = User::find(1);
// 수량 1 증가
$user->subscription('default')->incrementQuantity();
// 수량 5 증가
$user->subscription('default')->incrementQuantity(5);
// 수량 1 감소
$user->subscription('default')->decrementQuantity();
// 수량을 특정 값으로 설정
$user->subscription('default')->updateQuantity(10);여러 상품을 포함한 구독
여러 상품을 포함한 구독은 하나의 구독에 여러 상품을 묶어서 판매하는 방식입니다. 예를 들어 고객 지원 플랫폼에서 기본 구독 외에 실시간 채팅 추가 기능을 함께 구독하도록 할 수 있습니다.
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe([
'pri_monthly_basic',
'pri_chat_addon',
])->returnTo(route('dashboard'));
return view('subscribe', ['checkout' => $checkout]);
});특정 가격 ID의 구독 여부를 확인할 때는 subscribedToPrice 메서드를 사용합니다.
if ($user->subscribedToPrice('pri_chat_addon', 'default')) {
// 채팅 추가 기능을 구독 중입니다.
}구독에 아이템을 추가하거나 교체하거나 제거하는 것도 가능합니다.
$user->subscription('default')->swapAndInvoice(['pri_standard', 'pri_chat_addon']);복수 구독
Paddle은 한 고객이 여러 개의 구독을 동시에 유지할 수 있습니다. 예를 들어 헬스장 회원이 수영 구독과 PT 구독을 각각 가지고 있을 수 있습니다. 구독 생성 시 두 번째 인수로 타입을 구분합니다.
// 수영 구독 생성
$checkout = $user->subscribe('pri_swimming', 'swimming');
// PT 구독 생성
$checkout = $user->subscribe('pri_pt', 'pt');각 구독은 타입으로 구분하여 독립적으로 관리됩니다.
$user->subscription('swimming')->swap('pri_swimming_premium');
$user->subscription('pt')->cancel();구독 일시정지
구독을 일시정지하면 사용자의 결제가 중단되고, 일시정지가 해제될 때까지 서비스 접근을 막을 수 있습니다.
$user->subscription('default')->pause();일시정지된 구독은 paused 상태가 됩니다. Paddle은 현재 청구 기간이 끝날 때 구독을 실제로 일시정지합니다. 그 전까지는 paused_from 속성에서 일시정지 시작 시점을 확인할 수 있습니다.
if ($subscription->paused()) {
// 구독이 일시정지되었습니다.
}
if ($subscription->pausedFrom()->isFuture()) {
// 아직 일시정지 예정 상태입니다.
}일시정지된 구독을 재개하려면 resume 메서드를 사용합니다.
$user->subscription('default')->resume();구독 취소
구독을 취소하려면 cancel 메서드를 사용합니다.
$user->subscription('default')->cancel();구독이 취소되면 Paddle은 현재 청구 기간이 끝날 때 구독을 만료시킵니다. 취소 후 청구 기간이 끝나기 전까지는 onGracePeriod 메서드가 true를 반환합니다.
if ($user->subscription('default')->onGracePeriod()) {
// 유예 기간 중입니다.
}즉시 취소하려면 cancelNow 메서드를 사용합니다.
$user->subscription('default')->cancelNow();취소한 구독을 유예 기간 중에 복구하려면 resume 메서드를 사용합니다.
$user->subscription('default')->resume();구독 체험 기간
결제 수단을 먼저 등록하는 방식
결제 수단을 먼저 받고 체험 기간을 제공하려면 Paddle 대시보드에서 해당 가격의 체험 기간을 설정하거나, 체크아웃 세션 생성 시 trialDays 메서드를 사용합니다.
$checkout = $user->subscribe('pri_monthly', 'default')
->trialDays(14)
->returnTo(route('dashboard'));체험 기간 중인지 확인하려면 onTrial 메서드를 사용합니다.
if ($user->subscription('default')->onTrial()) {
// 체험 기간 중입니다.
}결제 수단 없이 시작하는 방식
결제 수단 없이 체험 기간을 제공하려면 사용자 모델의 trial_ends_at 컬럼에 체험 종료 날짜를 저장합니다.
$user = User::create([
// ...
]);
$user->createAsCustomer([
'trial_ends_at' => now()->addDays(14),
]);Cashier는 이 방식의 체험을 "일반 체험(generic trial)"이라고 부릅니다. 이 경우 아직 구독이 생성된 것이 아니므로, onTrial 메서드는 현재 날짜가 trial_ends_at 이전인지를 기준으로 동작합니다.
if ($user->onTrial()) {
// 체험 기간 중입니다.
}
if ($user->onGenericTrial()) {
// 일반 체험 기간 중입니다. (구독 미생성)
}체험 기간 연장 또는 활성화
이미 진행 중인 구독의 체험 기간을 연장하거나, 체험 기간을 즉시 종료하고 구독을 활성화하는 메서드도 제공됩니다.
// 체험 기간을 특정 날짜로 연장
$user->subscription('default')->extendTrial(now()->addDays(7));
// 체험 기간을 즉시 종료하고 구독 활성화
$user->subscription('default')->activateTrial();Paddle 웹훅 처리
Paddle은 구독 생성, 갱신, 취소 등 다양한 이벤트 발생 시 애플리케이션에 웹훅을 전송합니다. Cashier는 기본적으로 이러한 웹훅을 처리하는 컨트롤러를 제공하며, cashier/webhook 라우트를 자동으로 등록합니다.
Paddle 대시보드에서 웹훅 엔드포인트 URL을 https://your-app.com/paddle/webhook으로 설정하세요.
Cashier가 처리하는 웹훅 이벤트는 다음과 같습니다.
customer.updatedtransaction.completedtransaction.updatedsubscription.activatedsubscription.createdsubscription.updatedsubscription.pausedsubscription.resumedsubscription.past_duesubscription.canceled
웹훅 이벤트 핸들러 정의
Cashier는 웹훅을 수신할 때 Laravel 이벤트를 발행합니다. 커스텀 처리가 필요하다면 해당 이벤트에 대한 리스너를 등록하면 됩니다.
예를 들어 구독이 취소되었을 때 특별한 처리를 하고 싶다면:
<?php
namespace App\Listeners;
use Laravel\Paddle\Events\WebhookReceived;
class PaddleEventListener
{
public function handle(WebhookReceived $event): void
{
if ($event->payload['event_type'] === 'subscription.canceled') {
// 구독 취소 시 추가 처리...
}
}
}그런 다음 EventServiceProvider에 리스너를 등록합니다.
use App\Listeners\PaddleEventListener;
use Laravel\Paddle\Events\WebhookReceived;
protected $listen = [
WebhookReceived::class => [
PaddleEventListener::class,
],
];Cashier는 처리된 웹훅 이벤트 타입별로 별도의 이벤트도 발행합니다. 이 이벤트들은 Paddle의 페이로드와 함께 처리된 모델(청구 가능 모델, 구독 모델 등)도 포함합니다.
Laravel\Paddle\Events\CustomerUpdatedLaravel\Paddle\Events\TransactionCompletedLaravel\Paddle\Events\TransactionUpdatedLaravel\Paddle\Events\SubscriptionCreatedLaravel\Paddle\Events\SubscriptionUpdatedLaravel\Paddle\Events\SubscriptionPausedLaravel\Paddle\Events\SubscriptionCanceled
웹훅 서명 검증
웹훅의 보안을 위해 Paddle은 각 웹훅 요청에 서명을 포함합니다. Cashier는 자동으로 이 서명을 검증합니다. .env 파일에 PADDLE_WEBHOOK_SECRET 값을 올바르게 설정해 두면 됩니다.
PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"Paddle 대시보드의 웹훅 설정 페이지에서 서명 시크릿을 확인할 수 있습니다.
단건 결제
상품 결제
구독 없이 단일 상품을 결제하려면 checkout 메서드를 사용합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout('pri_deluxe_base');
return view('buy', ['checkout' => $checkout]);
});여러 상품을 한 번에 구매하게 하려면 가격 ID 배열을 전달합니다.
$checkout = $request->user()->checkout(['pri_standard', 'pri_addon']);거래 환불
거래를 환불하려면 Transaction 모델의 refund 메서드를 사용합니다.
use App\Models\User;
$user = User::find(1);
$transaction = $user->transactions()->first();
$response = $transaction->refund();부분 환불도 가능합니다. 금액을 센트 단위로 전달합니다.
// 10,000원 부분 환불
$response = $transaction->refund(1000000);거래 크레딧 지급
환불 대신 크레딧(계정 잔액)으로 지급하려면 credit 메서드를 사용합니다.
$response = $transaction->credit();거래 내역
지난 결제 및 예정 결제
사용자의 과거 결제 내역과 예정 결제를 조회할 수 있습니다. transactions 메서드로 과거 거래 목록을 가져올 수 있습니다.
use Illuminate\Http\Request;
Route::get('/transactions', function (Request $request) {
return view('transactions', [
'transactions' => $request->user()->transactions()->paginate(10),
]);
});뷰에서는 다음과 같이 표시합니다.
<ul>
@foreach ($transactions as $transaction)
<li>
{{ $transaction->billed_at->toFormattedDateString() }} -
{{ $transaction->total() }} ({{ $transaction->tax() }} 세금)
<a href="{{ route('download-invoice', $transaction->id) }}">인보이스 다운로드</a>
</li>
@endforeach
</ul>인보이스 PDF를 다운로드하려면 거래 인스턴스의 invoice 메서드를 사용합니다.
use Illuminate\Http\Request;
Route::get('/transactions/{transaction}/invoice', function (Request $request, $transactionId) {
$transaction = $request->user()->transactions()->findOrFail($transactionId);
return $transaction->invoice()->download();
})->name('download-invoice');다음 예정 결제를 조회하려면 nextPayment 메서드를 사용합니다.
$subscription = $user->subscription('default');
$nextPayment = $subscription->nextPayment();
$nextPayment->amount(); // 결제 금액
$nextPayment->date(); // 결제 예정일테스트
Cashier를 사용하는 코드를 테스트할 때는 실제 Paddle API 호출을 모킹(mocking)하는 것이 좋습니다.
Cashier의 fake 메서드를 사용하면 Paddle API 호출을 모킹하고, 구독 및 트랜잭션 관련 데이터를 로컬에서 제어할 수 있습니다.
use Laravel\Paddle\Cashier;
Cashier::fake();테스트 중 특정 상태의 구독 또는 트랜잭션 데이터가 필요하다면 모델 팩토리를 활용하세요.
use Laravel\Paddle\Subscription;
$subscription = Subscription::factory()->create([
'billable_id' => $user->id,
'billable_type' => get_class($user),
'status' => 'active',
]);실제 웹훅 통합을 포함한 종단 간(end-to-end) 테스트가 필요하다면 Paddle 샌드박스 환경을 활용하세요. 샌드박스에서 진행한 모든 결제와 구독은 실제 청구가 이루어지지 않으므로 안전하게 테스트할 수 있습니다.
Laravel Cashier (Paddle)
소개
WARNING
이 문서는 Paddle Billing과 연동되는 Cashier Paddle 2.x를 기준으로 작성되었습니다. 아직 Paddle Classic을 사용 중이라면 Cashier Paddle 1.x를 참고하세요.
Laravel Cashier Paddle은 Paddle의 구독 결제 서비스를 간결하고 유창한 인터페이스로 제공합니다. 구독 결제에 필요한 반복적인 보일러플레이트 코드 대부분을 Cashier가 대신 처리해 줍니다. 기본적인 구독 관리 외에도 구독 변경, 구독 수량 조정, 구독 일시 중지, 해지 유예 기간 처리 등 다양한 기능을 지원합니다.
Cashier Paddle을 본격적으로 사용하기 전에, Paddle의 개념 가이드와 API 문서를 함께 살펴보시길 권장합니다.
Laravel Cashier (Paddle)
Cashier 업그레이드
Cashier를 새 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 확인하세요.
Laravel Cashier (Paddle)
설치
Composer 패키지 매니저를 사용하여 Paddle용 Cashier 패키지를 설치합니다:
composer require laravel/cashier-paddle설치 후, vendor:publish Artisan 명령어로 Cashier 마이그레이션 파일을 퍼블리시합니다:
php artisan vendor:publish --tag="cashier-migrations"이어서 마이그레이션을 실행합니다. Cashier 마이그레이션은 다음 테이블들을 생성합니다:
| 테이블 | 설명 |
|---|---|
customers | 고객 정보 저장 |
subscriptions | 구독 정보 저장 |
subscription_items | 구독 항목 상세 저장 |
transactions | Paddle 결제 트랜잭션 저장 |
php artisan migrateWARNING
Cashier가 Paddle의 모든 이벤트를 올바르게 처리하려면 반드시 Cashier 웹훅 처리를 설정해야 합니다.
Paddle 샌드박스
로컬 및 스테이징 개발 환경에서는 Paddle 샌드박스 계정을 별도로 등록하여 사용하는 것을 권장합니다. 샌드박스 환경에서는 실제 결제 없이 다양한 결제 시나리오를 안전하게 테스트할 수 있습니다. Paddle이 제공하는 테스트용 카드 번호를 활용하면 결제 성공, 실패, 환불 등 다양한 상황을 시뮬레이션할 수 있습니다.
Paddle 샌드박스 환경을 사용할 때는 애플리케이션의 .env 파일에 PADDLE_SANDBOX 환경 변수를 true로 설정합니다:
PADDLE_SANDBOX=trueNOTE
샌드박스 계정과 실제 운영 계정은 완전히 별개입니다. 샌드박스에서 생성한 상품, 가격, 구독 정보는 운영 환경으로 자동 이전되지 않으므로, 운영 전환 시 Paddle 대시보드에서 별도로 설정해야 합니다.
개발이 완료되면 Paddle 벤더 계정을 신청할 수 있습니다. 운영 환경 배포 전에 Paddle로부터 애플리케이션 도메인에 대한 승인을 받아야 한다는 점을 유의하세요.
Laravel Cashier (Paddle)
설정
Billable 모델
Cashier를 사용하기 전에, 먼저 사용자 모델에 Billable 트레이트를 추가해야 합니다. 이 트레이트는 구독 생성, 결제 수단 정보 업데이트 등 일반적인 결제 관련 작업을 수행하는 다양한 메서드를 제공합니다:
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}사용자 외에 결제 가능한 엔티티(예: 팀, 조직 등)가 있다면, 해당 클래스에도 동일하게 트레이트를 추가할 수 있습니다:
use Illuminate\Database\Eloquent\Model;
use Laravel\Paddle\Billable;
class Team extends Model
{
use Billable;
}API 키
다음으로, 애플리케이션의 .env 파일에 Paddle 키를 설정해야 합니다. Paddle API 키는 Paddle 관리 패널에서 확인할 수 있습니다:
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
PADDLE_API_KEY=your-paddle-api-key
PADDLE_RETAIN_KEY=your-paddle-retain-key
PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"
PADDLE_SANDBOX=truePaddle 샌드박스 환경을 사용할 때는 PADDLE_SANDBOX를 true로 설정하세요. 실제 운영 환경(라이브)으로 배포할 때는 false로 변경해야 합니다.
PADDLE_RETAIN_KEY는 선택 사항으로, Paddle의 Retain 기능을 사용할 때만 설정하면 됩니다.
Paddle JS
Paddle은 결제 위젯을 초기화하기 위해 자체 JavaScript 라이브러리를 사용합니다. 레이아웃 파일의 </head> 태그 바로 앞에 @paddleJS Blade 디렉티브를 추가하면 라이브러리를 자동으로 로드할 수 있습니다:
<head>
...
@paddleJS
</head>통화 설정
인보이스에 금액을 표시할 때 사용할 로케일을 지정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용하여 통화 형식을 처리합니다. 예를 들어 한국 원화 형식으로 표시하려면 다음과 같이 설정합니다:
CASHIER_CURRENCY_LOCALE=ko_KRWARNING
en 이외의 로케일을 사용하려면 서버에 ext-intl PHP 확장 모듈이 설치되어 있어야 합니다.
기본 모델 재정의
Cashier가 내부적으로 사용하는 모델을 직접 확장하여 커스터마이징할 수 있습니다. 먼저 커스텀 모델을 정의하고, 해당 Cashier 모델을 상속합니다:
use Laravel\Paddle\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}모델을 정의한 후에는 App\Providers\AppServiceProvider의 boot 메서드에서 Cashier가 커스텀 모델을 사용하도록 지정합니다:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\Transaction;
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useTransactionModel(Transaction::class);
}Laravel Cashier (Paddle)
빠른 시작
단순 상품 판매
NOTE
Paddle Checkout을 사용하기 전에, Paddle 대시보드에서 고정 가격이 설정된 상품(Product)을 먼저 등록해야 합니다. 또한 Paddle 웹훅 처리도 미리 설정해 두어야 합니다.
결제 기능을 직접 구현하는 것은 꽤 복잡한 작업처럼 느껴질 수 있습니다. 하지만 Cashier와 Paddle의 Checkout Overlay를 활용하면, 현대적이고 안정적인 결제 흐름을 손쉽게 구축할 수 있습니다.
일회성 단건 상품을 판매하는 경우, Cashier의 checkout 메서드를 사용해 Paddle Checkout Overlay를 띄울 수 있습니다. 고객이 결제 정보를 입력하고 구매를 완료하면, 지정한 성공 URL로 리디렉션됩니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout('pri_deluxe_album')
->returnTo(route('dashboard'));
return view('buy', ['checkout' => $checkout]);
})->name('checkout');위 예시처럼, checkout 메서드에 Paddle "가격 식별자(price identifier)"를 전달하면 체크아웃 객체가 생성됩니다. Paddle에서 "가격(price)"이란 특정 상품에 연결된 판매 단가를 의미합니다.
필요한 경우 checkout 메서드는 Paddle에 고객 레코드를 자동으로 생성하고, 이를 애플리케이션 데이터베이스의 사용자와 연결합니다. 결제가 완료되면 고객은 성공 페이지로 이동하게 되며, 그곳에서 구매 완료 메시지를 표시할 수 있습니다.
buy 뷰에서는 Checkout Overlay를 띄우는 버튼을 추가합니다. Cashier Paddle에는 paddle-button Blade 컴포넌트가 기본으로 포함되어 있습니다. 필요하다면 오버레이 체크아웃을 직접 렌더링하는 방법도 있습니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
상품 구매
</x-paddle-button>Paddle Checkout에 메타 데이터 전달하기
상품을 판매할 때는 애플리케이션 내부의 Cart(장바구니)나 Order(주문) 모델과 결제 결과를 연결해야 하는 경우가 많습니다. Paddle Checkout Overlay로 이동하기 전에 주문 식별자(ID)와 같은 커스텀 데이터를 함께 전달하면, 결제 완료 후 웹훅을 통해 해당 주문을 정확히 찾아 처리할 수 있습니다.
아래 예시는 고객이 결제를 시작할 때 Order를 미리 생성해 두고, 해당 주문 ID를 Paddle Checkout에 전달하는 방식입니다. 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',
]);
$checkout = $request->user()->checkout($order->price_ids)
->customData(['order_id' => $order->id]);
return view('billing', ['checkout' => $checkout]);
})->name('checkout');customData 메서드로 전달한 order_id는 Paddle이 웹훅 이벤트를 발송할 때 그대로 포함하여 돌려줍니다. 이를 통해 결제 완료 후 해당 주문을 정확히 조회하고 상태를 업데이트할 수 있습니다.
결제가 완료되면 Paddle이 웹훅을 발송하고, Cashier는 이를 이벤트로 변환합니다. TransactionCompleted 이벤트를 리스닝하면 주문 처리를 자동화할 수 있습니다. 이벤트 리스너는 보통 AppServiceProvider의 boot 메서드에 등록합니다.
use App\Listeners\CompleteOrder;
use Illuminate\Support\Facades\Event;
use Laravel\Paddle\Events\TransactionCompleted;
/**
* 애플리케이션 서비스 부트스트랩.
*/
public function boot(): void
{
Event::listen(TransactionCompleted::class, CompleteOrder::class);
}CompleteOrder 리스너는 다음과 같이 구현할 수 있습니다.
namespace App\Listeners;
use App\Models\Order;
use Laravel\Paddle\Cashier;
use Laravel\Paddle\Events\TransactionCompleted;
class CompleteOrder
{
/**
* Cashier 웹훅 이벤트 처리.
*/
public function handle(TransactionCompleted $event): void
{
$orderId = $event->payload['data']['custom_data']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
}
}transaction.completed 이벤트의 전체 페이로드 구조는 Paddle 공식 문서를 참고하세요.
구독 판매
NOTE
Paddle Checkout을 사용하기 전에, Paddle 대시보드에서 고정 가격이 설정된 상품을 먼저 등록해야 합니다. 또한 Paddle 웹훅 처리도 미리 설정해 두어야 합니다.
구독 결제도 Cashier와 Paddle Checkout Overlay를 조합하면 간결하게 구현할 수 있습니다.
예를 들어, 월간(price_basic_monthly)과 연간(price_basic_yearly) 두 가지 요금제를 제공하는 "Basic" 구독 상품(pro_basic)과, 고급 플랜인 "Expert"(pro_expert)가 있다고 가정해 보겠습니다.
고객이 가격 페이지에서 "Basic 플랜 구독" 버튼을 클릭하면, 해당 요금제에 대한 Checkout Overlay가 열립니다. 아래와 같이 checkout 메서드로 구독 세션을 시작합니다.
use Illuminate\Http\Request;
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->checkout('price_basic_monthly')
->returnTo(route('dashboard'));
return view('subscribe', ['checkout' => $checkout]);
})->name('subscribe');subscribe 뷰에서는 Checkout Overlay를 여는 버튼을 추가합니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독 시작
</x-paddle-button>버튼을 클릭하면 고객이 결제 정보를 입력하고 구독을 시작할 수 있습니다. 일부 결제 수단은 처리에 수 초가 걸리기 때문에, 구독 시작 시점을 정확히 파악하려면 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('/subscribe');
}
return $next($request);
}
}미들웨어를 정의한 후, 보호할 라우트에 적용합니다.
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);구독 플랜 변경 및 취소 허용하기
고객이 구독 플랜을 변경하거나 취소할 수 있는 기능도 제공해야 합니다. 예를 들어 월간 플랜에서 연간 플랜으로 업그레이드하는 경우, 아래와 같은 라우트를 통해 처리할 수 있습니다.
use Illuminate\Http\Request;
Route::put('/subscription/{price}/swap', function (Request $request, $price) {
// 예: $price = 'price_basic_yearly'
$request->user()->subscription()->swap($price);
return redirect()->route('dashboard');
})->name('subscription.swap');구독 취소도 비슷하게 구현합니다. 취소를 요청하면 현재 청구 주기가 끝나는 시점에 구독이 종료됩니다.
use Illuminate\Http\Request;
Route::put('/subscription/cancel', function (Request $request) {
$request->user()->subscription()->cancel();
return redirect()->route('dashboard');
})->name('subscription.cancel');NOTE
Cashier의 웹훅 처리를 설정해 두면, Paddle에서 전송하는 웹훅을 수신하여 데이터베이스의 구독 상태를 자동으로 동기화합니다. 예를 들어 Paddle 대시보드에서 직접 구독을 취소하더라도, Cashier가 해당 웹훅을 받아 데이터베이스의 구독 상태를 "취소됨"으로 업데이트합니다.
Laravel Cashier (Paddle)
체크아웃 세션
고객에게 요금을 청구하는 대부분의 작업은 Paddle의 Checkout Overlay 위젯이나 인라인 체크아웃을 통해 이루어집니다.
Paddle로 결제를 처리하기 전에, Paddle 체크아웃 설정 대시보드에서 애플리케이션의 기본 결제 링크를 먼저 지정해야 합니다.
Overlay 체크아웃
Checkout Overlay 위젯을 화면에 표시하려면 먼저 Cashier로 체크아웃 세션을 생성해야 합니다. 체크아웃 세션은 위젯에 어떤 결제 작업을 수행할지 알려주는 역할을 합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});Cashier에는 paddle-button Blade 컴포넌트가 내장되어 있습니다. 체크아웃 세션을 이 컴포넌트의 prop으로 전달하면, 버튼을 클릭했을 때 Paddle 체크아웃 위젯이 열립니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독하기
</x-paddle-button>기본적으로 위젯은 Paddle의 기본 스타일로 표시됩니다. data-theme='light' 같은 Paddle 지원 속성을 컴포넌트에 추가하면 위젯 스타일을 원하는 대로 조정할 수 있습니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light">
구독하기
</x-paddle-button>Paddle 체크아웃 위젯은 비동기로 동작합니다. 사용자가 위젯 안에서 구독을 완료하면, Paddle이 애플리케이션으로 웹훅을 전송합니다. 이 웹훅을 받아서 데이터베이스의 구독 상태를 올바르게 업데이트해야 합니다. 따라서 웹훅 설정을 반드시 올바르게 구성해야 합니다.
WARNING
구독 상태가 변경된 후 웹훅이 도착하기까지의 지연은 보통 짧지만, 체크아웃 완료 직후에는 구독 정보가 아직 반영되지 않았을 수 있습니다. 애플리케이션 설계 시 이 점을 반드시 고려하세요.
Overlay 체크아웃 직접 렌더링하기
Laravel의 내장 Blade 컴포넌트를 사용하지 않고 Overlay 체크아웃을 직접 구현할 수도 있습니다. 먼저 앞의 예시와 동일하게 체크아웃 세션을 생성합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});다음으로, Paddle.js를 사용해 체크아웃을 초기화합니다. 아래 예시에서는 paddle_button 클래스가 지정된 링크를 생성합니다. Paddle.js는 이 클래스를 자동으로 감지하여, 링크를 클릭하면 Overlay 체크아웃을 표시합니다.
<?php
$items = $checkout->getItems();
$customer = $checkout->getCustomer();
$custom = $checkout->getCustomData();
?>
<a
href='#!'
class='paddle_button'
data-items='{!! json_encode($items) !!}'
@if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
@if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
@if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
>
상품 구매하기
</a>인라인 체크아웃
Overlay 방식이 아닌, 체크아웃 위젯을 페이지 안에 직접 삽입하고 싶다면 인라인 체크아웃을 사용할 수 있습니다. 이 방식은 체크아웃 HTML 필드를 직접 수정하는 것은 불가능하지만, 위젯을 애플리케이션 화면에 자연스럽게 내장할 수 있습니다.
Cashier는 인라인 체크아웃을 간편하게 사용할 수 있도록 paddle-checkout Blade 컴포넌트를 제공합니다. 먼저 체크아웃 세션을 생성합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});그런 다음, 체크아웃 세션을 컴포넌트의 checkout 속성으로 전달합니다.
<x-paddle-checkout :checkout="$checkout" class="w-full" />인라인 체크아웃 컴포넌트의 높이를 조정하려면 height 속성을 사용합니다.
<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />인라인 체크아웃의 더 다양한 커스터마이징 옵션은 Paddle의 인라인 체크아웃 가이드와 체크아웃 설정 옵션을 참고하세요.
인라인 체크아웃 직접 렌더링하기
내장 Blade 컴포넌트를 사용하지 않고 인라인 체크아웃을 직접 구현할 수도 있습니다. 먼저 앞의 예시와 동일하게 체크아웃 세션을 생성합니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $user->checkout('pri_34567')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});다음으로 Paddle.js를 사용해 체크아웃을 초기화합니다. 아래 예시는 Alpine.js를 활용한 구현이지만, 사용 중인 프론트엔드 스택에 맞게 자유롭게 변경할 수 있습니다.
<?php
$options = $checkout->options();
$options['settings']['frameTarget'] = 'paddle-checkout';
$options['settings']['frameInitialHeight'] = 366;
?>
<div class="paddle-checkout" x-data="{}" x-init="
Paddle.Checkout.open(@json($options));
">
</div>게스트 체크아웃
애플리케이션 계정이 없는 비회원 사용자를 위한 체크아웃 세션이 필요한 경우에는 guest 메서드를 사용합니다.
use Illuminate\Http\Request;
use Laravel\Paddle\Checkout;
Route::get('/buy', function (Request $request) {
$checkout = Checkout::guest(['pri_34567'])
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});생성된 체크아웃 세션은 Paddle 버튼 또는 인라인 체크아웃 Blade 컴포넌트에 동일하게 전달하여 사용할 수 있습니다.
Laravel Cashier (Paddle)
가격 미리보기
Paddle은 통화별로 가격을 커스터마이징할 수 있어, 국가마다 다른 가격을 설정할 수 있습니다. Cashier Paddle은 previewPrices 메서드를 통해 이러한 가격 정보를 조회할 수 있습니다. 이 메서드에 조회하려는 가격 ID 배열을 전달하면 됩니다:
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456']);기본적으로 통화는 요청의 IP 주소를 기반으로 자동 결정됩니다. 특정 국가의 가격을 조회하려면 address 옵션을 통해 국가 코드와 우편번호를 직접 지정할 수도 있습니다:
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [
'country_code' => 'KR',
'postal_code' => '06236',
]]);가격 정보를 조회한 후에는 원하는 방식으로 화면에 표시할 수 있습니다:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>소계(세전 금액)와 세금을 분리하여 표시할 수도 있습니다:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} 세금)</li>
@endforeach
</ul>가격 미리보기에 대한 자세한 내용은 Paddle 공식 API 문서를 참고하세요.
특정 고객에 대한 가격 미리보기
이미 등록된 고객에게 해당 고객에게 적용되는 가격을 보여주고 싶다면, 고객 인스턴스에서 직접 가격을 조회할 수 있습니다:
use App\Models\User;
$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);내부적으로 Cashier는 해당 사용자의 고객 ID를 이용해 적절한 통화로 가격을 조회합니다. 예를 들어, 미국에 거주하는 사용자에게는 USD로, 한국에 거주하는 사용자에게는 KRW로 가격이 표시됩니다. 일치하는 통화를 찾을 수 없는 경우에는 해당 상품의 기본 통화가 사용됩니다. 상품 또는 구독 플랜의 가격은 Paddle 관리 패널에서 통화별로 자유롭게 설정할 수 있습니다.
할인 적용 가격 미리보기
할인이 적용된 가격을 미리 보여주고 싶다면, previewPrices 메서드를 호출할 때 discount_id 옵션으로 할인 ID를 전달하면 됩니다:
use Laravel\Paddle\Cashier;
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
'discount_id' => 'dsc_123'
]);할인이 반영된 가격은 다음과 같이 표시할 수 있습니다:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
@endforeach
</ul>Laravel Cashier (Paddle)
고객 (Customers)
고객 기본값 설정
Cashier를 사용하면 체크아웃 세션을 생성할 때 고객 정보의 기본값을 미리 지정할 수 있습니다. 기본값을 설정해두면 체크아웃 위젯에서 이메일 주소와 이름이 자동으로 채워져, 고객이 바로 결제 단계로 넘어갈 수 있습니다.
빌링 모델에서 다음 메서드를 오버라이드하여 기본값을 설정하세요:
/**
* Paddle에 연동할 고객 이름을 반환합니다.
*/
public function paddleName(): string|null
{
return $this->name;
}
/**
* Paddle에 연동할 고객 이메일 주소를 반환합니다.
*/
public function paddleEmail(): string|null
{
return $this->email;
}이 기본값은 Cashier에서 체크아웃 세션을 생성하는 모든 동작에 공통으로 적용됩니다.
고객 조회
Paddle 고객 ID로 고객을 조회하려면 Cashier::findBillable 메서드를 사용합니다. 해당 빌링 모델의 인스턴스가 반환됩니다:
use Laravel\Paddle\Cashier;
$user = Cashier::findBillable($customerId);고객 생성
구독을 시작하지 않고 Paddle 고객만 먼저 생성하고 싶은 경우, createAsCustomer 메서드를 사용할 수 있습니다:
$customer = $user->createAsCustomer();이 메서드는 Laravel\Paddle\Customer 인스턴스를 반환합니다. Paddle에 고객이 생성된 이후에는 원하는 시점에 구독을 시작할 수 있습니다. Paddle API에서 지원하는 추가 고객 생성 파라미터가 있다면 $options 배열로 전달할 수 있습니다:
$customer = $user->createAsCustomer($options);구독 (Subscriptions)
구독 생성
구독을 생성하려면 먼저 데이터베이스에서 청구 대상 모델 인스턴스를 가져옵니다. 일반적으로 App\Models\User 인스턴스가 됩니다. 모델 인스턴스를 가져온 후 subscribe 메서드를 사용해 체크아웃 세션을 생성할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});subscribe 메서드의 첫 번째 인수는 사용자가 구독할 Paddle 가격 식별자입니다. 두 번째 인수는 구독의 내부 "타입"으로, 애플리케이션이 단일 구독만 제공한다면 default 또는 primary와 같이 지정할 수 있습니다. 이 구독 타입은 내부 용도로만 사용되며 사용자에게 노출되지 않습니다. 또한 공백을 포함해서는 안 되며, 구독을 생성한 이후에는 절대 변경하지 않아야 합니다.
customData 메서드를 사용해 구독에 커스텀 메타데이터를 추가할 수도 있습니다.
$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
->customData(['key' => 'value'])
->returnTo(route('home'));체크아웃 세션을 생성한 후에는 Cashier Paddle에 포함된 paddle-button Blade 컴포넌트에 전달하면 됩니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독하기
</x-paddle-button>사용자가 체크아웃을 완료하면 Paddle에서 subscription_created 웹훅이 발송됩니다. Cashier는 이 웹훅을 수신해 구독을 설정합니다. 모든 웹훅이 올바르게 수신·처리되려면 웹훅 처리 설정이 제대로 완료되어 있어야 합니다.
구독 상태 확인
사용자가 구독한 후에는 다양한 편의 메서드로 구독 상태를 확인할 수 있습니다. subscribed 메서드는 사용자가 유효한 구독을 보유하고 있으면(트라이얼 기간 포함) true를 반환합니다.
if ($user->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()) {
// 유료 구독자가 아닌 경우...
return redirect('/billing');
}
return $next($request);
}
}사용자가 아직 트라이얼 기간 중인지 확인하려면 onTrial 메서드를 사용합니다. 트라이얼 중임을 사용자에게 안내하는 메시지를 표시할 때 유용합니다.
if ($user->subscription()->onTrial()) {
// ...
}subscribedToPrice 메서드는 사용자가 특정 Paddle 가격 ID의 플랜을 구독 중인지 확인합니다. 아래 예시에서는 default 구독이 월간 가격을 활성 구독 중인지 확인합니다.
if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
// ...
}recurring 메서드는 사용자가 현재 활성 구독 중이며 트라이얼 기간이나 유예 기간이 아닌지를 확인합니다.
if ($user->subscription()->recurring()) {
// ...
}구독 취소 상태
사용자가 한때 활성 구독자였지만 구독을 취소했는지 확인하려면 canceled 메서드를 사용합니다.
if ($user->subscription()->canceled()) {
// ...
}구독을 취소했지만 아직 구독이 완전히 만료되지 않아 "유예 기간(grace period)" 중인지도 확인할 수 있습니다. 예를 들어 3월 10일에 만료 예정인 구독을 3월 5일에 취소한 경우, 사용자는 3월 10일까지 유예 기간에 해당합니다. 유예 기간 동안에는 subscribed 메서드가 여전히 true를 반환합니다.
if ($user->subscription()->onGracePeriod()) {
// ...
}결제 연체 상태
구독 결제가 실패하면 구독이 past_due 상태로 표시됩니다. 이 상태에서는 고객이 결제 정보를 업데이트할 때까지 구독이 활성화되지 않습니다. pastDue 메서드로 연체 여부를 확인할 수 있습니다.
if ($user->subscription()->pastDue()) {
// ...
}구독이 연체 상태라면 사용자에게 결제 정보를 업데이트하도록 안내해야 합니다.
past_due 상태의 구독도 유효한 것으로 처리하고 싶다면 Cashier가 제공하는 keepPastDueSubscriptionsActive 메서드를 사용할 수 있습니다. 일반적으로 AppServiceProvider의 register 메서드에서 호출합니다.
use Laravel\Paddle\Cashier;
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
}WARNING
구독이 past_due 상태일 때는 결제 정보를 업데이트하기 전까지 구독을 변경할 수 없습니다. 따라서 이 상태에서 swap 또는 updateQuantity 메서드를 호출하면 예외가 발생합니다.
구독 스코프
대부분의 구독 상태는 쿼리 스코프로도 제공됩니다. 특정 상태의 구독을 데이터베이스에서 쉽게 조회할 수 있습니다.
// 유효한 구독 전체 조회...
$subscriptions = Subscription::query()->valid()->get();
// 특정 사용자의 취소된 구독 조회...
$subscriptions = $user->subscriptions()->canceled()->get();사용 가능한 스코프 목록은 다음과 같습니다.
Subscription::query()->valid();
Subscription::query()->onTrial();
Subscription::query()->expiredTrial();
Subscription::query()->notOnTrial();
Subscription::query()->active();
Subscription::query()->recurring();
Subscription::query()->pastDue();
Subscription::query()->paused();
Subscription::query()->notPaused();
Subscription::query()->onPausedGracePeriod();
Subscription::query()->notOnPausedGracePeriod();
Subscription::query()->canceled();
Subscription::query()->notCanceled();
Subscription::query()->onGracePeriod();
Subscription::query()->notOnGracePeriod();구독 단건 청구
구독 단건 청구를 사용하면 정기 구독 외에 일회성 요금을 구독자에게 청구할 수 있습니다. charge 메서드에 하나 또는 여러 개의 가격 ID를 전달합니다.
// 단일 가격 청구...
$response = $user->subscription()->charge('pri_123');
// 여러 가격 동시 청구...
$response = $user->subscription()->charge(['pri_123', 'pri_456']);charge 메서드는 다음 청구 주기에 실제로 고객에게 청구됩니다. 즉시 청구하려면 chargeAndInvoice 메서드를 사용하세요.
$response = $user->subscription()->chargeAndInvoice('pri_123');결제 정보 업데이트
Paddle은 구독별로 결제 수단을 저장합니다. 구독의 기본 결제 수단을 변경하려면 구독 모델의 redirectToUpdatePaymentMethod 메서드를 사용해 Paddle이 제공하는 결제 수단 업데이트 페이지로 고객을 리다이렉트하세요.
use Illuminate\Http\Request;
Route::get('/update-payment-method', function (Request $request) {
$user = $request->user();
return $user->subscription()->redirectToUpdatePaymentMethod();
});사용자가 정보를 업데이트하면 Paddle이 subscription_updated 웹훅을 발송하고, 애플리케이션 데이터베이스의 구독 정보가 갱신됩니다.
플랜 변경
사용자가 구독 후 다른 플랜으로 변경하고 싶을 때는 구독의 swap 메서드에 변경할 Paddle 가격 식별자를 전달합니다.
use App\Models\User;
$user = User::find(1);
$user->subscription()->swap($premium = 'pri_456');다음 청구 주기를 기다리지 않고 즉시 청구서를 발행하면서 플랜을 변경하려면 swapAndInvoice 메서드를 사용하세요.
$user = User::find(1);
$user->subscription()->swapAndInvoice($premium = 'pri_456');일할 계산 (Proration)
기본적으로 Paddle은 플랜 변경 시 일할 계산을 적용합니다. 일할 계산 없이 구독을 변경하려면 noProrate 메서드를 사용하세요.
$user->subscription('default')->noProrate()->swap($premium = 'pri_456');일할 계산을 적용하지 않으면서 즉시 청구서를 발행하려면 noProrate와 swapAndInvoice를 함께 사용합니다.
$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');플랜 변경 시 고객에게 요금을 청구하지 않으려면 doNotBill 메서드를 사용하세요.
$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');Paddle의 일할 계산 정책에 대한 자세한 내용은 Paddle 일할 계산 문서를 참고하세요.
구독 수량
구독이 "수량(quantity)"의 영향을 받는 경우도 있습니다. 예를 들어 프로젝트 관리 애플리케이션이 프로젝트당 월 1만 원을 청구한다면, incrementQuantity 및 decrementQuantity 메서드로 구독 수량을 쉽게 조정할 수 있습니다.
$user = User::find(1);
$user->subscription()->incrementQuantity();
// 현재 수량에 5를 추가...
$user->subscription()->incrementQuantity(5);
$user->subscription()->decrementQuantity();
// 현재 수량에서 5를 차감...
$user->subscription()->decrementQuantity(5);특정 수량으로 직접 설정하려면 updateQuantity 메서드를 사용하세요.
$user->subscription()->updateQuantity(10);일할 계산 없이 수량을 변경하려면 noProrate 메서드를 함께 사용합니다.
$user->subscription()->noProrate()->updateQuantity(10);다중 상품 구독에서의 수량 조정
다중 상품 구독의 경우, 수량을 변경할 가격의 ID를 증감 메서드의 두 번째 인수로 전달합니다.
$user->subscription()->incrementQuantity(1, 'price_chat');다중 상품 구독
다중 상품 구독을 사용하면 하나의 구독에 여러 청구 상품을 할당할 수 있습니다. 예를 들어 고객 서비스 헬프데스크 애플리케이션에서 기본 구독료가 월 10달러이고, 라이브 채팅 부가 상품이 월 15달러 추가 요금인 경우를 생각해볼 수 있습니다.
구독 체크아웃 세션을 생성할 때 subscribe 메서드의 첫 번째 인수로 가격 배열을 전달해 여러 상품을 지정할 수 있습니다.
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe([
'price_monthly',
'price_chat',
]);
return view('billing', ['checkout' => $checkout]);
});위 예시에서 고객의 default 구독에는 두 가격이 연결됩니다. 각 가격은 해당 청구 주기에 따라 청구됩니다. 필요하다면 연관 배열로 각 가격의 수량을 지정할 수도 있습니다.
$user = User::find(1);
$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);기존 구독에 가격을 추가하려면 구독의 swap 메서드를 사용합니다. 이때 기존 가격과 수량도 함께 포함해야 합니다.
$user = User::find(1);
$user->subscription()->swap(['price_chat', 'price_original' => 2]);위 예시는 새 가격을 추가하지만 다음 청구 주기까지 고객에게 청구되지 않습니다. 즉시 청구하려면 swapAndInvoice 메서드를 사용하세요.
$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);swap 메서드에서 제거할 가격을 생략하면 해당 가격이 구독에서 삭제됩니다.
$user->subscription()->swap(['price_original' => 2]);WARNING
구독에서 마지막 남은 가격은 제거할 수 없습니다. 가격을 모두 제거하려면 구독을 취소해야 합니다.
다중 구독
Paddle은 고객이 여러 구독을 동시에 보유할 수 있도록 지원합니다. 예를 들어 헬스클럽에서 수영 구독과 웨이트 트레이닝 구독을 별도로 운영하고, 각각 다른 가격을 적용하는 경우를 생각해볼 수 있습니다.
구독 생성 시 subscribe 메서드의 두 번째 인수로 구독 타입을 지정합니다. 타입은 해당 구독의 종류를 나타내는 임의의 문자열입니다.
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');
return view('billing', ['checkout' => $checkout]);
});이 예시에서는 고객에게 월간 수영 구독을 시작합니다. 이후 연간 구독으로 변경하고 싶다면 swimming 구독의 가격을 교체하면 됩니다.
$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');물론 구독을 완전히 취소할 수도 있습니다.
$user->subscription('swimming')->cancel();구독 일시 정지
구독을 일시 정지하려면 사용자의 구독에서 pause 메서드를 호출합니다.
$user->subscription()->pause();구독이 일시 정지되면 Cashier는 데이터베이스의 paused_at 컬럼을 자동으로 설정합니다. 이 컬럼은 paused 메서드가 true를 반환하기 시작하는 시점을 결정하는 데 사용됩니다. 예를 들어 3월 1일에 구독을 일시 정지했지만 다음 청구가 3월 5일로 예정되어 있다면, paused 메서드는 3월 5일까지 false를 반환합니다. 사용자가 결제한 기간 동안은 계속 서비스를 이용할 수 있도록 하는 것이 일반적이기 때문입니다.
기본적으로 일시 정지는 다음 청구 주기에 적용됩니다. 즉시 일시 정지하려면 pauseNow 메서드를 사용하세요.
$user->subscription()->pauseNow();pauseUntil 메서드를 사용하면 특정 시점까지만 구독을 일시 정지할 수 있습니다.
$user->subscription()->pauseUntil(now()->plus(months: 1));즉시 정지하면서 특정 시점까지만 유지하려면 pauseNowUntil 메서드를 사용하세요.
$user->subscription()->pauseNowUntil(now()->plus(months: 1));사용자가 구독을 일시 정지했지만 아직 "유예 기간" 중인지 확인하려면 onPausedGracePeriod 메서드를 사용합니다.
if ($user->subscription()->onPausedGracePeriod()) {
// ...
}일시 정지된 구독을 재개하려면 resume 메서드를 호출하세요.
$user->subscription()->resume();WARNING
구독이 일시 정지된 상태에서는 수정이 불가능합니다. 플랜 변경이나 수량 업데이트를 하려면 먼저 구독을 재개해야 합니다.
구독 취소
구독을 취소하려면 사용자의 구독에서 cancel 메서드를 호출합니다.
$user->subscription()->cancel();구독이 취소되면 Cashier는 데이터베이스의 ends_at 컬럼을 자동으로 설정합니다. 이 컬럼은 subscribed 메서드가 false를 반환하기 시작하는 시점을 결정하는 데 사용됩니다. 예를 들어 3월 5일에 만료 예정인 구독을 3월 1일에 취소하면, subscribed 메서드는 3월 5일까지 true를 반환합니다. 결제한 기간 동안은 서비스를 계속 이용할 수 있도록 하는 것이 일반적이기 때문입니다.
사용자가 구독을 취소했지만 아직 "유예 기간" 중인지 확인하려면 onGracePeriod 메서드를 사용합니다.
if ($user->subscription()->onGracePeriod()) {
// ...
}즉시 구독을 취소하려면 cancelNow 메서드를 호출하세요.
$user->subscription()->cancelNow();유예 기간 중인 구독의 취소를 중단(복구)하려면 stopCancelation 메서드를 호출합니다.
$user->subscription()->stopCancelation();WARNING
Paddle 구독은 취소 후 재개할 수 없습니다. 고객이 구독을 다시 이용하려면 새로운 구독을 생성해야 합니다.
Laravel Cashier (Paddle)
구독 체험판 (Subscription Trials)
결제 수단을 먼저 수집하는 체험판
고객에게 체험 기간을 제공하면서도 결제 수단 정보를 미리 수집하고 싶다면, Paddle 대시보드에서 해당 가격(price)에 체험 기간을 설정하면 됩니다. 그 후에는 일반적인 방식과 동일하게 체크아웃 세션을 시작합니다:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $request->user()
->subscribe('pri_monthly')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});애플리케이션이 subscription_created 이벤트를 수신하면, Cashier는 데이터베이스의 구독 레코드에 체험 기간 종료일을 기록하고, 해당 날짜가 지나기 전까지는 Paddle이 고객에게 청구하지 않도록 설정합니다.
WARNING
체험 기간이 끝나기 전에 구독을 취소하지 않으면, 체험이 만료되는 즉시 자동으로 결제가 이루어집니다. 반드시 사용자에게 체험 종료일을 미리 안내하세요.
사용자가 현재 체험 기간 중인지 확인하려면 onTrial 메서드를 사용합니다:
if ($user->onTrial()) {
// 체험 기간 중...
}이미 체험 기간이 만료되었는지 확인하려면 hasExpiredTrial 메서드를 사용합니다:
if ($user->hasExpiredTrial()) {
// 체험 기간 만료됨...
}특정 구독 타입에 대해 체험 중인지 확인하려면, onTrial 또는 hasExpiredTrial 메서드에 타입을 인수로 전달합니다:
if ($user->onTrial('default')) {
// ...
}
if ($user->hasExpiredTrial('default')) {
// ...
}결제 수단 없이 제공하는 체험판
결제 수단 정보를 미리 수집하지 않고 체험 기간을 제공하고 싶다면, 사용자에 연결된 고객 레코드의 trial_ends_at 컬럼에 원하는 체험 종료일을 직접 설정하면 됩니다. 일반적으로 사용자 회원가입 시점에 처리합니다:
use App\Models\User;
$user = User::create([
// ...
]);
$user->createAsCustomer([
'trial_ends_at' => now()->plus(days: 10)
]);Cashier는 이 방식을 "일반 체험판(generic trial)" 이라고 부릅니다. 실제 구독과 연결되지 않은 체험이기 때문입니다. trial_ends_at 값이 현재 날짜보다 미래라면, User 인스턴스의 onTrial 메서드는 true를 반환합니다:
if ($user->onTrial()) {
// 체험 기간 중...
}실제 구독을 생성할 준비가 되었다면, 평소와 동일하게 subscribe 메서드를 사용합니다:
use Illuminate\Http\Request;
Route::get('/user/subscribe', function (Request $request) {
$checkout = $request->user()
->subscribe('pri_monthly')
->returnTo(route('home'));
return view('billing', ['checkout' => $checkout]);
});사용자의 체험 종료일을 조회하려면 trialEndsAt 메서드를 사용합니다. 체험 중인 경우 Carbon 날짜 인스턴스를 반환하고, 체험 중이 아니면 null을 반환합니다. 기본 구독이 아닌 특정 구독 타입의 체험 종료일을 조회하려면 구독 타입을 인수로 전달할 수도 있습니다:
if ($user->onTrial('default')) {
$trialEndsAt = $user->trialEndsAt();
}사용자가 "일반 체험판" 상태인지, 즉 아직 실제 구독을 생성하지 않은 체험 중인지 구체적으로 확인하려면 onGenericTrial 메서드를 사용합니다:
if ($user->onGenericTrial()) {
// 일반 체험판 기간 중 (실제 구독 미생성 상태)...
}체험 기간 연장 또는 즉시 활성화
기존 구독의 체험 기간을 연장하려면 extendTrial 메서드를 호출하고 새로운 체험 종료 시점을 지정합니다:
$user->subscription()->extendTrial(now()->plus(days: 5));반대로, 체험을 즉시 종료하고 구독을 활성화하려면 구독 인스턴스의 activate 메서드를 호출합니다:
$user->subscription()->activate();Paddle 웹훅 처리
Paddle은 다양한 이벤트가 발생할 때 여러분의 애플리케이션에 웹훅(webhook)을 통해 알림을 보낼 수 있습니다. Cashier 서비스 프로바이더는 기본적으로 웹훅 컨트롤러를 가리키는 라우트를 자동으로 등록하며, 이 컨트롤러가 모든 수신 웹훅 요청을 처리합니다.
기본 동작으로는 결제 실패가 반복된 구독 취소, 구독 업데이트, 결제 수단 변경 등을 자동으로 처리합니다. 아래에서 살펴보겠지만, 이 컨트롤러를 확장하면 원하는 Paddle 웹훅 이벤트를 자유롭게 처리할 수 있습니다.
애플리케이션이 Paddle 웹훅을 정상적으로 수신하려면 Paddle 관리 패널에서 웹훅 URL을 설정해야 합니다. Cashier의 웹훅 컨트롤러는 기본적으로 /paddle/webhook 경로로 요청을 수신합니다. Paddle 관리 패널에서 활성화해야 할 웹훅 목록은 다음과 같습니다.
- Customer Updated
- Transaction Completed
- Transaction Updated
- Subscription Created
- Subscription Updated
- Subscription Paused
- Subscription Canceled
WARNING
수신되는 웹훅 요청은 반드시 Cashier가 제공하는 웹훅 서명 검증 미들웨어로 보호해야 합니다.
웹훅과 CSRF 보호
Paddle 웹훅 요청은 Laravel의 CSRF 보호를 우회해야 합니다. 따라서 bootstrap/app.php 파일에서 paddle/* 경로를 CSRF 검증 대상에서 제외해야 합니다.
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'paddle/*',
]);
})웹훅과 로컬 개발 환경
로컬 개발 환경에서 Paddle이 웹훅을 전송하려면 외부에서 접근 가능한 URL이 필요합니다. Ngrok이나 Expose 같은 터널링 서비스를 사용해 로컬 서버를 외부에 노출할 수 있습니다. Laravel Sail을 사용 중이라면 Sail의 사이트 공유 명령어를 활용하면 편리합니다.
웹훅 이벤트 핸들러 정의
Cashier는 결제 실패로 인한 구독 취소 등 일반적인 Paddle 웹훅을 자동으로 처리합니다. 추가적인 웹훅 이벤트를 직접 처리하고 싶다면, Cashier가 디스패치하는 다음 이벤트에 리스너를 등록하면 됩니다.
Laravel\Paddle\Events\WebhookReceivedLaravel\Paddle\Events\WebhookHandled
두 이벤트 모두 Paddle 웹훅의 전체 페이로드를 담고 있습니다. 예를 들어 transaction.billed 웹훅을 처리하고 싶다면, 아래와 같이 리스너를 등록합니다.
<?php
namespace App\Listeners;
use Laravel\Paddle\Events\WebhookReceived;
class PaddleEventListener
{
/**
* 수신된 Paddle 웹훅을 처리합니다.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['event_type'] === 'transaction.billed') {
// 이벤트 처리 로직...
}
}
}Cashier는 수신된 웹훅 유형에 특화된 전용 이벤트도 디스패치합니다. 이 이벤트들은 Paddle의 전체 페이로드 외에도, 웹훅 처리에 사용된 청구 대상 모델, 구독 정보, 영수증 등 관련 모델 정보도 함께 포함합니다.
Laravel\Paddle\Events\CustomerUpdatedLaravel\Paddle\Events\TransactionCompletedLaravel\Paddle\Events\TransactionUpdatedLaravel\Paddle\Events\SubscriptionCreatedLaravel\Paddle\Events\SubscriptionUpdatedLaravel\Paddle\Events\SubscriptionPausedLaravel\Paddle\Events\SubscriptionCanceled
기본 웹훅 라우트 경로를 변경하고 싶다면, .env 파일에 CASHIER_WEBHOOK 환경 변수를 정의하면 됩니다. 이 값은 웹훅 라우트의 전체 URL이어야 하며, Paddle 관리 패널에 설정한 URL과 반드시 일치해야 합니다.
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url웹훅 서명 검증
웹훅 요청의 신뢰성을 보장하려면 Paddle의 웹훅 서명을 활용하세요. Cashier는 수신된 Paddle 웹훅 요청의 유효성을 자동으로 검사하는 미들웨어를 기본으로 제공합니다.
웹훅 서명 검증을 활성화하려면 애플리케이션의 .env 파일에 PADDLE_WEBHOOK_SECRET 환경 변수를 설정해야 합니다. 웹훅 시크릿 값은 Paddle 계정 대시보드에서 확인할 수 있습니다.
단건 결제
상품 결제
고객에게 상품 구매를 진행하려면 청구 가능(billable) 모델 인스턴스의 checkout 메서드를 사용해 결제 세션을 생성합니다. checkout 메서드는 하나 또는 여러 개의 가격 ID를 인수로 받으며, 연관 배열을 사용해 구매 수량을 지정할 수도 있습니다.
use Illuminate\Http\Request;
Route::get('/buy', function (Request $request) {
$checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);
return view('buy', ['checkout' => $checkout]);
});결제 세션을 생성한 후에는 Cashier가 제공하는 paddle-button Blade 컴포넌트를 사용해 사용자가 Paddle 결제 위젯을 열고 구매를 완료할 수 있도록 합니다.
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구매하기
</x-paddle-button>결제 세션에는 customData 메서드가 있어, 트랜잭션 생성 시 원하는 커스텀 데이터를 함께 전달할 수 있습니다. 커스텀 데이터 전달에 사용할 수 있는 옵션에 대한 자세한 내용은 Paddle 공식 문서를 참고하세요.
$checkout = $user->checkout('pri_tshirt')
->customData([
'custom_option' => $value,
]);트랜잭션 환불
트랜잭션을 환불하면 구매 시 사용된 고객의 결제 수단으로 환불 금액이 반환됩니다. Paddle 구매를 환불하려면 Cashier\Paddle\Transaction 모델의 refund 메서드를 사용합니다. 이 메서드는 첫 번째 인수로 환불 사유를 받고, 이후 환불할 가격 ID와 선택적으로 환불 금액을 연관 배열로 전달할 수 있습니다. 특정 청구 가능 모델의 트랜잭션 목록은 transactions 메서드로 조회합니다.
예를 들어, pri_123과 pri_456 가격이 포함된 트랜잭션을 환불하는 경우를 생각해 보겠습니다. pri_123은 전액 환불하고, pri_456은 2달러만 부분 환불하는 상황입니다.
use App\Models\User;
$user = User::find(1);
$transaction = $user->transactions()->first();
$response = $transaction->refund('실수로 결제됨', [
'pri_123', // 이 가격은 전액 환불
'pri_456' => 200, // 이 가격은 부분 환불 (200센트)
]);위 예시는 트랜잭션의 특정 항목만 환불합니다. 트랜잭션 전체를 환불하려면 사유만 전달하면 됩니다.
$response = $transaction->refund('실수로 결제됨');환불에 대한 자세한 내용은 Paddle 환불 문서를 참고하세요.
WARNING
환불은 최종 처리 전에 항상 Paddle의 승인이 필요합니다.
트랜잭션 크레딧 적립
환불과 유사하게, 트랜잭션에 크레딧을 적립할 수도 있습니다. 크레딧 적립 시 해당 금액이 고객의 잔액으로 추가되어 이후 구매에 사용할 수 있습니다. 단, 크레딧 적립은 수동으로 수집되는(manually-collected) 트랜잭션에만 적용 가능합니다. 구독처럼 자동으로 수집되는(automatically-collected) 트랜잭션의 크레딧은 Paddle이 자체적으로 처리합니다.
$transaction = $user->transactions()->first();
// 특정 항목 전액 크레딧 적립
$response = $transaction->credit('보상 크레딧', 'pri_123');자세한 내용은 Paddle의 크레딧 관련 문서를 참고하세요.
WARNING
크레딧은 수동으로 수집된 트랜잭션에만 적용할 수 있습니다. 자동으로 수집된 트랜잭션의 크레딧은 Paddle이 직접 처리합니다.
Laravel Cashier (Paddle)
거래(Transactions)
청구 가능한 모델의 거래 내역은 transactions 프로퍼티를 통해 쉽게 조회할 수 있습니다.
use App\Models\User;
$user = User::find(1);
$transactions = $user->transactions;거래(Transaction)는 상품 구매에 대한 결제 내역이며, 각각 인보이스와 함께 저장됩니다. 완료된 거래만 애플리케이션 데이터베이스에 저장된다는 점에 유의하세요.
고객의 거래 목록을 화면에 표시할 때는 거래 인스턴스의 메서드를 활용해 결제 정보를 보여줄 수 있습니다. 예를 들어, 아래와 같이 테이블로 나열하고 인보이스를 다운로드할 수 있는 링크를 제공할 수 있습니다.
<table>
@foreach ($transactions as $transaction)
<tr>
<td>{{ $transaction->billed_at->toFormattedDateString() }}</td>
<td>{{ $transaction->total() }}</td>
<td>{{ $transaction->tax() }}</td>
<td><a href="{{ route('download-invoice', $transaction->id) }}" target="_blank">인보이스 다운로드</a></td>
</tr>
@endforeach
</table>download-invoice 라우트는 다음과 같이 정의할 수 있습니다.
use Illuminate\Http\Request;
use Laravel\Paddle\Transaction;
Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
return $transaction->redirectToInvoicePdf();
})->name('download-invoice');이전 결제 및 예정 결제
정기 구독의 이전 결제 내역과 다음 결제 예정 정보는 lastPayment 및 nextPayment 메서드로 조회할 수 있습니다.
use App\Models\User;
$user = User::find(1);
$subscription = $user->subscription();
$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();두 메서드 모두 Laravel\Paddle\Payment 인스턴스를 반환합니다. 단, 다음 두 가지 경우에는 null이 반환됩니다.
lastPayment: 웹훅을 통한 거래 동기화가 아직 이루어지지 않은 경우nextPayment: 구독이 취소되는 등 청구 주기가 종료된 경우
다음 결제 금액: {{ $nextPayment->amount() }} (결제 예정일: {{ $nextPayment->date()->format('Y/m/d') }})Laravel Cashier (Paddle)
테스트
테스트
결제 흐름이 의도한 대로 동작하는지 확인하려면, 실제 Paddle 환경에서 직접 결제 플로우를 수동으로 테스트해 보는 것이 좋습니다.
자동화 테스트(CI 환경 포함)에서는 Laravel HTTP 클라이언트의 페이크(fake) 기능을 활용하여 Paddle로 전송되는 HTTP 요청을 가로챌 수 있습니다. 이 방법은 Paddle의 실제 응답을 검증하지는 않지만, Paddle API를 실제로 호출하지 않고도 애플리케이션 로직을 테스트할 수 있는 실용적인 방법입니다.
NOTE
수동 테스트 시에는 Paddle이 제공하는 샌드박스(Sandbox) 환경을 사용하세요. 샌드박스 환경에서는 실제 결제가 이루어지지 않으므로 안전하게 결제 플로우 전체를 검증할 수 있습니다.