사용 기술

프론트엔드는 Next.js / 백엔드는 스프링을 이용하여 포트원 API로 결제 기능을 구현해볼 것이다.

간단히 결제 기능에만 집중해서 만들것이며 기본적인 CORS같은건 굳이 설명하지 않는다.

이 글에는 프론트엔드 사이드 내용만 적고 백엔드 쪽은 이 글로 분리하려고 한다.

  • Next.js 14 / App router
  • TypeScript

사전 작업

아무래도 외부 라이브러리를 사용하는거다 보니 사전작업이 필요하다.

포트원 개발자센터를 찬찬히 읽어보면 이런저런 기능을 이용할 수 있을것이다.

1. 포트원에 회원가입

회원가입 -> 사이드 바의 결제 연동 탭 -> 식별코드/API Keys 탭

2. 결제용 채널 추가

2-1

2-2

2-3

3. Next.js에 SDK 추가

// app router 일 때
// RootLayout 에 Script 태그로 SDK 추가
...
<Script src="https://cdn.iamport.kr/v1/iamport.js" />
...

// page router 일 때 
// pages/_document.tsx에 Script 태그로 SDK 추가
...
<Script
     src="https://cdn.iamport.kr/v1/iamport.js"
     strategy="beforeInteractive"
/>
...

구현하기

@types > type.d.ts

정의만 해둔게 대부분이라 그냥 복붙해서 type 정의 해두자.

스크롤이 기니까 쭈욱 넘기면 된다.

// @types 경로에 아래 타입을 정의해두고 이용한다.

declare global {
  interface Window {
    IMP?: Iamport;
  }
}

export interface Iamport {
  init: (accountID: string) => void;
  request_pay: (
    params: RequestPayParams,
    callback?: RequestPayResponseCallback,
  ) => void;
  loadUI: (
    type: PaypalUI,
    params: PaypalRequestPayParams,
    callback?: RequestPayResponseCallback,
  ) => void;
  updateLoadUIRequest: (type: PaypalUI, params: PaypalRequestPayParams) => void;
}

export interface RequestPayAdditionalParams {
  /**
   * ### 디지털 구분자
   * - 휴대폰 결제수단인 경우 필수 항목
   * - 결제제품이 실물이 아닌 경우 true
   * - 실물/디지털 여부에 따라 수수로율이 상이하게 측정됨
   */
  digital?: boolean;
  /**
   * ### 가상계좌 입금기한
   * - 결제수단이 가상계좌인 경우 입금기한을 설정할 수 있습니다.
   * @example
   * - YYYY-MM-DD
   * - YYYYMMDD
   * - YYYY-MM-DD HH:mm:ss
   * - YYYYMMDDHHmmss
   */
  vbank_due?: string;
  /**
   * ### 결제완료이후 이동될 EndPoint URL 주소
   * - 결제창이 새로운 창으로 리다이렉트 되어 결제가 진행되는 결제 방식인 경우 필수 설정 항목 입니다.
   * - 대부분의 모바일 결제환경에서 결제창 호출시 필수 항목입니다.
   * - 리다이렉트 환경에서 해당 필드 누락시 결제 결과를 수신 받지 못합니다.
   */
  m_redirect_url?: string;
  /**
   * ### 모바일 앱 결제중 가맹점 앱복귀를 위한 URL scheme
   * - WebView 환경 결제시 필수설정 항목 입니다.
   * - ISP/앱카드 앱에서 결제정보인증 후 기존 앱으로 복귀할 때 사용합니다.
   */
  app_scheme?: string;
  /**
   * ### 사업자등록번호
   * - 다날-가상계좌 결제수단 사용시 필수 항목
   */
  biz_num?: string;
}

/**
 * Interface for Display configurations.
 *
 * @property {number[]} [card_quota] - 할부결제는 5만원 이상 결제 요청시에만 이용 가능합니다.
 *
 * @example
 * // 일시불만 결제 가능
 * { card_quota: [] }
 *
 * @example
 * // 일시불을 포함한 2, 3, 4, 5, 6개월까지 할부개월 선택 가능
 * { card_quota: [2,3,4,5,6] }
 */
export interface Display {
  card_quota?: number[];
}

// TODO: 다시 확인 필요, 공식문서에서 페이지마다 다른 정보를 알려주고있음
type PG =
  | "html5_inicis"
  | "inicis"
  | "kcp"
  | "kcp_billing"
  | "uplus"
  | "nice"
  | "kicc"
  | "bluewalnut"
  | "kakaopay"
  | "danal"
  | "danal_tpay"
  | "mobilians"
  | "payco"
  | "paypal"
  | "eximbay"
  | "naverpay"
  | "naverco"
  | "smilepay"
  | "alipay"
  | "paymentwall"
  | "tosspay"
  | "smartro"
  | "settle"
  | "settle_acc"
  | "daou"
  | "tosspayments"
  | "paypal_v2"
  | "nice_v2"
  | "smartro_v2"
  | "ksnet";

