Laravel Cashier (Paddle)
번역일: 2026년 6월 25일
Laravel Cashier (Paddle)
소개
WARNING
이 문서는 Paddle Billing과 연동하는 Cashier Paddle 2.x 기준입니다. 아직 Paddle Classic을 사용 중이라면 Cashier Paddle 1.x를 참고하세요.
Laravel Cashier Paddle은 Paddle의 구독 결제 서비스를 유창하고 표현력 있는 인터페이스로 제공합니다. 반복적인 구독 결제 코드를 대부분 자동으로 처리해 줍니다. 기본적인 구독 관리 외에도 플랜 전환, 구독 수량 관리, 구독 일시정지, 취소 유예 기간 등 다양한 기능을 지원합니다.
Cashier Paddle을 본격적으로 사용하기 전에 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 migrateWARNING
Paddle의 모든 이벤트를 올바르게 처리하려면 반드시 Cashier 웹훅 처리를 설정해야 합니다.
Paddle 샌드박스
로컬 및 스테이징 개발 환경에서는 Paddle 샌드박스 계정을 등록하여 실제 결제 없이 테스트할 수 있습니다. Paddle의 테스트 카드 번호를 사용해 다양한 결제 시나리오를 시뮬레이션할 수 있습니다.
샌드박스 환경을 사용할 때는 .env 파일에 다음 환경 변수를 설정하세요:
PADDLE_SANDBOX=true개발이 완료되면 Paddle 벤더 계정을 신청하세요. 프로덕션 배포 전 Paddle이 애플리케이션의 도메인을 승인해야 합니다.
설정
결제 가능 모델
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 키를 설정합니다. 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_SANDBOX는 Paddle 샌드박스 환경 사용 시 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);
}빠른 시작
단일 상품 판매
NOTE
Paddle 체크아웃을 사용하기 전에 Paddle 대시보드에서 고정 가격의 상품을 먼저 정의해야 합니다. 그리고 Paddle 웹훅 처리도 반드시 설정하세요.
Cashier와 Paddle 체크아웃 오버레이를 활용하면 강력한 결제 통합을 손쉽게 구현할 수 있습니다.
일회성 상품 구매를 처리하려면 checkout 메서드로 체크아웃 오버레이를 시작합니다. 결제가 완료되면 지정한 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에 고객을 생성하고 애플리케이션 데이터베이스의 유저 레코드와 연결합니다.
뷰에서는 Cashier Paddle에 내장된 paddle-button Blade 컴포넌트로 체크아웃 오버레이를 띄우는 버튼을 추가합니다:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
상품 구매
</x-paddle-button>Paddle 체크아웃에 메타 데이터 전달
상품 판매 시 완료된 주문을 추적하기 위해 Cart나 Order 같은 애플리케이션 자체 모델을 사용하는 경우가 많습니다. 체크아웃 완료 후 고객이 돌아왔을 때 주문을 식별할 수 있도록 주문 ID를 체크아웃에 함께 전달할 수 있습니다:
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');고객이 체크아웃을 완료하면 주문을 "완료" 상태로 변경해야 합니다. 이를 위해 Cashier가 발행하는 TransactionCompleted 이벤트를 리스닝합니다. 서비스 프로바이더의 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\Cashier\Cashier;
use Laravel\Cashier\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 체크아웃을 사용하기 전에 Paddle 대시보드에서 고정 가격의 상품을 먼저 정의해야 합니다. 그리고 Paddle 웹훅 처리도 반드시 설정하세요.
구독 판매도 Cashier와 Paddle 체크아웃 오버레이로 간단히 구현할 수 있습니다.
예를 들어 월간(price_basic_monthly)과 연간(price_basic_yearly) 플랜을 제공하는 "Basic" 상품(pro_basic)과 Expert 플랜(pro_expert)이 있다고 가정하겠습니다.
고객이 Basic 월간 플랜의 "구독" 버튼을 클릭하면 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');뷰에서 체크아웃 오버레이를 띄울 버튼을 추가합니다:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
구독하기
</x-paddle-button>구독이 시작되면 특정 섹션은 구독 유저만 접근할 수 있도록 제한해야 합니다. 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) {
$user->subscription()->swap($price); // 예: "price_basic_yearly"
return redirect()->route('dashboard');
})->name('subscription.swap');구독 취소도 함께 제공합니다:
use Illuminate\Http\Request;
Route::put('/subscription/cancel', function (Request $request, $price) {
$user->subscription()->cancel();
return redirect()->route('dashboard');
})->name('subscription.cancel');취소된 구독은 현재 결제 주기가 끝날 때 종료됩니다.
NOTE
Cashier 웹훅 처리가 올바르게 설정되어 있으면, Paddle 대시보드에서 구독을 취소해도 Cashier가 웹훅을 받아 데이터베이스를 자동으로 동기화합니다.
체크아웃 세션
대부분의 결제 작업은 Paddle의 체크아웃 오버레이 위젯이나 인라인 체크아웃을 통해 이루어집니다.
체크아웃을 처리하기 전에 Paddle 체크아웃 설정 대시보드에서 애플리케이션의 기본 결제 링크를 정의해야 합니다.
오버레이 체크아웃
체크아웃 오버레이 위젯을 표시하기 전에 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>data-theme='light'와 같은 Paddle 지원 속성을 추가하여 위젯 스타일을 커스터마이즈할 수 있습니다:
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="light">
구독하기
</x-paddle-button>Paddle 체크아웃 위젯은 비동기로 동작합니다. 유저가 위젯 안에서 구독을 생성하면 Paddle이 애플리케이션으로 웹훅을 전송하여 구독 상태를 업데이트합니다. 따라서 웹훅 처리를 반드시 올바르게 설정해야 합니다.
WARNING
구독 상태 변경 후 웹훅 수신까지 약간의 지연이 발생할 수 있습니다. 체크아웃 완료 직후 구독 정보가 즉시 반영되지 않을 수 있음을 고려해서 애플리케이션을 설계하세요.
오버레이 체크아웃 직접 렌더링
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를 사용하여 체크아웃을 초기화합니다. paddle_button 클래스가 할당된 링크를 클릭하면 오버레이 체크아웃이 표시됩니다:
<?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>인라인 체크아웃
오버레이 방식 대신 체크아웃 위젯을 페이지 내에 직접 임베드할 수 있습니다. 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]);
});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 컴포넌트에 전달하면 됩니다.
가격 미리보기
Paddle은 통화별로 다른 가격을 설정할 수 있습니다. Cashier Paddle의 previewPrices 메서드로