Laravel Cashier (Paddle)

번역일: 2026년 6월 25일

Laravel Cashier (Paddle)

소개

WARNING

이 문서는 Paddle Billing과 연동하는 Cashier Paddle 2.x 버전을 다룹니다. 아직 Paddle Classic을 사용 중이라면 Cashier Paddle 1.x를 참고하세요.

Laravel Cashier PaddlePaddle의 구독 결제 서비스를 위한 직관적이고 표현력 있는 인터페이스를 제공합니다. 반복적으로 작성해야 하는 구독 결제 관련 코드의 대부분을 자동으로 처리해 줍니다. 기본적인 구독 관리 외에도 플랜 변경, 구독 수량 조정, 구독 일시 정지, 취소 유예 기간 등 다양한 기능을 지원합니다.

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"

이제 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 customers, subscriptions, subscription_items, transactions 테이블이 생성됩니다:

php artisan migrate

WARNING

Cashier가 모든 Paddle 이벤트를 정상적으로 처리하려면 반드시 웹훅 처리 설정을 완료해야 합니다.

Paddle 샌드박스

로컬 및 스테이징 환경에서 개발할 때는 Paddle 샌드박스 계정을 등록해 사용하세요. 샌드박스 환경에서는 실제 결제 없이 다양한 결제 시나리오를 테스트할 수 있으며, Paddle이 제공하는 테스트 카드 번호를 활용할 수 있습니다.

샌드박스 환경을 사용할 때는 .env 파일에 PADDLE_SANDBOX 값을 true로 설정합니다:

PADDLE_SANDBOX=true

개발이 완료되면 Paddle 벤더 계정을 신청하세요. 프로덕션 배포 전에 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_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=true

PADDLE_SANDBOXPaddle 샌드박스 환경 사용 시 true로, 프로덕션 배포 시에는 false로 설정합니다.

PADDLE_RETAIN_KEY는 Paddle Retain 기능을 사용할 때만 설정하면 됩니다.

Paddle JS

Paddle은 자체 JavaScript 라이브러리를 통해 결제 위젯을 초기화합니다. 레이아웃 파일의 </head> 태그 바로 앞에 @paddleJS Blade 디렉티브를 추가해 라이브러리를 로드하세요:

<head> ... @paddleJS </head>

통화 설정

청구서에 표시되는 금액의 형식을 지정하는 로케일을 설정할 수 있습니다. Cashier는 내부적으로 PHP의 NumberFormatter 클래스를 사용합니다. 한국어 환경이라면 ko_KR을 사용할 수 있습니다:

CASHIER_CURRENCY_LOCALE=ko_KR

WARNING

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

기본 모델 재정의

Cashier가 내부적으로 사용하는 모델을 자유롭게 확장할 수 있습니다. 커스텀 모델을 만들고 Cashier의 모델을 상속하면 됩니다:

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

커스텀 모델을 정의했다면 App\Providers\AppServiceProviderboot 메서드에서 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 대시보드에서 고정 가격이 지정된 상품을 먼저 등록하고, 웹훅 처리 설정도 완료해야 합니다.

Cashier와 Paddle Checkout Overlay를 사용하면 강력한 결제 기능을 간단하게 구현할 수 있습니다.

일회성 상품을 판매할 때는 checkout 메서드로 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에서 고객 레코드가 없으면 자동으로 생성하고, 애플리케이션 데이터베이스의 사용자와 연결합니다.

뷰에서는 Cashier Paddle에 포함된 paddle-button Blade 컴포넌트를 사용해 결제 버튼을 표시합니다:

<x-paddle-button :checkout="$checkout" class="px-8 py-4"> 상품 구매 </x-paddle-button>

결제 시 메타데이터 전달하기

상품을 판매할 때는 CartOrder 모델로 주문을 추적하는 경우가 많습니다. 결제 완료 후 어떤 주문인지 식별할 수 있도록 customData 메서드로 커스텀 데이터를 전달할 수 있습니다:

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

결제가 완료되면 Paddle이 웹훅을 통해 알려줍니다. Cashier가 발행하는 TransactionCompleted 이벤트를 리스닝하여 주문 상태를 업데이트할 수 있습니다. AppServiceProviderboot 메서드에 이벤트 리스너를 등록합니다:

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 결제를 사용하기 전에 Paddle 대시보드에서 고정 가격이 지정된 상품을 먼저 등록하고, 웹훅 처리 설정도 완료해야 합니다.

예를 들어 월간 플랜(price_basic_monthly)과 연간 플랜(price_basic_yearly)을 갖는 "Basic" 구독 서비스(pro_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');

뷰에서 paddle-button 컴포넌트로 구독 버튼을 표시합니다:

<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에서 수신한 웹훅을 기반으로 데이터베이스를 자동으로 동기화합니다. 예를 들어 Paddle 대시보드에서 구독을 취소하면, Cashier가 해당 웹훅을 받아 데이터베이스에 취소 상태를 자동으로 반영합니다.

결제 세션

고객에게 청구하는 대부분의 작업은 Paddle의 Checkout Overlay 위젯이나 인라인 결제를 통해 이루어집니다.

결제를 처리하기 전에 Paddle 결제 설정 대시보드에서 애플리케이션의 기본 결제 링크를 먼저 설정해야 합니다.

오버레이 결제

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 컴포넌트에 결제 세션을 전달합니다. 버튼을 클릭하면 Paddle 결제 위젯이 표시됩니다:

<x-paddle-button :checkout="$checkout" class="px-8 py-4"> 구독 시작 </x-paddle-button>

data-theme='light'처럼 Paddle이 지원하는 속성을 컴포넌트에 추가해 위젯을 커스터마이징할 수 있습니다:

<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light"> 구독 시작 </x-paddle-button>

Paddle 결제 위젯은 비동기로 동작합니다. 고객이 위젯에서 구독을 완료하면 Paddle이 웹훅을 통해 애플리케이션에 알려주므로, 구독 상태를 정확히 관리하려면 웹훅 처리 설정이 반드시 필요합니다.

WARNING

구독 상태 변경 후 웹훅이 수신되기까지 약간의 지연이 있을 수 있습니다. 결제 완료 직후 구독 정보가 즉시 반영되지 않을 수 있다는 점을 애플리케이션 설계에서 고려하세요.

오버레이 결제 직접 렌더링하기

Blade 컴포넌트 없이 직접 오버레이 결제를 렌더링할 수도 있습니다. 이전 예제와 동일하게 결제 세션을 생성한 뒤, 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 컴포넌트 없이 직접 인라인 결제를 렌더링하려면, 결제 세션을 생성한 뒤 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 컴포넌트에 전달해 사용합니다.

가격 미리보기

Paddle은 국가별로 다른 가격을 설정할 수 있습니다. Cashier Paddle의 previewPrices 메서드로 가격 ID를 전달하면 해당 가격 정보를 조회할 수 있습니다:

use Laravel\Paddle\Cashier; $prices = Cashier::previewPrices(['pri_123', 'pri_456']);

기본적으로 요청의 IP 주소를 기반으로 통화가 결정되지만, 특정 국가를 직접 지정할 수도 있습니다:

use Laravel\Paddle\Cashier; $prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [ 'country_code' => 'KR', 'postal_code' => '06000', ]]);

조회한 가격을 원하는 방식으로 표시합니다:

<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 문서](https://developer.paddle.com/api-reference/pricing

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

번역일: 2026년 6월 25일