본문 바로가기

Dev/PHP

[PHP] Laravel - PayPal 결제 모듈 연동하기 (1) - 프런트엔드 처리

 

 

 

 

이번 포스팅에서는 Laravel 프로젝트에서 PayPal 결제 모듈 연동하는 방법에 대해 알아보겠습니다.

연동 방법은 매우 다양하게 있지만, 여기서는 화면에서 javascript를 통해 결제 모델을 생성하고 서버 측에서 capture(결제 승인) 하는 방식을 이용해보도록 하겠습니다.

 

본 포스팅에서 다루는 방법은 결제 후 결제 정보를 자체 DB에 저장하기 위한 솔루션으로 고안했습니다.

(javascript api만으로 결제 진행하는 것도 가능하고 javascript api 없이 서버 스크립트만으로 결제 진행하는 것도 가능합니다.) 

 

1. PayPal 가입

www.paypal.com/kr/webapps/mpp/home

위 링크를 타고 PayPal에 계정을 생성합니다.

 

PayPal 가입

사이트 우측 상단에 가입하기 버튼을 누르면 위와 같은 화면이 나오는데, PayPal로 결제받기에 체크하고 가입을 진행합니다.

 

중간에 설문조사, 이메일 등록, 비즈니스 유형 설정등의 과정은 생략하겠습니다.

 

 

2. PayPal 디벨로퍼 둘러보기

가입이 완료되었다면 아래의 페이팔 개발자 사이트에 접근이 가능합니다.

developer.paypal.com/developer/applications/

 

Applications - PayPal Developer

Log in to the Dashboard to create, edit, and manage PayPal apps. PayPal apps give you sandbox and live credentials that you use to make API calls, build, and test integrations. Non-U.S. developers: read our FAQ.

developer.paypal.com

 

 

디벨로퍼 페이팔 My App

 

사이트에 접속하면 위와 같이 Sandbox / Live 스위치가 보이고 하단에 애플리케이션 목록이 보입니다.

Live는 실제로 수익을 거둘때 사용하는 앱이고 Sandbox는 개발 및 테스트를 위한 앱입니다.


본 예제에서는 사전에 미리 생성되어있는 Default Appication을 활용할 것입니다.

두 개 중 하나의 Default Application을 선택합니다.

 

 

여기서 Client ID와 Secret은 이후 프로젝트에서 사용해야 하니 잘 기억해둡니다.

다음으로 좌측 사이드 매뉴에서 Sandbox항목의 Accounts를 클릭합니다.

 

 

샌드박스에서 테스트용으로 제공해주는 계정의 목록입니다.

여기서 Personal 타입인 계정을 통해 결제 테스트를 진행하게 됩니다.

각 계정의 비밀번호 및 결제 정보는 Manage accounts의 View/edit account 항목을 클릭해 확인할 수 있습니다.

기본적으로 각 계정은 달러화 기준 5000달러가 제공되며 개발하다가 잔고가 부족해지면 다시 설정할 수도 있습니다.

 

 

3. 결제 프로세스 흐름

구현할 PayPal 결제 구조

 

이번에 구현할 페이팔 API는 결제 과정이 총 3단계로 구성합니다.

 

1. createOrder: 주문 생성

2. onApprove: 결제

3. captureOrder: 거래 캡처(결제 승인)

 

이 중에서 주문 생성과 결제 부분은 블레이드(HTML)에서 즉, javascript를 통해 진행하고

3번 항목은 컨트롤러로 불러와서 처리하겠습니다.

또한 결제 과정에서 악의적인 HTML 조작을 통해 상품 가격과 같은 데이터를 변조할 수 있으므로 이를 방지하는 몇 가지 처리도 같이 해 주도록 하겠습니다.

 

 

4. php paypal-checkout-sdk 추가

paypal-checkout-sdk는 php에서 페이팔 api를 사용하기 위한 패키지입니다.

아래의 명령어로 프로젝트에 포함합니다.

 

$ composer require paypal/paypal-checkout-sdk

 

composer.json

 

composer.json 파일을 열고 paypal-checkout-sdk가 제대로 추가되었는지 확인합니다.

 

 

5. .env 파일 세팅 및 설정 파일 생성

api를 호출하기위한 키값들은 .env 파일에서 일괄 관리하는 게 좋습니다.

아래와 같이 .env 파일에 작성합니다.

 

