본문 바로가기
React

[React/Django] 포트원으로 결제 기능 구현하기

by kingyejin 2024. 10. 11.

먼저 결제 기능이 어떻게 구현되는 지 부터 보면 아래와 같습니다!

어디에서 기능이 어떻게 이루어지고 왜 이렇게 구조가 짜여지는지 이해하면 구현하기가 쉽습니다

이제 프론트는 React로, 백앤드는 Django로 하여 결제 기능을 구현해보겠습니다~!


1. 포트원 회원가입하기

https://portone.io/korea/ko

 

포트원 | 온라인 비즈니스 성장을 돕는 기업

포트원이 제공하는 단 한 줄의 코드로 세상의 모든 결제를 손쉽게 연동해보세요. PG사 통합결제 연동, 해외결제, 파트너 정산 관리, 결제 애널리틱스, 수수료 혜택까지, 포트원의 맞춤 컨설팅을

portone.io


2. 결제 연동하기

메인화면에서 시작하기> 관리자콘솔 > 좌측 메뉴바 > '결제 연동'을 클릭!

그리고 식별코드 ・ API Keys을 확인해줍니다.

 

*Key값은 추후에 yml 파일에 설정해주어 iamport와 server의 요청을 연동해주어 식별하는 역할을 합니다.

 

그리고 아래와 같이 전자 결제 테스트용으로 신청해줍니다.

저는 카카오페이 간편 결제 모듈을 사용할 거라서 아래와 같이 신청해줍니다.


3. 프론트 설정 [React]

 

1. 결제 버튼 뒤의 HTML에 먼저 PortOne SDK를 추가한 다음, 자바스크립트 코드를 추가

PortOne SDK가 설치되면 IMP 라는 전역 변수가 생성되고, 이 변수를 통해 포트원과 상호작용할 수 있습니다. 

return (  

      <Button onClick={onClickPay}>결제요청</Button>
      <script src="https://cdn.iamport.kr/v1/iamport.js"></script>
      <script src="/main.js"></script>

);

 

 

2. 나의 결제 연동 정보에 "고객사 식별코드"를 복사하여 IMP.init("")에 넣어 초기화하기

 

"request_pay" 함수: 구매자에 대한 정보를 전달하는 함수로, 결제 요청에 대한 모든 옵션이 포함된 구성 객체를 받음. 해당 옵션들은 포트원 웹사이트에 있음

 

아래에서는 pg, pay_method, amount, name, merchant_uid 등의 가장 중요하고 필수적인 옵션과 그 외의 옵션들을 사용함.

 

"function(response)" 함수: 구매자에 대한 정보를 전달받는 함수로, 이 함수는 결제가 성공적으로 완료된 경우 혹은 결제할 수 없는 경우 또는 결제 창을 닫을 때 호출됨.

- ChargeApi.js 전체 코드-

chargeInstance는 content-type이 백앤드와 맞게 application/json이어야 합니다.

그리고 헤더에 토큰이 들어가있어야 하기 때문에 interceptors 설정이 필요합니다.

그리고 verifyPayment 함수는 백앤드 API로 필요한 파라미터 user_email과 imp_uid를 보내주는 프론트 API입니다. 

import axios from "axios";
import Cookies from "js-cookie";

//chargeInstance 설정
export const chargeInstance = axios.create({
  baseURL: testURL,
  headers: {
    "Content-Type": "application/json",
    withCredentials: true,
  },
});

