주문 기능 개발과 토스 페이먼츠 도입

Habyte·2023년 7월 17일
2

쇼핑몰 프로젝트

목록 보기
2/2

쇼핑몰의 가장 중요한 기능이 무엇일까? 당연하다. 물건을 고르고 살 수 있어야 한다.

나는 웹에서의 주문과 결제에 대해서 아무것도 아는 것이 없었다.
실제 고객들의 돈이 오고 가는 중요한 기능이라 오류가 발생하면 어떡하나 마냥 두렵기도 했다. 그래도 일단 해봐야 알지 않겠는가?
이번 프로젝트에서 어떤 과정을 통해 어떻게 해당 기능을 공부하고 구현했는지 정리해 보았다.

PG사 선택하기

웹에서 발생하는 결제의 대부분은 카드결제와 간편결제다. 그렇다면 카드, 간편 결제를 하기위해 수많은 카드사와 계약을 해야 할까?

이럴때 우리를 도와주는 것이 전자결제 지급대행사PG사 라고도 부른다. PG사는 여러 카드사와 계약을 채결하고 개인 사업자에게 수수료를 받으며 결제 및 지불을 대행하는 회사로 카드결제, 간편결제, 계좌이체, 가상계좌(무통장입금) 등 다양한 결제 방식을 온라인에서 편하게 연동할 수 있게 해주는 서비스를 제공해 준다.
PG사와 내 쇼핑몰

일단은 국내 결제만 고려하기로 결정했고 토스페이먼츠, KG이니시스, 나이스 페이먼츠등 잘 알려진 국내 PG사들 중 토스페이먼츠를 이용하기로 했다. 기본으로 제공하는 결제 위젯이 마음에 가장 들었고 제공하는 개발자 문서와 예시가 정말 잘 정리 되어 있어서 정해진 결정 이였다.


프로젝트에 적용하기

결제 연동에 필요한 과정은 토스페이먼츠 개발자 페이지에 너무도 자세히 설명해주고 있어서 큰 어려움 없이 적용할 수 있었다. 참고했던 자료들은 아래에 첨부했다.

토스페이먼츠 개발 가이드
토스페이먼츠 결제위젯 샘플 깃헙

토스 페이먼츠의 결제 과정

토스의 결제 인증과 승인 과정

시작하기 이전에 먼저 간단하게 결제 과정이 어떻게 흘러가는지 알아보자.

토스에 결제를 요청하면 인증승인 과정이 차례대로 진행된다.
여기서 인증은 결제 정보가 올바른지 검증하는 과정이고 승인 은 인증에 성공한 결제를 최종 승인하는 과정이다. 승인 요청에 성공하면 결제 요청 과정이 끝나게 된다.

또한, 이 인증과 승인 사이에 리다이렉트 과정이 있다.
토스에서 인증 결과를 URL의 쿼리 파라미터에 담아 리다이렉트하면, 해당 정보를 받아 결제 승인을 처리하는 개념이다.

결제 요청 과정에 대한 더 자세한 설명은 이 블로그 포스트에서 확인할 수 있다.

API 키 설정하기

제일 먼저 할일은 API 키를 받아와서 환경 변수에 저장하는 것이다.
토스페이먼츠 에서는 회원가입 하기 전에도 테스트 키를 제공받아 볼 수 있고 가입 이후엔 나만의 테스트 키를 지급 받는다.

// 문서 테스트 키
// 토스페이먼츠 회원가입하기 전이라면 아래 키로 결제위젯을 연동하세요.
const clientKey = 'test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq'
const secretKey = 'test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R'

// 내 테스트 키
// 토스페이먼츠에 회원가입을 완료했다면 아래 키로 결제위젯을 연동하세요.
// 로그인하면 문서의 API 키가 모두 내 테스트 키로 변경됩니다.
const clientKey = <개인 클라이언트키>
const secretKey = <개인 시크릿키>

이 테스트 키를 이용하면 실제 결제는 진행되지 않지만 모든 결제 로직을 테스트 해보고 토스 개발자센터에서 결제 내역을 확인해 볼 수 있다.

결제 위젯 그리기

개발자 홈페이지엔 기본적으로 결제 위젯을 그리는 샘플 자바스크립트 코드가 제공되어 있었고 나는 토스에서 제공해주는 리액트 샘플 코드를 참고해서 프로젝트에 적용했다. ( 최근에 다시 보니 nextjs 코드도 업데이트 되어있다 )

제일 먼저 토스페이먼츠 sdk 를 추가해준다.