type PaymentMethod =
  | "card"
  | "trans"
  | "vbank"
  | "phone"
  | "paypal"
  | "applepay"
  | "naverpay"
  | "samsung"
  | "kpay"
  | "kakaopay"
  | "payco"
  | "lpay"
  | "ssgpay"
  | "tosspay"
  | "cultureland"
  | "smartculture"
  | "happymoney"
  | "booknlife"
  | "point"
  | "wechat"
  | "alipay"
  | "unionpay"
  | "tenpay";

interface EscrowProduct {
  id: string;
  /** 상품명 */
  name: string;
  /** 상품 코드 */
  code: string;
  /** 상품 단위 가격 */
  unitPrice: number;
  /** 수량 */
  quantity: number;
}

interface Card {
  /**
   * - 현재 KG이니시스, KCP, 토스페이먼츠, 나이스페이먼츠, KICC, 다날 6개 PG사에 대해서만 카드사 결제창 direct 호출이 가능합니다.
   * - 일부 PG사의 경우, 모든 상점아이디에 대하여 카드사 결제창 direct 노출 기능을 지원하지 않습니다. 반드시 포트원을 통해 현재 사용중인 상점아이디가 카드사 결제창 direct 호출이 가능하도록 설정이 되어있는지 PG사에 확인이 필요합니다.
   */
  direct?: {
    /** 카드사 금융결제원 표준 코드.  {@link https://chaifinance.notion.site/53589280bbc94fab938d93257d452216?v=eb405baf52134b3f90d438e3bf763630 링크} 참조 */
    code: string;
    /** 할부 개월 수. 일시불일 시 0 으로 지정. */
    quota: number;
  };
  detail?: {
    /** 금결원 카드사 코드 {@link https://chaifinance.notion.site/53589280bbc94fab938d93257d452216?v=eb405baf52134b3f90d438e3bf763630 링크} 참조 */
    card_code: string;
    /** 해당카드 활성화 여부 */
    enabled: boolean;
  }[];
}

export interface RequestPayParams extends RequestPayAdditionalParams {
  /**
   * ### PG사 구분코드
   *
   * @example
   * PG사코드.{상점ID}
   */
  pg?: string;
  /**
   * ### 결제수단 구분코드
   * - PG사별 지원되는 결제수단이 모두 상이합니다.
   */
  pay_method: PaymentMethod;
  /**
   * ### 에스크로 결제창 활성화 여부
   * - 일부 PG사만 지원됩니다.
   * - 에스크로 설정은 PG사와 협의 이후 진행되어야 하는점 주의하세요
   */
  escrow?: boolean;
  /**
   * ### 에스크로 결제 정보
   * - 에스크로 결제(escrow: true)시에만 유효하고, 필수 값은 아닙니다.
   * - 토스페이먼츠 신모듈 (pg: tosspayments.~)시에만 유효합니다
   */
  escrowProducts?: EscrowProduct[];
  /**
   * ### 가맹점 주문번호
   * - 주문번호는 매 결제 요청시 고유하게 채번 되어야 합니다.
   * - 40Byte 이내로 작성해주세요
   * - 결제 승인완료 처리된 주문번호를 동일하게 재 설정시 사전거절 처리 됩니다.
   */
  merchant_uid: string;
  /**
   * ### 결제대상 제품명
   * - 16byte 이내로 작성해주세요
   */
  name?: string;
  /**
   * 결제금액
   */
  amount: number;
  /**
   * ### 사용자 정의 데이타
   * - 결제 응답시 echo 로 받아보실수 있는 필드 입니다.
   * - JSON notation(string)으로 저장됩니다.
   * - 주문 건에 대해 부가정보를 저장할 공간이 필요할 때 사용합니다
   */
  custom_data?: Record<any, any>;
  /**
   * ### 면세금액
   * - 결제 금액 중 면세금액에 해당하는 금액을 입력합니다.
   */
  tax_free?: number;
  /**
   * ### 부가세
   * - 결제 금액 중 부가세(기본값: null)
   * - 지원되는 PG사
   *   - 나이스페이먼츠
   */
  vat_amount?: number | null;
  /**
   * ### 결제통화 구분코드
   * - PayPal은 원화(KRW) 미 지원으로 USD가 기본
   * - PayPal에서 지원하는 통화는 {@link https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/ PayPal 지원 통화} 참조
   */
  currency?: string;
  /** ### 결제창 언어 설정 (지원되지 않은 일부 PG사 존재) */
  language?: "en" | "ko";
  /** ### 주문자명 */
  buyer_name?: string;
  /**
   * ### 주문자 연락처
   * - 일부 PG사에서 해당 필드 누락시 오류 발생
   */
  buyer_tel?: string;
  /**
   * ### 주문자 이메일
   * - 일부 PG사에서 해당 필드 누락시 오류 발생(페이먼트월)
   */
  buyer_email?: string;
  /**
   * ### 주문자 주소
   */
  buyer_addr?: string;
  /**
   * ### 주문자 우편번호
   */
  buyer_postcode?: string;
  /**
   * ### confirm_process 사용 시 가맹점 endpoint url 설정
   * - 기술지원 메일로 별도 요청이 필요합니다. (support@portone.io)
   */
  confirm_url?: string;
  /**
   * ### 웹훅(Webhook) 수신 주소
   * - 포트원 관리자 콘솔에 설정한 웹훅 주소대신 사용할 웹훅 주소를 결제시마다 설정할 수 있습니다.
   * - 해당 값 설정시 관리자 콘솔에 설정한 주소로는 웹훅발송이 되지 않는점 유의하시기 바랍니다.
   */
  notice_url?: string | string[];
  /**
   * ### 가맹점 정의 빌링키
   * - 비인증 결제 이용시 빌링키와 1:1로 맵핑되는 가맹점 정의 고객 빌링키입니다.
   */
  customer_uid?: string;
  display?: Display;
  card?: Card;
}

