web/snulion

카카오페이를 이용한 간편결제에 대해 araboza

민사민서 2024. 10. 5. 17:01

주요 구성 요소

- 구매자 (상품 선택하고 결제 진행하는 사용자)

- client (프론트엔드 (React), 구매자가 상호작용하는 웹사이트나 앱. 결제 요청을 받고 결제창을 호출하는 인터페이스 역할)

- Server (백엔드 (Django), PG사에게 결제 승인을 요청하고, 그에 대한 응답을 받는 상점의 서버)

- 페이먼트(PG사) (실제 결제 처리와 승인, 결제 상태 등을 관리하는 결제 대행사)

 

PG(Payment Gateway)사란?

PG사는 이커머스 결제 대행 서비스를 진행하는 중개업체

Payment Gateway의 준말로, `‘결제를 위한 관문’`이라고 이해

PG사는 카드 결제, 간편결제, 계좌이체, 가상계화(무통장입금) 등 다양한 결제 방식을 사용할 수 있게 연결

 

결제 flow

1. 결제창 호출 (결제 준비)

구매자는 상품의 ‘결제 버튼’을 클릭하여 결제를 시작

Client가 Server에 결제 요청을 보내게 되고, Server는 이 결제 요청에 대한 응답으로 카카오페이의 api를 호출하여 결제 준비를 요청

이 요청이 PG사로 전달되어, PG사가 Client에게 결제창 URL을 응답으로 반환

Client가 해당 URL을 이용해 결제창을 띄움

구매자 → Client → Server → PG사 → Client → 구매자가 결제창 확인

 

2. 카드 정보 전달 및 인증 (카드사 인증 요청)

구매자가 해당 결제창에서 결제 수단을 선택

PG사는 고객 정보를 카드사로 전달하고, 카드사는 내부적으로 구매자가 실제 카드사의 고객이 맞는지 확인

구매자 → PG사 → 카드사

 

3. 서버에서 결제 승인 요청 날리기 (결제 승인)

카드사가 고객 인증을 성공적으로 하면, PG사가 pg token(구매자 인증에 대한 증표)을 Client한테 발급

Client가 Server한테 pg token을 보내주면 그때 Server가 해당 구매자가 인증된 고객인 걸 알게 됨

결제 승인 절차를 위해 Server가 PG사로 다시 결제 승인 요청을 날림

PG사는 카드사와의 통신을 통해 `실제 결제`를 처리함

Server가 보낸 결제 승인 요청에 대한 응답으로  Server에 결제 완료 데이터를 전달

 

PG사→ Client → Server → PG사 → Server

 

카카오페이 실습하기