$ yarn add @tosspayments/payment-widget-sdk

그 이후 결제창을 띄우고 싶은 주문 페이지로 가서 결제 위젯을 띄워준다.

위젯 인스턴스를 생성하기 위해서 clientKeycustomerKey 가 필요하다.
clientKey 에는 앞서 저장한 환경 변수값을 넣었는데 customerKey 이 녀석은 뭐하는 녀석인가. 역시 토스 개발자 문서에 자세히 나와 있다.

customerKey 는 상점에서 고객을 구분하기 위해 사용되며 다른 사용자가 이 값을 알게 되면 악의적으로 사용가능 함으로 충분히 무작위적인 고유한 값을 사용해야 한다. 또, 고객에게 할당된 키는 변경없이 항상 같은 값이어야 한다.
해당 키는 재구매율, 이탈률, 구매전환율 등을 측정하거나 결제창에서 이탈한 고객을 다시 결제로 유도하는데 이용된다.

따라서 나는 customerKey 에는 DB에 저장되어 있는 id 값을 사용했다.
만약, 비회원 주문일 경우에는 sdk 에서 제공해주는 ANONYMOUS 값으로 지정해주면 된다.
그리고 결제할 금액의 양까지 지정해주면 일단 위젯 뛰우기는 성공!

// ../order.tsx

import { useEffect, useRef } from "react";
import {
  loadPaymentWidget,
  PaymentWidgetInstance,
  ANONYMOUS,
} from "@tosspayments/payment-widget-sdk";
import { nanoid } from "nanoid";

export default function Order() {
  const { data } = useSession();
  const paymentWidgetRef = useRef<PaymentWidgetInstance | null>(null);
  const paymentMethodsWidgetRef = useRef<ReturnType<
    PaymentWidgetInstance["renderPaymentMethods"]
  > | null>(null);
  
  const clientKey = process.env.NEXT_PUBLIC_PAYMENTS_CLIENT!;
  const customerKey = data ? data.user?.id! : ANONYMOUS;
  const price = 15000;

  useEffect(() => {
    // 결제창 로드
    (async () => {
      const paymentWidget = await loadPaymentWidget(clientKey, customerKey);

      const paymentMethodsWidget = paymentWidget.renderPaymentMethods(
       	"#payment-widget",
        price,
      );
      paymentWidget.renderAgreement("#agreement");

      paymentWidgetRef.current = paymentWidget;
      paymentMethodsWidgetRef.current = paymentMethodsWidget;
    })();
  }, []);

  return (
    <main>
      <div>
        <div id="payment-widget" />
        <div id="agreement" />
      </div>
    </main>
  );
}

결제 요청하기

그 다음 결제하기 버튼을 클릭했을 때 호출할 함수를 만들어 준다.
버튼을 클릭했을 때 앞서 만든 위젯 인스턴스의 requestPayment 함수를 호출하면 된다. 해당 함수를 호출할 때 다음과 같은 값들을 지정해 줘야한다: orderId, orderName, customerName, successUrl, failUrl .

주문번호 생성하기

이 중에서 orderId 는 나중에 주문 정보 트랙킹을 위해 사용되고 CS 처리 및 사용자 편의성을 고려해 따로 생성 로직을 구현했다. 따라서 다음과 같은 규칙들을 고려해서 설정했다:

  • 주문별로 고유한 값
  • 주문에 대한 대략적인 정보를 담고있음 (업무효율성 향상)
  • 고정된 길이 + 최대한 짧은 길이 (고객실수 방지)

따라서 다음과 같은 형태로 주문 번호를 생성하기로 했다.

연월일(YYMMDD) + 모든 영문자 5자리

// ../order.tsx

import { customAlphabet } from "nanoid";

// 주문번호 생성 로직
const today = new Date();
const year = today.getFullYear().toString().slice(-2);
const month = (today.getMonth() + 1).toString().padStart(2, "0");
const day = today.getDate().toString().padStart(2, "0");
const yymmdd = year + month + day;

const customNanoid = customAlphabet(
  "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  5
);
const orderId = yymmdd + customNanoid();

리다이렉트 경로 설정하기

그 다음으로는 successUrlfailUrl 을 설정해 주어야 한다.
이 두 경로는 각각 결제 인증에 성공했을 때 그리고 실패했을 때 리다이렉트 될 경로다.

해당 정보들을 모두 입력해 준 뒤 코드는 다음과 같다:

// ../order.tsx