PAYPAL_CLIENT_ID=발급받은 페이팔 client id를 입력합니다.
PAYPAL_SECRET=발급 받은 페이팔 secret을 입력합니다.
PAYPAL_MODE=sandbox
PAYPAL_CURRENCY=USD

 

PAYPAL_CURRENCY는 결제에 사용될 통화 단위를 의미합니다.

본 예제에서는 달러를 기준으로 진행합니다.

 

이제 config 디렉터리에 paypal.php라는 이름으로 파일을 하나 생성하고 아래의 내용을 작성합니다.

 

<?php return [
    'client_id' => env('PAYPAL_CLIENT_ID'),
    'secret' => env('PAYPAL_SECRET'),
    'currency' => env('PAYPAL_CURRENCY'),
    'mode' => env('PAYPAL_MODE'),
];

 

이제 이 파일은 페이팔과 관련한 작업에만 사용될 겁니다.

 

 

6. 컨트롤러 준비

다음의 명령어로 컨트롤러를 하나 생성합니다.

이 파일은 화면에 보여줄 상품 정보를 준비하고 결제 시 상품 유효성 검사, 결제 승인 등의 역할을 하게 됩니다.

$ php artisan make:controller PaypalController

 

7. 진입 라우트 정의

먼저, Paypal.Controller.php에서 최초 결제 화면에 진입할 컨트롤러 함수를 정의합니다.

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;

class PaypalController extends Controller
{
    public function index(Request $request)
    {

        $product_info  = array(
            'pd_id' => '1',
            'description' => 'paypal test product'
            'amount' => array(
                'value' => '0.01',
                'currency' => config('paypal.currency')
             )
        );
    
        return view('index', ['product_info' => $product_info]);
    }
    
}

?>

 

index 함수에서는 판매에 필요한 상품 정보를 준비합니다.

priduct_info에서 pd_id를 제외한 각 key값들은 페이팔 api에서 실제로 사용되고 있는 인자들의 이름입니다.

구현 혼선을 방지하기위해 php 변수에도 같은 이름을 붙였습니다.

 

각 변수들의 의미는 다음과 같습니다.

  • description: 상품 명

  • amount: 제품 가격과 관련한 대분류

  • value: 제품 가격

  • currency: 기준 통화

pd_id는 거래 모델 생성 시 유효성을 검증하기 위한 장치로 추가했습니다.

이후 이 pd_id의 쓰임에 대해 자세히 등장합니다.

 

다음으로 routes\web.php에서 index 함수를 동작시킬 라우트를 정의합니다.

 

<?php

...

use App\Http\Controllers\PaypalController;

Route::get('/', [PaypalController::class, 'index']);

 

resources\views에 index.blade.php 파일을 생성하고 적절히 화면을 꾸며줍니다.

 

<!DOCTYPE html>
<html>
    <head>
    	<meta charset="utf-8">
        <meta id="csrf-token content="{{csrf_token()}}>
        
        <title>Paypal Payment Test</title>
        <script src="https://www.paypal.com/sdk/js?client-id={{config('paypal.client_id')}}$currency={{config('paypal.currency')}}"></script>
        
        <style>
            .layer {
                display: flex;
                justify-content:center;
            }
            
            table {
                border 1px solid #000000;
                min-width: 30%;
            }
            
            th, td {
                border: 1px slid #000000;
            }
        </style>
        
    </head>
    <body>
        <div class="layer">
            <table class="pd_table">
                <tr>
                    <th>상품 명</th>
                    <th>가격</th>
                </tr>
                <tr>
                    <td>
                        <span>{{$product_info['description']}}</span>
                        <input type="hidden" id="pd_id" value="{{$product_info['description']}}">
                    </td>
                    <td>
                        <span>{{$product_info['amount']['value']}} / {{$product_info['amount']['currency']}}</span>
                    </td>
                </tr>
            </table>
        </div>
    </body>
    
</html>
    
    

 

여기서 주목해야할 점은 paypal api CDN입니다.

이 api를 통해 페이지상에서 거래 모델을 생성하고 결제를 준비하는 작업을 진행할 예정입니다.

필요한 쿼리 매개 변수는 client id와 currency입니다.

표현식을 통해 paypal.php의 설정 정보를 가져오도록 처리합니다.

 

이제 localhost:8000/으로 이동해 제대로 작성되었는지 확인합니다.

 

index.php

 

이렇게 가장 기초적인 준비는 마쳤습니다.