1. 카카오페이 developers 에서 (https://developers.kakaopay.com) 어플리케이션 등록

 

2. 환경변수 추가하기

django에다가는 아래와 같이

KAKAO_PAY_KEY='{아까 발급받은 secret(dev) 키}'

 

react에다가도 아래와 같이

REACT_APP_KAKAO_PAY_CID='TC0ONETIME'

* CID는 가맹점 코드, 카카오페이와 제휴를 통해 발급받은 가맹점 코드, 사업자 등록이 되어있지 않은 테스트 CID이긴 함

https://partner.kakaopay.com/partner/offline/application-information

 

카카오페이 파트너센터

카카오페이 가맹점을 신청하는 그때부터 가맹점이 되어 장사하는 모든 순간까지. 사장님은 덜하세요 카카오페이가 더할게요.

partner.kakaopay.com

 

카카오페이 요청 본문

- cid : 가맹점 번호

- partner_order_id / partner_user_id : 가맹점 주문번호, 회원 ID, 결제 내역 바탕으로 추후 cs나 영수증 처리 등을 할 때 쓰이는 값 (unique 하게 서비스에서 자체적으로 관리해야 함, 카카오페이에서 위 값에 대한 unique 검사를 따로 하진 않더라)

- item_name, quantity, total_amount : 구매하는 상품명, 상품 수량, 결제 총액 값

- tax_free_amount: 상품 비과세 금액

- approval_url, cancel_url, fail_url : 결제 준비 요청이 성공 / 취소 / 실패했을 때 보여질 화면에 대한 주소 (리다이렉트 위치), 즉 react에서 각각 url 값에 매칭되는 router 설정을 미리 해두어야 함

 

응답 본문

 

- tid(transcation id) : 카카오페이 측에서 발급해주는 결제 고유 번호, 결제 요청 한 건당 하나씩 매핑되는 정보 추후 결제 승인 단계에서 활용되기 때문에 서버에서 자체적으로 보유하고 있어야 함

- next_redirect_pc_url : pc web 기준으로 프론트엔드 화면을 어디로 redirect 할지 (mobile, android, ios 등등도 지정 가능)

 

Payment라는 모델을 django Server에 새롭게 생성

# Create your models here.
class Payment(models.Model):
    tid=models.CharField(max_length=100) # 결제 준비 완료 후 다음단계(결제 승인 api) 요청시 필요한 값
    partner_order_id=models.CharField(max_length=100) # 결제 준비 완료 후 다음단계(결제 승인 api) 요청시 필요한 값
    partner_user_id=models.CharField(max_length=100) # 결제 준비 완료 후 다음단계(결제 승인 api) 요청시 필요한 값
    point=models.CharField(max_length=100)
    price=models.IntegerField(default=0)
    pay_status=models.CharField(max_length=100, default='ready') # 결제 준비까지 된거냐, 결제 승인까지 된거냐
    user=models.ForeignKey(User, on_delete=models.CASCADE, related_name='pay_buyer', null=True)

 

views.py 도 작성, api/payment/ 엔드포인트 작성

class PayReadyView(APIView):
    def post(self, request):
        pay_data = request.data

        user = request.user
        if not user.is_authenticated: # 인증받은 유저인지 확인
            return Response({"detail": "please signin."}, status=status.HTTP_401_UNAUTHORIZED)
        
        pay_data = json.dumps(pay_data) # json 형태로 변환

        response = requests.post(payready_url, headers=pay_header, data=pay_data) # 카카오페이 서버에 요청
        response_data = response.json()

        if response.status_code == 200:
            Payment.objects.create( # 결제 준비 완료 후, DB애 저장
                tid=response_data['tid'],
                partner_order_id=request.data['partner_order_id'],
                partner_user_id=request.data['partner_user_id'],
                point=request.data['item_name'],
                price=request.data['total_amount'],
                user=user
            )

        return Response(response.json(), status=response.status_code)

이건 결제 요청 관련 api

class PayApproveView(APIView):
    def post(self, request):
        user = request.user
        if not user.is_authenticated: # 인증받은 유저인지 확인
            return Response({"detail": "please signin."}, status=status.HTTP_401_UNAUTHORIZED)

        # pg_token, tid, cid 값 가져오기
        pg_token = request.data['pg_token']
        tid = request.data['tid']
        cid = request.data['cid']
        
        pay_hist = Payment.objects.get(tid=tid) # 이전에 저장해주었던 결제 요청(준비) 이력을 불러옴
        pay_data = {
            'cid': cid,
            'tid': tid,
            'partner_order_id': pay_hist.partner_order_id,
            'partner_user_id': pay_hist.partner_user_id,
            'pg_token': pg_token
        }
        
        # 카카오페이 서버에 결제 승인 요청
        pay_data = json.dumps(pay_data)
        response = requests.post(payapprove_url, headers=pay_header, data=pay_data)

        if response.status_code == 200:
            pay_hist.pay_status = 'approved' # pay history 의 상태를 approved 로 변경
            userprofile = UserProfile.objects.get(user=user)
            userprofile.remaining_points+= int(pay_hist.point) # 유저의 포인트를 증가
            pay_hist.save()
            userprofile.save()

        return Response(response.json(), status=response.status_code)

이건 결제 승인 관련 api

 

프론트단에서 주의해야 할 사항

- tid 값을 localStorage에 저장: 결제 승인 api에서 백엔드에게 인증을 위해 tid, pg_token 값을 보내주어야 하는데 next_redirect_pc_url 로 리다이렉션 해버리면 tid 값 날아가기 때문에 백업해둠

 

- next_redirect_pc_url 은 approvePage의 url에다가 pg_token을 query string으로 추가해서 전달해줌

cf) 결제 요청 → 승인 직전에 next_redirect_pc_url 페이지로 리다이렉트 → 결제 승인 → 성공

 

export const paymentApprove = async (tid, pg_token) => {
  try {
      const res = await instanceWithToken.post("/payment/approve/", {
          "pg_token": pg_token,
          "tid": tid,
          "cid": process.env.REACT_APP_KAKAO_PAY_CID
      });
      return res;
  } catch (e) {
      console.error(e);
  }
} // 이런 식으로 함수를 정의해놓고

useEffect(() => {
    const url = new URL(window.location.href);
    const pg_token = url.searchParams.get("pg_token");

        if (pg_token) {
              paymentApprove(localStorage.getItem('tid'), pg_token).then(
                    async (res) => {
                          console.log('res', res);
                          localStorage.removeItem('tid');

                          // 사용자 포인트 업데이트 필요
                          await fetchUserInfo().then((res) => {
                                console.log('userinfo', res);
                                dispatch(
                                      setUserProfile({
                                            ...data,
                                            remaining_points: res.remaining_points,
                                      }),
                                );
                          });

                          window.location.href = '/';
                    },
              );
        }
    }, []);
  // useEffect문 내부에서 pg_token 부분을 url에서 가져오고, tid는 로컬 스토리지에서 가져오고

 

'web > snulion' 카테고리의 다른 글

상태관리에 대해 araboza (2)  (1) 2024.10.05
상태관리에 대해 araboza  (1) 2024.09.25
OAuth 2.0에 대해 araboza  (2) 2024.09.25