export interface RequestPayAdditionalResponse {
  /**
   * ### 신용카드 승인번호
   * - 신용카드 결제수단에 한하여 제공
   */
  apply_num?: string;
  /**
   * ### 가상계좌 입금 계좌번호
   * - PG사로부터 전달된 정보 그대로 제공에 따라 숫자 외 dash(-) 또는 기타 기호가 포함되어 있을 수 있음
   */
  vbank_num?: string;
  /**
   * ### 가상계좌 입금은행 명
   */
  vbank_name?: string;
  /**
   * ### 가상계좌 예금주
   * - 계약된 사업자명으로 표시됨, 단, 일부 PG사의 경우 null 을 반환하므로 자체 처리 필요
   */
  vbank_holder?: string | null;
  /**
   * ### 가상계좌 입금기한 (UNIX timestamp)
   */
  vbank_date?: string;
}

export interface RequestPayResponse extends RequestPayAdditionalResponse {
  /**
   * ### 결제 성공여부
   * - 결제승인 혹은 가상계좌 발급이 성공한 경우, True\
   * - PG사/결제수단에 따라 imp_success로 반환됨
   */
  success?: boolean;
  /**
   * ### 결제 실패코드
   * - 결제가 실패하는 경우 PG사 원천코드가 내려갑니다.
   */
  error_code?: string;
  /**
   * ### 결제 실패메세지
   * - 결제가 실패하는 경우 PG사 원천메세지가 내려갑니다.
   */
  error_msg?: string;
  /**
   * ### 포트원 고유 결제번호
   * - success가 false이고 사전 validation에 실패한 경우, imp_uid는 null일 수 있음
   */
  imp_uid?: string | null;
  /** ### 주문번호 */
  merchant_uid: string;
  /** ### 결제수단 구분코드 */
  pay_method?: PaymentMethod;
  /** 결제금액 */
  paid_amount?: number;
  /** 결제상태 */
  status?: string;
  /** 주문자명 */
  name?: string;
  /** PG사 구분코드 */
  pg_provider?: PG;
  /**
   * ### 간편결제 구분코드
   * - 결제창에서 간편결제 호출시 결제 승인된 PG사 구분코드
   * - 일부 PG사 또는 간편결제로 결제가 발생되지 않은 경우 해당 파라미터는 생략됩니다.
   */
  embb_pg_provider?:
    | "naverpay"
    | "kakaopay"
    | "payco"
    | "samsungpay"
    | "ssgpay"
    | "lpay";
  /**
   * ### PG사 거래번호
   * - PG사에서 거래당 고유하게 부여하는 거래번호입니다.
   */
  pg_tid?: string;
  /** ### 주문자명 */
  buyer_name?: string;
  /** ### 주문자 이메일 */
  buyer_email?: string;
  /** ### 주문자 연락처 */
  buyer_tel?: string;
  /** ### 주문자 주소 */
  buyer_addr?: string;
  /** ### 주문자 우편번호 */
  buyer_postcode?: string;
  /** ### 가맹점 임의 지정 데이터 */
  custom_data?: string;
  /** ### 결제승인시각 (UNIX timestamp) */
  paid_at?: string;
  /** ### 거래 매출전표 URL */
  receipt_url?: string;
}