const proceedPayment = async () => {
    const paymentWidget = paymentWidgetRef.current;

    try {
      await paymentWidget?.requestPayment({
        orderId: orderId,
        orderName: orderTitle,
        customerName: data ? data.user?.name! : newAdrs.name,
        successUrl: `${window.location.origin}/order/success`,
        failUrl: `${window.location.origin}/order/fail`,
      });
    } catch (err) {
      console.log(err);
    }
  };

return (
    <main>
      <div>
        <div id="payment-widget" />
        <div id="agreement" />
        <button
          type="button"
          onClick={proceedPayment}
        >
          결제하기
        </button>
      </div>
    </main>
  );
};

결제 승인하기

결제 승인 과정은 결제 요청 때 저장해 둔 결제 정보와 요청 결과로 돌아온 결제 정보 값이 같은지 검증하는 과정이다. 즉 클라이언트에서 결제 금액을 조작해 승인하는 행위를 방지하기 위해 거치는 과정이다.

앞서 진행한 결제 요청이 성공하게 되면 설정해놓은 successUrl 로 리다이렉트 되게 되는데 이때 URL에 포함된 파라미터를 사용해 결제 승인을 요청할 수 있다.

결제 성공 URL은 다음과 같은 형태의 쿼리 파라미터를 전달 받는다.

https://{ORIGIN}/success?paymentKey={PAYMENT_KEY}&orderId={ORDER_ID}&amount={AMOUNT}&paymentType={PAYMENT_TYPE}

구현한 결제 인증 과정은 다음과 같다.

// ../order/success.tsx

import { useEffect } from "react";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import axios from "axios";

import { OrderConfirmType } from "common/types/tosspayments";
import Loader from "components/Loader/Loader";

export default function Success() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const secretKey = process.env.NEXT_PUBLIC_PAYMENTS_SECRET!;
  const orderId = searchParams.get("orderId");
  const paymentKey = searchParams.get("paymentKey");
  const amount = searchParams.get("amount");
  const authKey = btoa(secretKey + ":");

  // 서버로 결제 승인 요청 보내기
  useEffect(() => {
    if (
      !orderId ||
      !paymentKey ||
      !amount ||
      !authKey
    )
      return;

    const getOrderConfirmData = async () => {
      try {
        const confirm = await axios.post(
          "https://api.tosspayments.com/v1/payments/confirm",
          { paymentKey: paymentKey, amount: amount, orderId: orderId },
          {
            headers: {
              Authorization: `Basic ${authKey}`,
              "Content-Type": "application/json",
            },
          }
        );
       
        router.push(`/order/confirmation/${order.data.data.id}`);
      } catch (error) {
        console.log(
          "결제 승인 및 주문 저장과정에서 에러가 발생했습니다. " + error
        );
      }
    };

    getOrderConfirmData();
    
  }, [amount, authKey, data, orderId, paymentKey]);

  return <Loader isLoading={true} />;
}

느낀점

처음에 회원인 사용자들만 결제할 수 있도록 설계를 했는데 이후 비회원 주문 기능을 추가하면서 여러 오류가 발생했다. 일단은 발생하는 에러들을 해결해 놓은 상태이지만 만약에 또 로직을 변경해야 하는 일이 있다면 또 많은 문제가 발생할 수 있었다.

코드를 변경할 때마다 어디서 오류가 발생할지 알수가 없게되고 변경한 코드를 신뢰할 수 없게되는 경험은 결코 즐겁지 않은 경험이였고 만약 개인 프로젝트가 아닌 여러명이 작업하고 있었다면 문제는 더 커질 것 같았다. 이런 경험으로 왜 코드를 리팩토링하고 모듈화 하는지 그리고 왜 테스트 코드가 중요한지 느낄 수 있었다.

그리고 토스 페이먼츠에서 제공하는 개발자 문서에 감명 받았다. 최근에 작은 사이드 프로젝트를 진행했는데, 세네명만 협업하는데도 api가 잘 정리되어 있지 않으면 혼란스럽고 프로젝트 진행이 더뎌지는 경험을 했다. 반면 토스 개발자 문서는 다른 자료를 전혀 참고할 필요가 없을 정도로 상세하게 잘 정리되어 있었다.

이번 작업을 하면서 그냥 생각나는데로 기능 구현하기 급급하던 내 모습을 반성하게 되었다. 코드의 신뢰성을 높이고 문서화의 중요성을 알 수 있었다.



레퍼런스

토스페이먼츠 개발 가이드
토스페이먼츠 결제위젯 샘플 깃헙
CS처리에-효율적인-주문번호-만들기-주문번호-알고리즘

profile
인생 저장소

0개의 댓글