이제 이 페이지에서 페이팔 api 호출 준비를 해보도록 하겠습니다.

 

 

8. PayPal Smart Button 추가

javascript용 페이팔 체크아웃 api는 Smart Button이라는 강력한 기능을 제공합니다.

paypal.Buttons() 함수를 선언하는 것만으로도 간단하게 api와 연동할 수 있는 편리한 기능입니다.

7번 항목에서 작성한 index.blade.php에 다음과 같이 추가 작성합니다.

 

<!DOCTYPE html>
<html>

...

    <body>
        <div class="layer">
            ...
        </div>
        <div class="layer">
            <div id="paypal-button-container"></div>
        </div>
    </body>
    
    <script>
        paypal.Buttons().render('#paypal-button-container');
    </script>
</html>

 

localhost:8000/으로 이동해서 어떻게 바뀌었는지 확인해봅니다.

 

 

Smart Button 덕분에 단 2줄의 코드로 페이팔 결제 api가 적용되었습니다.

테스트 삼아 paypal 버튼을 눌러보겠습니다.

 

 

 

페이팔 결제를 위한 팝업을 출력해주고 있습니다.

이 로그인 화면에서는 실제 거래용 계정을 사용하면 안 됩니다.

페이팔 디벨로퍼에서 Accounts에서 확인했던 personal 계정을 이용해 로그인해야 합니다.

 

 

 

로그인하면 위와 같은 팝업 페이지로 이동하는데, 바로 결제를 눌러 결제하면 잠시 후 팝업이 사라지면서 거래는 완료됩니다.

그럼 거래가 정상적으로 완료되었는지 확인해봅니다.

거래 내역 확인은 아래의 페이지에서 확인 가능합니다.

https://www.sandbox.paypal.com/ 

 

Send Money, Pay Online or Set Up a Merchant Account - PayPal

PayPal is the faster, safer way to send money, make an online payment, receive money or set up a merchant account.

www.sandbox.paypal.com

 

여기서 로그인할 때는 사용한 api app의 소유자 Sandbox Account를 입력해야 합니다.

app 소유자 계정은 디벨로퍼에서 My App의 상세 팝업에서 확인 가능합니다.

 

 

PayPal My App 소유자 계정

 

 

샌드박스에 로그인하면 대시보드가 보이는데 스크롤을 맨 아래로 내려봅니다.

 

 

위 사진과 같이 방금 진행한 거래 정보가 기록되어있습니다.

api 연동이 되었음을 확인했으니 이제 이 Smart Button을 금액 설정, 데이터 유효성 검사 등 프로젝트에서 컨트롤할 수 있도록 만들어야 합니다.

 

 

9. PayPal Smart Button 핸들링

위 코드는 단순히 api 통신이 이루어지는지 확인하기 위한 함수이므로 그대로 사용하기에는 무리가 있습니다.

따라서 Button 함수 안에 과정에 따라 어떠한 행위를 할 것인지 세세하게 구분해 주어야 할 필요가 있습니다.

이를 위해 Smart Button에서는 다양한 함수를 지원하지만, 본 포스팅에서는 예제에서 사용할 함수만 소개하겠습니다.

 

함수명

설명

createOrder

화면상에서 결제 버튼을 눌렀을 때 동작합니다.

이 함수에서 결제에 필요한 상품을 세팅합니다.

이 함수에서 정해진 값을 반환하면 결제 팝업이 생성되고 페이팔 api에 의해 결제 모델이 만들어지게 됩니다.

onApprove

createOrder 함수에 의해 생성된 결제 팝업에서 결제 버튼을 누르게 되면 동작합니다.

이 함수에 진입한 시점부터 페이팔 활동 기록에 거래 상태가 기록됩니다.

여기까지 진행되었다면 페이팔 활동 상태가 '대기 중'으로 기록됩니다.

onCancel

거래 도중에 팝업을 닫거나 결제를 취소하면 거치게 되는 함수입니다.

onError

거래 진행 중 에러가 발생할 경우 이 함수를 거칩니다.

보통은 반환해야 할 값이 유효하지 않을 경우 이 함수가 동작합니다.

 

 

위 과정에서 가장 중요한 부분은 createOrder 함수 부분입니다.

여기서, 앞단에 설명한 pd_id를 기준으로 상품에 대한 디테일 정보를 서버로부터 다시 받아 오도록 구성하겠습니다.