export type RequestPayResponseCallback = (response: RequestPayResponse) => void;

type PaypalUI = "paypal-spb" | "paypal-rt";

export interface PaypalRequestPayParams extends RequestPayParams {
  pg: string;
  pay_method: "paypal";
  /**
   * ### 국가코드
   * - 주의: 페이팔 일반결제 테스트 모드시에만 유효
   */
  country?: string;
  /**
   * ### 구매자 이름 주의:
   * - 페이팔에서만 유효하며 buyer_name이 아닌 buyer_first_name과 buyer_last_name 입력을 권장
   */
  buyer_first_name?: string;
  /**
   * ### 구매자 이름 주의:
   * - 페이팔에서만 유효하며 buyer_name이 아닌 buyer_first_name과 buyer_last_name 입력을 권장
   */
  buyer_last_name?: string;
  /**
   * ### 결제 상품 정보
   * - 구매 상품 상세 정보를 의미하며 전달 한 값 중 name(상품 명), quantity(상품 수량), unitPrice(상품 단위 금액)만 결제창에 표기됩니다.
   * - 페이팔은 해당 파라미터 입력을 강력 권장하고 있으니, 되도록 입력해주시기 바랍니다.
   * - 각 상품의 수량 * 단위 가격의 총 합이 주문 총 금액과 반드시 일치해야합니다. 일치하지 않는 경우 에러 메시지가 리턴되면서 결제창이 호출되지 않습니다.
   */
  products?: {
    id?: string;
    name?: string;
    code?: string;
    unitPrice?: number;
    quantity?: number;
    tag?: string;
  }[];
  /**
   * ### 결제 통화
   * @default USD
   */
  currency?: string;
}

app > pay > page.tsx

위에서 사전준비 할 때 봤던 고객사 식별코드(가맹점 식별코드)를 여기서 사용한다.

편의상 이렇게 적어뒀지만 구현에 따라 변수로 받아서 사용하면 된다.

"use client";

import axios from "axios";
import { RequestPayParams, RequestPayResponse } from "../../@types/type";

export default function Test() {
  const onClickPayment = () => {
    if (!window.IMP) return;
    /* 1. 가맹점 식별하기 */
    const { IMP } = window;
    IMP.init("가맹점 식별코드"); // 가맹점 식별코드

    /* 2. 결제 데이터 정의하기 */
    const data: RequestPayParams = {
      pg: "html5_inicis", // PG사 : https://developers.portone.io/docs/ko/tip/pg-2 참고
      pay_method: "card", // 결제수단
      merchant_uid: `mid_${new Date().getTime()}`, // 주문번호
      amount: 100, // 결제금액
      name: "아임포트 결제 데이터 분석", // 주문명
      buyer_name: "홍길동", // 구매자 이름
      buyer_tel: "01012341234", // 구매자 전화번호
      buyer_email: "example@example.com", // 구매자 이메일
      buyer_addr: "신사동 661-16", // 구매자 주소
      buyer_postcode: "06018", // 구매자 우편번호
    };
    console.log(data);

    /* 4. 결제 창 호출하기 */
    IMP.request_pay(data, callback);
  };

  function callback(response: RequestPayResponse) {
    const { success, error_msg, imp_uid } = response;
    console.log("Payment success!");
    console.log("Payment ID : " + response.imp_uid);
    console.log("Order ID : " + response.merchant_uid);
    console.log("Payment Amount : " + response.paid_amount);

    // 성공 시 백엔드로 주문번호 전송
    if (success) {
      axios.post(`http://localhost:8080/orders/${imp_uid}`);
      alert("결제 성공");
    } else {
      alert(`결제 실패: ${error_msg}`);
    }
  }

  return (
    <>
      <div className="bg"> hi </div>
      <button onClick={onClickPayment}>결제하기</button>
    </>
  );
}


결제까지 진행하진 않았지만 결제도 테스트 계정에서 돈이 빠져나가는 식으로 잘 된다.

이제 백엔드와 연동해서 검증/환불 로직을 구현하면 완성

이니시스

카카오페이

 

참고 자료

 

포트원(아임포트) Front-end 연동하기(react, next.js, typescript)

포트원(구 아임포트) 프론트엔드에 붙여보기 (with Next.js + Typescript)

velog.io

 

'프론트엔드 > 연습' 카테고리의 다른 글

react-hook-form 이해하기  (0) 2024.03.13
Next.js 폴더 구조  (0) 2024.03.11
쿼리 스트링을 이용하여 모달 상태 관리  (0) 2024.01.27
페이지네이션 구현  (0) 2024.01.25
6주차 코드 리뷰 후기  (0) 2024.01.03

+ Recent posts