본문 바로가기
개발 기록장

[React] 토스페이먼츠 API로 Paypal 연동 해외결제 구현하기

by heereal 2023. 6. 14.

6월 1일에 출시된 따끈따끈한 토스페이먼츠 Paypal 연동 해외결제 API. React와 Next.js로 Paypal 해외결제를 구현한 과정을 기록으로 남겨보려 한다.

 

참고로 이번 프로젝트는 결제 수단이 Paypal 해외결제 한 가지였기 때문에 결제위젯을 사용하지 않고 예약 페이지 내의 결제 버튼을 클릭했을 때 바로 Paypal 결제 페이지로 연결되도록 구현했다.

 

 

1. npm 패키지 설치

npm install @tosspayments/payment-sdk
npm install nanoid

`script` 태그에 결제위젯 SDK 파일을 추가하는 방법이 아닌 npm 패키지로 설치하는 방법을 선택했다. nanoid는 결제 요청 시 `orderId`를 랜덤으로 생성하기 위해 설치한다.

 

 

2. API 키 준비

// 토스페이먼츠와 계약하지 않았다면 아래 PayPal 전용 테스트 키로 결제위젯을 연동하세요.
const clientKey = 'test_ck_BE92LAa5PVb1wPvWGxe37YmpXyJj'
const secretKey = 'test_sk_N5OWRapdA8d7wP41EbYro1zEqZKL'

토스페이먼츠 개발자센터 상단에 [내 개발정보] 탭에 들어가면 테스트 API 키를 확인할 수 있다. `clientKey`는 결제를 요청할 때, `secretKey`는 결제를 승인할 때 사용한다.

 

 

3. 결제 요청

import { loadTossPayments } from '@tosspayments/payment-sdk';
import { nanoid } from 'nanoid';

// 결제하기 버튼 클릭해서 결제창 띄우기
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const orderId = nanoid();

    loadTossPayments(clientKey).then((tossPayments) => {
      tossPayments
        .requestPayment('해외간편결제', {
          // 결제 정보 파라미터
          amount: totalPrice,
          orderId,
          customerName: name,
          orderName: serviceId.toString(),
          successUrl: `${window.location.origin}/success`,
          failUrl: `${window.location.origin}/fail`,
          provider: 'PAYPAL',
          currency: 'USD',
          country: 'US',
        })
        .then(() => {
          // sessionStorage에 예약 정보 저장
          setResvStorage({
            orderId,
            serviceId,
            ...
            totalPrice
          });
        })
        .catch(function (error) {
          // 결제 고객이 결제창을 닫았을 때 에러 처리
          if (
            error.code === 'USER_CANCEL' ||
            error.code === 'PAY_PROCESS_CANCELED'
          ) {
            alert('Payment has been canceled by the customer.');
            console.log('error', error);

            // 그 외의 경우 에러 처리
          } else {
            console.log('error:', error);
          }
        })
    });
};

`requestPayment` 메서드에 첫 번째 파라미터로 '해외간편결제'를 설정하고, 두 번째 파라미터로는 결제 정보를 넘긴다. 그리고 성공 혹은 실패했을 시 리다이렉트되는 페이지를 따로 생성하고 URL을 연결했다.

 

결제 정보 파라미터는 아래 링크에서 상세하게 확인할 수 있다.

https://docs.tosspayments.com/reference/js-sdk#requestpayment해외간편결제-결제-정보

 

나중에 추가적으로 작성하겠지만 then 부분에서 `Recoil-Persist`를 이용해서 예약 정보를 임시로 `sessionStorage`에 저장했다.

 

 

4. Paypal 결제

결제 요청에 성공하면 페이팔 결제 페이지로 넘어가게 된다. 테스트 단계에서는 토스페이먼츠에서 제공하는 테스트 계정으로 로그인하여 결제를 테스트할 수 있다.

 

 

5. 결제 요청 결과 확인

https://my-store.com/success?paymentKey={PAYMENT_KEY}&orderId={ORDER_ID}&amount={AMOUNT}

결제에 성공하면 다음과 같은 URL로 리다이렉트 되며 쿼리 파라미터로 paymentKey, orderId, amount 등의 정보를 얻을 수 있다.

 

import { useSearchParams } from 'next/navigation';

const searchParams = useSearchParams();
const orderId = searchParams.get('orderId');
const paymentKey = searchParams.get('paymentKey');
const amount = searchParams.get('amount');

나는 이 정보를 `useSearchParams` 훅으로 추출해서 사용했다.

 

 

6. 결제 승인

// src/app/success/page.tsx
import { useRouter, useSearchParams } from 'next/navigation';