그 이유는, 서버에서 값을 다시 받아오지 않고 HTML에 있는 정보만을 수집해서 상품 정보를 구성할 경우, 악의적인 사용자가 페이지 소스를 수정해서 제품 정보를 조작할 수 있기 때문입니다.

 

스크립트 구현에 앞서 본 포스팅에서 구축할 전반적인 결제 프로세스 흐름을 살펴보겠습니다.

(예외처리에 대한 부분은 생략한 도식입니다.)

 

PayPal 결제 프로세스

 

순서

설명

1

사용자가 화면에서 결제 버튼을 눌렀을 때 진입하며, 진입 즉시 pd_id를 verified_product 컨트롤러 함수로 전달해 유효성을 검증받는다.

2

verified_product에서 올바른 값을 반환받으면 api에 의해 로그인 팝업 출력.

3

유효성에 문제가 없을 경우 createOrder 함수로부터 결제에 필요한 데이터를 받는다.

4

전달받은 데이터를 order_capture 컨트롤러 함수로 전달하며 order_capture 컨트롤러는 캡처 API를 이용해 결제 승인 처리한다. 이때, 성공 실패 여부를 onApprove 함수에 반환한다.
(이때 결제 팝업은 닫힌다.)

5

모든 결제 처리가 완료되어 함수를 종료한다.

 

 

금전과 관련된 부분이기 때문에 다소 복잡한 프로세스를 거칩니다.

위 도식에 근거해 작성한 javascript 소스코드는 아래와 같이 확장됩니다.

 

<!DOCTYPE html>
<html>

...

    <script>
        //Custom Exception Func
        function PayPalException(name, message){
            this.name = name;
            this.message = message;
        }

        paypal.Buttons({
            createOrder: function(data, actions){ //결제 요청
                return fetch('/verified-product', {
                    method:'POST',
                    headers:{
                        "Content-Type": "application/json;charset=utf-8",
                        "X-CSRF-TOKEN": document.getElementById("csrf-token").getAttribute("content")
                    },
                    body: JSON.stringfy({
                        pd_id: document.getElementById("pd_id").value
                    })
                }).then(function (response){
                    return response.json();
                }).then(function (result){
                    if(result.valid == 'true'){
                        return actions.order.create({
                            purchase_units: [{
                                description: result.purchase_units.description,
                                amount: {
                                    value: result.purchase_units.amount.value,
                                    currency: result.purchase_units.amount.currency
                                },
                            }]
                        });
                    } else {
                        throw PayPalException(result.name, result.message);
                    }
                });
            },
            onApprove: function(data, actions){
                let res = fetch('/order-capture', {
                    method: 'POST',
                    headers: {
                        "Content-Type": "application/json;charset=utf-8",
                        "X-CSRF-TOKEN": document.getElementById("csrf-token").getAttribute("content")
                    },
                    body: JSON.stringfy({ //결제 정보를 DB에 등록하는 경우, 이곳에서 데이터를 전달합니다.
                        order_id: data.orderID
                    })
                }).then(function (response){
                    return response.json();
                }).then( function (result){
                    if(result.valid == true){
                        alert('결제가 완료되었습니다.');
                    } else {
                        throw new PayPalException(result.name, result.message);
                    }
                }).catch(function(err){
                    alert('[' + err.name + ']\n' + err.message);
                });
            },
            onError: function(err){
                alert('[' + err.name + ']\n' + err.message);
            },
            onCancel: function(data){
                console.log('거래 취소');
            }
        }).render('#paypal-button-container');
    </script>
</html>

 

꽤 분량이 되는 코드인데, 함수 하나하나 분석해보겠습니다.

 

 

[0] PayPalException

function PayPalException(name, message){
    this.name = name;
    this.message = message;
}

 

스크립트 첫 부분에 선언한 PayPalException 함수입니다.

이 함수는 사용자 정의 예외 객체로, 이후 서버와의 통신에서 예기치 못한 오류가 발생했을 때 키값 name, message를 반환하도록 설계할 예정입니다.

그때, catch문이나 onError 페이팔 함수에 예외사항을 던져주어 하는데, 그때 사용할 프레임이라고 보면 됩니다.

 

 

[1] createOrder