chargeInstance.interceptors.request.use(
  (config) => {
    // 쿠키에서 access token 꺼내기
    const token = Cookies.get("accTK");
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

//백앤드 API
export const verifyPayment = async (user_email, imp_uid) => {
  const response = await chargeInstance.post("payment/verify", {
    user_email: user_email,
    imp_uid: imp_uid,
  });
  console.log(response);
  return response.data;
};

- charge.js 전체 코드-

*다음 코드는 본인이 짜둔 react 코드에 입혀서 적절하게 사용하면 됩니다!

import { verifyPayment } from "../../api/ChargeApi";

const Charge = () => {  

  useEffect(() => {
    const script = document.createElement('script');
    script.src = "https://cdn.iamport.kr/v1/iamport.js";
    script.onload = () => {
      if (window.IMP) {
        window.IMP.init('imp04037430');
      } else {
        console.error('IMP 라이브러리 로드 실패');
      }
    };
    document.body.appendChild(script);
  }, []);
  
  const onClickPay = () => {
    if (window.IMP) {
      window.IMP.request_pay({
        pg: "kakaopay",
        pay_method: "card",
        merchant_uid: "ORD20231030-000002",
        name:"크레딧 충전",
        amount: "60000",
        buyer_email: "----@naver.com",
        buyer_name: "김예진",
        buyer_tel: "010-1234-5678",
        buyer_addr: "경기도",
        buyer_postcode: "123-456",

      }, function(response){
       console.log("response:", response);
          const { status, err_msg, imp_uid, name } = response;
          if (err_msg) {
            alert(err_msg);
          }
       if (status === "paid") {
            //백으로 헤더에 토큰이랑 바디에 imp_uid 보내야 함
            alert(
              `Message: ${user_email}번 회원 ${name} 결제 시도, imp_uid: ${imp_uid}`
            );
            verifyPayment(user_email, imp_uid);
        }});};
      }
      
      
return (  
<Button onClick={onClickPay}>결제요청</Button>
  <script src="https://cdn.iamport.kr/v1/iamport.js"></script>
  <script src="/main.js"></script>
    );  
};  

export default Charge;

구현된 프론트 화면

 

해당 QR로 테스트 결제를 완료하면 아래와 같은 Response가 온다.

status가 결제 전 "AUTHENTICATED" 에서 결제 완료 후, "paid"로 된 것을 볼 수 있다.


3. 백앤드 설정 [Django]

*백앤드 설정이 필요한 궁극적인 목적은 실제 품목의 데이터 가격과 프론트에서 설정한 데이터 가격이 일치하는 지 확인하기 위함!

백앤드 설정을 하지 않아도 결제기능은 동작하지만,

실제 품목은 10,00원인데 사용자가 가격을 0원으로 설정하여 무한대로 사는 행위를 막기 위해선 반드시 필요함!!!

 

1. view.py에 프론트에서 사용할 verifyPayment 함수에 대한 API 추가하기 

from dotenv import load_dotenv
import os
from rest_framework.decorators import api_view
from banya_utils.charge import check_empty_values, throw_error, check_subscription_validity


@api_view(['POST'])
def verify_payment(request):
    try:
        user_email = request.data.get('user_email')
        imp_uid = request.data.get('imp_uid')
        print(f"user_email: {user_email}, imp_uid: {imp_uid}")  # 확인을 위해 출력
        
        # 입력값 검증
        check_empty_values(imp_uid, user_email)

        # 상점 접근 토큰 발급
        token_response = requests.post(
            "https://api.iamport.kr/users/getToken",
            json={
                "imp_key": os.getenv("PORTONE_KEY"),
                "imp_secret": os.getenv("PORTONE_SECRET")
            },
            headers={"Content-Type": "application/json"}
        )
        token_data = token_response.json()
        access_token = token_data.get('response', {}).get('access_token')
        print(f"access_token: {access_token}")
        if not access_token:
            throw_error(status.HTTP_400_BAD_REQUEST, "FAIL_TO_GET_TOKEN")

        # 결제 정보 요청
        payment_response = requests.get(
            f"https://api.iamport.kr/payments/{imp_uid}",
            headers={"Authorization": access_token}
        )
        payment_data = payment_response.json()
        payment_info = payment_data.get('response', {})

        # 결제 정보 확인
        amount = payment_info.get('amount')
        name = payment_info.get('name')
        print(f"amount: {amount}, name: {name}")

        if not amount or not name:
            throw_error(status.HTTP_400_BAD_REQUEST, "FAIL_TO_GET_PAYMENT_RESPONSE")

        # 유효한 구독 정보 확인
        is_subscription_valid = check_subscription_validity(name, amount)

        if is_subscription_valid:
            return Response({
                "message": "PAYMENT_SUCCESS",
                "isSubscribed": True
            }, status=status.HTTP_200_OK)
        else:
            return Response({
                "message": "PAYMENT_FAILED"
            }, status=status.HTTP_400_BAD_REQUEST)

    except Exception as error:
        return Response({
            "message": str(error)
        }, status=status.HTTP_400_BAD_REQUEST)

 

[벡엔드 동작 순서에 대한 간단한 설명]

1. 포트원 api (https://api.iamport.kr/users/getToken)에 imp_key와 imp_secret를 바디로 보내서 access_token을 받아옴.

 

2. 프론트로 받은 파라미터 imp_uid를 이용해 포트원 api (https://api.iamport.kr/payments/imp_uid)에 이전에 받은 access_token을 헤더에 넣어서 response를 받아옴. 

 

해당 요청에 대한 response는 아래와 같을 것입니다.

그럼 해당 response 중 필요한 amount, name과 같은 필요한 정보들을 들고와

아래에서 설정해줄 utils.py의 check_subscription_validity 함수와 같은 곳에 사용하여 타당성을 검증할 수 있습니다.


2. utils.py에 view.py에서 사용하는 함수 추가하기

해당 check_subscription_validity 함수 내의 valid_name과 valid_amount는 개발자가 세팅해주면 된다.

*이 외에도 user_email 이나 다른 정보들의 타당성 검사도 세팅 추가해주면 가능

from rest_framework.exceptions import APIException

def check_empty_values(*args):
    for value in args:
        if not value:
            raise APIException("One or more required fields are empty.")


def throw_error(status_code, message):
    raise APIException({"status_code": status_code, "message": message})


def check_subscription_validity(name, amount):
    # 유효한 구독인지 확인하는 로직 작성
    # 이 함수는 DB 또는 외부 시스템과의 통신이 필요할 수 있음
    # 예시:

    valid_name = "반야 트레이너 크레딧 충전"
    valid_amount = 14554

    return name == valid_name and amount == valid_amount

3.  .env 파일에 view.py에서 사용하는 포트원 KEY와 SECRET 코드 추가하기

*백앤드에선 해당 키를 이용해 포트원에서 access_token을 받아옴.

 

포트원 결제 연동 > 연동 정보 > 식별코드 & API Keys에서 확인 가능

PORTONE_KEY = 고객사 식별코드
PORTONE_SECRET = REST API secret


해당 백앤드 설정을 모두 완료 후, 테스트 결제를 하면 아래와 같은 Response가 온다.

백앤드 터미널 로그 print

 

만약 utils.py의 check_subscription_validity(name, amount): 함수에서

설정한 기본 판매 품목 데이터 가격(valid_amount)과 사용자가 요청한 결제 가격(amount)이 동일할 때, 

결제를 시행하면 PAYMENT_SUCCESS가 오고 동일하지 않으면 PAYMENT_FAILED가 온다.

개발자 툴 로그 console.log


아래의 노마드코더 유튜브와 블로그가 많은 도움이 되었습니다!

https://www.youtube.com/watch?v=JsiTJlLitMI&t=4s

 

https://hey-story.tistory.com/12

 

포트원으로 간편결제 구현하고 postman으로 테스트하기 feat. node.js

소셜 로그인 다음으로 꼭 해보고 싶었던 것이 있다면 간편결제였다. 진짜 결제까지는 아니더라도 qr 코드를 발급하고 결제 정보가 db에 저장되는 로직을 구현해보고 싶었다. 그래서 소셜 로그인

hey-story.tistory.com