const SuccessPayments = () => {
  const router = useRouter();
  
  // 결제 성공 URL 쿼리 파라미터
  const searchParams = useSearchParams();
  const orderId = searchParams.get('orderId');
  const paymentKey = searchParams.get('paymentKey');
  const amount = searchParams.get('amount');
  
  // sessionStorabe에 저장한 예약 정보 가져오기
  const resvStorage = useRecoilValue(resvStorageState);
  
  // sessionStorage에 저장한 예약 정보 reset
  const resetSessionStorage = useResetRecoilState(resvStorageState);

  // 결제 승인 및 DB 추가 완료했을 시 true로 변경
  const [isApproved, setIsApproved] = useState(false);

  // 토스 결제 승인 함수
  const confirmPayments = async () => {
    let options = {
      method: 'POST',
      url: 'https://api.tosspayments.com/v1/payments/confirm',
      headers: {
        Authorization:
          'Basic dGVzdF9za19XZDQ.....tTTc1eTB2Og==', 
          'Content-Type': 'application/json',
      },
      data: { paymentKey, amount, orderId },
    };

    axios
      .request(options)
      .then(function (response) {
        postReservation(); // 예약 내역 DB에 저장
      })
      .catch(function (error) {
        console.error(error);
        router.push('/failPayments'); // 승인 실패 시 실패 페이지로 이동
      });
  };

  // 결제 승인 시 DB에 결제 내용 create
  const postReservation = async () => {
    try {
      const response = await axios.post('/api/reservation', {
        serviceId: resvStorage.serviceId,
        orderId: resvStorage.orderId,
        ...
        totalPrice: resvStorage.totalPrice,
      });
      console.log('DB post 완료 => ', response);

      // 결제 승인 및 DB 추가 완료했을 시 spinner 사라짐
      setIsApproved(true);
      resetSessionStorage(); // sessionStorage 초기화
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    // 결제 금액과 successUrl로 받은 금액 일치하지 않으면 실패 페이지로 이동
    if (amountNum !== resvStorage.totalPrice) {
      router.push('/failPayments');
    } else {
      confirmPayments();
    }
  }, []);

  // 로딩 중 spinner
  if (!isApproved) {
    return <LoadingSpinner />;
  }
  
  return (
  	...
  )
}

 

 a. 인증 헤더 값 생성 

echo -n 'test_sk_N5OWRapdA8d7wP41EbYro1zEqZKL' + ':' | base64

결제 승인 요청 시 인증 헤더 값이 필요한데 `secretKey`를 이용해서 이 인증 헤더 값을 생성한다. VSC 터미널에 상단 명령어의 `secretKey` 부분을 자신의 테스트 API 키로 교체한 후에 실행한다.

 

let options = {
      method: 'POST',
      url: 'https://api.tosspayments.com/v1/payments/confirm',
      headers: {
        Authorization:
          'Basic 여기에복붙', 
          'Content-Type': 'application/json',
      },
      data: { paymentKey, amount, orderId },
    };

그러면 문자열을 인코딩해서 출력하는데 이 값을 그대로 복사해서 header Authorization에서 교체하면 된다.

 

 b. 결제 금액이 일치하는지 확인하기 

requestPayment() 메서드에 담아 보낸 결제 금액과 successUrl로 돌아온 amount 값이 같은지 반드시 확인해 보세요. 클라이언트에서 결제 금액을 조작해 승인하는 행위를 방지할 수 있습니다. 만약 값이 다르다면 결제를 취소하고 구매자에게 알려주세요. 

토스페이먼츠 공식 문서에서 다음과 같이 경고하고 있기 때문에 useEffec로 결제 요청 시 전송한 가격과 성공 페이지 URL에서 추출한 가격이 일치하는지 확인하는 로직을 추가했다. 두 가격이 일치할 때만 결제 승인을 요청하고, 일치하지 않는다면 결제 실패 페이지로 이동한다.

 

 c. axios.request로 결제 승인 요청 

결제 성공 URL에서 추출한 paymentKey, amount, orderId 정보를 넘기면서 결제 승인 API를 호출한다.

 

 d. 결제 승인 완료 후 DB에 결제 정보 저장 

결제 승인에 성공하면 예약 정보를 DB에도 따로 저장하도록 했다. 그런데 Paypal 결제 페이지로 넘어가면서 페이지가 새로고침되는 것인지 global state에 저장해 둔 사용자의 예약 정보가 모두 날아가는 문제가 발생했다.

 

그래서 Recoil-Persist 라이브러리를 이용해서 결제를 요청할 때 sessionStorage에 사용자가 입력한 예약 정보를 임시로 저장해 둔 후에 결제 성공 페이지에서는 sessionStoragedp 저장해 둔 예약 정보를 가져와서 DB에 업로드한 후 sessionStorage를 비워주는 방식으로 구현했다.

 

자세한 코드는 상단의 `postReservation` 함수를 참고하면 된다. 또한 Recoil-Persist 관련해서는 글을 따로 작성해 두었다.

 

[Recoil] 새로고침해도 유지되는 state_Recoil Persist

결제 기능을 구현하는 과정에서 결제창이 넘어가며 state가 초기화되어 결제 승인 페이지에 도착하기 전에 사용자가 입력한 예약 정보가 모두 날아가는 문제가 발생했다. 그래서 사용자가 입력

divheer.tistory.com

 

 

7. 결제 내역 확인

토스페이먼츠 개발자용 테스트상점 > 테스트 결제내역에서 다음과 같이 결제상태를 확인할 수 있다. 해외결제의 경우 외화 결제액은 정확히 표시되지 않는다고 하니 결제액 보고 놀라지 않기!

 

 


참고 문서

https://docs.tosspayments.com/guides/payment-widget/integration-paypal

https://docs.tosspayments.com/guides/paypal

https://velog.io/@tosspayments/React로-결제-페이지-개발하기-ft.-결제위젯

https://github.com/tosspayments/payment-widget-sample

 

 

댓글