createOrder: function(data, actions){ //결제 요청
    return fetch('/verified-product', {
        method:'POST',
        headers:{
            "Content-Type": "application/json;charset=utf-8",
            "X-CSRF-TOKEN": document.getElementById("csrf-token").getAttribute("content")
        },
        body: JSON.stringfy({
            pd_id: document.getElementById("pd_id").value
        })
    }).then(function (response){
        return response.json();
    }).then(function (result){
        if(result.valid == 'true'){
            return actions.order.create({
                purchase_units: [{
                    description: result.purchase_units.description,
                    amount: {
                        value: result.purchase_units.amount.value,
                        currency: result.purchase_units.amount.currency
                    },
                }]
            });
        } else {
            throw PayPalException(result.name, result.message);
        }
    });
}

 

버튼을 클릭하면 먼저 /verified-product url로 POST 요청을 보냅니다.

이때, 전달할 값으로 input 태그의 hidden으로 지정된 pd_id 값을 보내며, function (result) 구문에서 서버로부터 값을 반환받는데, 이때 컨트롤러에서의 반환 값은 아래와 같이 구성될 예정입니다.

 

 

값이 유효할 경우 예시 반환 구조

result : {
    valid : true,
    purcahse_units : {
        description : 'paypal test product',
        amount : {
            value : '0.01',
            currency : 'USD'
        }
    }
}

위와 같은 경우에는 purchase_units 구조체에 대응하는 값들을 적용하고 페이팔 api 함수인 actions.order.create() 함수를 호출합니다.

 

값이 유효하지 않을 경우 반환 구조

result : {
    valid: false,
    name: 'No Data',
    message: 'This product does not exist.'
}

위와 같은 경우에는 사전에 사용자 정의로 선언한 PayPalException에 name과 message값을 적용하고 throw 문을 통해 onError 함수로 보내게 되며 결제는 중단됩니다.

 

 

[1] onApprove

onApprove: function(data, actions){
    let res = fetch('/paypal/order/capture', {
        method: 'POST',
        headers: {
            "Content-Type": "application/json;charset=utf-8",
            "X-CSRF-TOKEN": document.getElementById("csrf-token").getAttribute("content")
        },
        body: JSON.stringfy({ //결제 정보를 DB에 등록하는 경우, 이곳에서 데이터를 전달합니다.
            order_id: data.orderID
        })
    }).then(function (response){
        return response.json();
    }).then( function (result){
        if(result.valid == true){
            alert('결제가 완료되었습니다.');
        } else {
            throw new PayPalException(result.name, result.message);
        }
    }).catch(function(err){
        alert('[' + err.name + ']\n' + err.message);
    });
}

 

creatOrder의 actions.order.create() 함수의 역할이 완료되면 동작하는 함수입니다.

onApprove에 진입하게 되면 페이팔 서버에 기록되게 되고 해당 거래는 '대기 중'으로 지정됩니다.

이때, capture (결제 승인) 처리를 해주어야 비로소 결제가 완료됩니다.

페이팔로부터 반환 받은 data 변수의 구성 키값은 아래와 같습니다.

 

//createOrder 에서 반환받은 data 값의 구성

{
    billingToken: 'some value'
    facilitatorAccessToken: 'some value'
    orderID: 'some value'
    payerID: 'some value'
    paymentID: 'some value'
}

 

여기서, capture를 하기 위해서는 orderID가 반드시 있어야 합니다.

onApprove에 접근하면 orderID를 /paypal/order/capture로 보내 결제 승인을 요청해야 합니다.

 

 

/paypal/order/capture에서 capture를 마치고 반환하는 값은 아래와 같이 구성할 예정입니다.

(페이팔로부터 반환받은 값이 아닙니다. php에서 프로세스 수행 결과를 알려주는 반환 값입니다.)

 

result: {
    valid: false,
    name: 'payment success / fail',
    message: 'paymunt successfully Unkown paypal error'
}

 

반환받는 구조체는 성공할 때나 실패할 때나 동일한 구조를 갖게 할 예정입니다.

valid가 true일 경우 성공 알림을 출력하고 fail 일 때는 throw로 예외를 발생시킵니다.

 

이때, 이 함수 내에 catch문이 별도로 존재하는 이유는, 실행하는 함수가 onApprove안에서 실행되는 별도의 함수이기 때문에 onError로 예외가 반환되지 않기 때문에 별도 선언해주었습니다.

 

 

이제 프런트엔드에서 해야 할 일은 완료되었습니다.

다음 포스팅에서는 서버 사이드 구현에 대해 다루도록 하겠습니다.

 

이상입니다.