일반결제 연동하기


해당 가이드는 아임포트 일반 결제 기능을 웹사이트에 설치하고 서버 데이터베이스에 결제 결과 정보를 저장하는 방법을 안내합니다.아임포트의 JavaScript 라이브러리를 삽입하여 클라이언트에 결제기능을 추가합니다. 그 다음 고객의 결제 프로세스 완료 후 서버에 조회 키를 전달하는 로직을 작성합니다. 서버에서는 아임포트 서버로부터 결제정보를 조회한 후 결제 위변조 여부를 검증한 후 서비스의 데이터베이스에 저장합니다.
STEP1아임포트 라이브러리 추가하기
웹사이트의 결제페이지의 HTML에 아래의 <script>를 삽입합니다. 아임포트 라이브러리는 jQuery 기반으로 동작하기 때문에 jQuery 1.0 이상이 반드시 설치되어 있어야합니다.
  <!-- jQuery -->
  <script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
  <!-- iamport.payment.js -->
  <script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.1.5.js"></script>
STEP2가맹점 식별하기
IMP 객체의 init 함수의 인자에 [가맹점 식별코드](/implementation/account-info?lang=ko)를 삽입하고 웹사이트의 결제 페이지에서 호출합니다.
  var IMP = window.IMP; // 생략해도 괜찮습니다.
  IMP.init("imp00000000"); // "imp00000000" 대신 발급받은 "가맹점 식별코드"를 사용합니다.
코드에서는 init()함수의 인자에 임의의 번호를 작성했습니다. 여러분의 관리자 대시보드에서 가맹점 식별번호를 확인 후 함수의 인자에 작성하면 됩니다.
STEP3결제창 호출코드 추가하기
IMP.request_pay(param, callback)을 호출합니다. 함수의 첫번째 인자인 param에 결제 요청에 필요한 속성과 값을 담습니다.
  // IMP.request_pay(param, callback) 호출
  IMP.request_pay({ // param
    pg: "html5_inicis",
    pay_method: "card",
    merchant_uid: "ORD20180131-0000011",
    name: "노르웨이 회전 의자",
    amount: 64900,
    buyer_email: "gildong@gmail.com",
    buyer_name: "홍길동",
    buyer_tel: "010-4242-4242",
    buyer_addr: "서울특별시 강남구 신사동",
    buyer_postcode: "01181"
  }, function (rsp) { // callback
    if (rsp.success) {
        ...,
        // 결제 성공 시 로직,
        ...
    } else {
        ...,
        // 결제 실패 시 로직,
        ...
    }
  });
IMP.request_pay(param, callback)을 호출하면 PC 환경에서는 지정한 pg사의 결제모듈 창이 나타납니다. 반면 모바일 환경에서는 pg사의 웹사이트로 리다이렉트하는 프로세스를 제공하는 pg사도 있습니다.

함수의 첫번째 인자인 param 객체(object)에는 결제요청에 필요한 정보를 담습니다. 결제하고자 하는 수단(pay_method), 상품의 이름(name), 상품 가격(amount)등의 속성에 알맞은 값을 지정합니다. param속성에 대해 자세히 알아보세요.

주문번호(merchant_uid) 생성하기

IMP.request_pay를 호출하기 전에 여러분의 서버에 주문 정보를 전달(데이터베이스에 주문정보 INSERT)하고 서버가 생성한 주문 번호를 parammerchant_uid속성에 지정하기를 권장드립니다. 결제 완료 후 결제 위변조 여부를 검증하는 단계에서 신뢰도있는 검증을 위해 여러분의 서버에서 주문정보를 조회해야 하기 때문입니다.
그 다음, 두번째 인자인 callback은 고객이 결제를 완료한 후 실행되는 함수입니다. callback함수로 전달되는 인자 rsp는 결제의 성공여부, 결제 정보, 에러 정보 등을 담고있습니다. 고객의 결제 프로세스 완료 후 rsp을 통해 결제 결과를 확인하고 수행하고자 하는 작업을 callback에서 작성하면 됩니다. callback으로 전달되는 rsp인자에 대해 자세히 알아보세요.

웹사이트의 주문페이지에서는 버튼을 클릭하여 결제를 시작하는 것이 일반적인 흐름입니다. 아래의 예제와 같이 결제 버튼을 클릭하여 IMP.request_pay(param, callback)함수를 호출하는 코드를 작성할 수 있습니다.
  <button onclick="requestPay()">결제하기</button>
  ...
  <script>
    function requestPay() {
      // IMP.request_pay(param, callback) 호출
      IMP.request_pay({ // param
          pg: "html5_inicis",
          pay_method: "card",
          merchant_uid: "ORD20180131-0000011",
          name: "노르웨이 회전 의자",
          amount: 64900,
          buyer_email: "gildong@gmail.com",
          buyer_name: "홍길동",
          buyer_tel: "010-4242-4242",
          buyer_addr: "서울특별시 강남구 신사동",
          buyer_postcode: "01181"
      }, function (rsp) { // callback
          if (rsp.success) {
              ...,
              // 결제 성공 시 로직,
              ...
          } else {
              ...,
              // 결제 실패 시 로직,
              ...
          }
      });
    }
  </script>
일반적으로 버튼의 onclick 이벤트가 발생하면 IMP.request_pay(param, callback) 함수를 호출하여 결제 프로세스를 시작할 수 있습니다.
STEP4가맹점 서버에 쿼리 파라메터 전달하기
callback에서 결제가 성공(rsp.success: true)하면 rspimp_uidmerchant_uid를 서버에 전달하는 로직을 작성합니다.
  IMP.request_pay({
    /* ...중략... */
  }, function (rsp) { // callback
    if (rsp.success) { // 결제 성공 시: 결제 승인 또는 가상계좌 발급에 성공한 경우
      // jQuery로 HTTP 요청
      jQuery.ajax({
          url: "https://www.myservice.com/payments/complete", // 가맹점 서버
          method: "POST",
          headers: { "Content-Type": "application/json" },
          data: {
              imp_uid: rsp.imp_uid,
              merchant_uid: rsp.merchant_uid
          }
      }).done(function (data) {
        // 가맹점 서버 결제 API 성공시 로직
      })
    } else {
      alert("결제에 실패하였습니다. 에러 내용: " +  rsp.error_msg);
    }
  });
가맹점 서버에 imp_uid(거래 고유 번호)를 전달하면 아임포트 서버에서 imp_uid로 결제 정보를 조회할 수 있습니다. 또, 가맹점에서 관리하는 주문번호인 merchant_uid로 가맹점의 데이터베이스에서 주문 정보를 조회합니다. 조회한 정보들을 통해 결제 위변조 여부를 검증하고, 서비스의 데이터베이스에 저장할 수 있습니다.

거래정보 조회를 서버사이드에서 수행하는 이유

거래정보를 조회하기 위해서는 관리자 대시보드에서 발급받은 REST API키REST API Secret으로 토큰(access_token)을 발급받은 후, 해당 토큰을 결제 정보를 조회 API 요청에 포함해야합니다. 토큰 발급 과정을 클라이언트에서 수행하면 REST API키REST API Secret이 노출되어 보안상 안전하지 않기 때문에 토큰 발급 및 거래 정보 조회 과정은 반드시 서버사이드에서 수행해야합니다.
STEP5서버에서 거래 검증 및 데이터 동기화
결제가 완료되면 서버에서 해당 거래의 결제금액이 위변조 되었는지 검증하고 거래 데이터를 여러분의 서비스의 데이터베이스에 저장하여 데이터를 동기화합니다.
1API Endpoint 생성하기
먼저, 서버에서 imp_uid를 전달받는 API Endpoint를 생성합니다.
  app.use(bodyParser.json());
  // "/payments/complete"에 대한 POST 요청을 처리
  app.post("/payments/complete", async (req, res) => {
    try {
      const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
    } catch (e) {
      res.status(400).send(e);
    }
  });
위의 코드는 /payments/complete에 대한 POST요청을 처리하는 API입니다. HTTP request의 body에 담긴 imp_uid를 추출했습니다.
2결제정보 조회하기
그 후, [REST API키](/implementation/account-info?lang=ko)[REST API Secret](/implementation/account-info?lang=ko) 을 활용하여 https:\//api.iamport.kr/users/getToken에서 액세스 토큰(access token)을 발급받고, 해당 액세스 토큰과 추출한 imp_uid를 사용하여 https:\//api.iamport.kr/payments/$imp_uid에 대한 요청을 통해 결제정보를 조회합니다.

REST API 액세스 토큰(access token)

사적 리소스에 접근하는 모든 아임포트 REST API에 대한 요청에는 액세스 토큰(access token)을 포함해야 합니다. 액세스 토큰의 발급 및 사용 방법은 REST API 액세스 토큰(access token) 문서에서 자세히 살펴볼 수 있습니다.
  app.use(bodyParser.json());
  ...
  // "/payments/complete"에 대한 POST 요청을 처리
  app.post("/payments/complete", async (req, res) => {
    try {
      const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
      ...
      // 액세스 토큰(access token) 발급 받기
      const getToken = await axios({
        url: "https://api.iamport.kr/users/getToken",
        method: "post", // POST method
        headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
        data: {
          imp_key: "imp_apikey", // REST API키
          imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
        }
      });
      const { access_token } = getToken.data.response; // 인증 토큰
      ...
      // imp_uid로 아임포트 서버에서 결제 정보 조회
      const getPaymentData = await axios({
        url: \`https://api.iamport.kr/payments/\${imp_uid}\`, // imp_uid 전달
        method: "get", // GET method
        headers: { "Authorization": access_token } // 인증 토큰 Authorization header에 추가
      });
      const paymentData = getPaymentData.data.response; // 조회한 결제 정보
      ...
    } catch (e) {
      res.status(400).send(e);
    }
  });
위의 코드에서, 결제정보를 조회하기 위해 아임포트 계정에 발급되는 REST API키REST API Secret으로 액세스 토큰을 발급받았습니다. 액세스 토큰 발급은 https:\//api.iamport.kr/users/getTokenContent-Typeapplication/json, 그리고 body에 imp_key: REST API키, imp_secret: REST API Secret을 포함하여 POST방식으로 요청을 생성했습니다.

해당 요청에 대한 응답이 오면, 응답 데이터에 담겨있는 인증토큰인 access_token을 저장합니다. 그 다음 https:\//api.iamport.kr/payments/{imp_uid}imp_uid를 작성하고 Authorization에 발급받은 액세스 토큰인 access_token을 지정하고 GET방식으로 요청을 생성하여 결제정보를 조회했습니다.

아임포트 REST API에 대한 HTTP 요청

아임포트의 REST API를 사용하기 위해서는 각 API별 HTTP 요청 상세에 유의해야합니다.

API별로 요구되는 요청 방식(GET또는 POST), 요청 header(Authorization: access_token, Content-Type: application/json), 요청 parameter 등에 알맞게 요청을 생성해야합니다.

각 API에 대한 상세는 아임포트 REST API문서에서 확인할 수 있습니다.

HTTP 요청에 대해 익숙하지 않다면, 다음의 링크에서 확인할 수 있습니다.
3결제 위변조 여부 검증 후 데이터 동기화하기
거래 금액과 원래 결제되었어야 하는 금액을 비교하여 결제 위변조 여부를 검증하고, 결제 정보를 데이터베이스에 저장합니다.

결제 위변조 여부를 검증하는 이유

결제 요청은 클라이언트 환경에서 이루어지기 때문에 클라이언트의 스크립트를 조작해 금액을 위변조하여 결제를 요청할 수 있습니다. 따라서 결제 완료 후 실제로 거래된 금액과 거래되었어야 하는 금액을 반드시 비교해야합니다.

예를 들어, 100,000원짜리 상품을 결제할 때에는 amount: 100000으로 설정하여 결제요청을 생성해야 하지만, 공격자가 스크립트를 조작하여 해당 속성을 실제 금액보다 낮은 값으로 지정하여 결제요청을 생성할 수 있습니다. 클라이언트에서의 스크립트 조작은 원천적으로 막을 수 없는 기술적 특징이 있기 때문에, 결제 후 서버사이드에서 결제 위변조 여부를 반드시 검증해야합니다.
결제되었어야 하는 금액을 가맹점의 데이터베이스에서 조회하고, 아임포트에서 조회한 실제 결제 금액과 비교합니다. 그 후, 결제 정보를 데이터베이스에 저장한 뒤 결제 상태(status)에 따라 알맞은 응답을 생성합니다.
  app.use(bodyParser.json());
  ...
  // "/payments/complete"에 대한 POST 요청을 처리
  app.post("/payments/complete", async (req, res) => {
    try {
      const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
      // 액세스 토큰(access token) 발급 받기
      /* ...중략... */
      // imp_uid로 아임포트 서버에서 결제 정보 조회
      /* ...중략... */
      const paymentData = getPaymentData.data.response; // 조회한 결제 정보
      ...
      // DB에서 결제되어야 하는 금액 조회
      const order = await Orders.findById(paymentData.merchant_uid);
      const amountToBePaid = order.amount; // 결제 되어야 하는 금액
      ...
      // 결제 검증하기
      const { amount, status } = paymentData;
      if (amount === amountToBePaid) { // 결제 금액 일치. 결제 된 금액 === 결제 되어야 하는 금액
        await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // DB에 결제 정보 저장
        ...
        switch (status) {
          case "ready": // 가상계좌 발급
            // DB에 가상계좌 발급 정보 저장
            const { vbank_num, vbank_date, vbank_name } = paymentData;
            await Users.findByIdAndUpdate("/* 고객 id */", { $set: { vbank_num, vbank_date, vbank_name }});
            // 가상계좌 발급 안내 문자메시지 발송
            SMS.send({ text: \`가상계좌 발급이 성공되었습니다. 계좌 정보 \${vbank_num} \${vbank_date} \${vbank_name}\`});
            res.send({ status: "vbankIssued", message: "가상계좌 발급 성공" });
            break;
          case "paid": // 결제 완료
            res.send({ status: "success", message: "일반 결제 성공" });
            break;
        }
      } else { // 결제 금액 불일치. 위/변조 된 결제
        throw { status: "forgery", message: "위조된 결제시도" };
      }
    } catch (e) {
      res.status(400).send(e);
    }
  });
이전 과정의 예제에 이어, 원래 결제 되었어야하는 금액을 서비스의 데이터베이스에서 조회했습니다. 위의 코드에서는 주문내역 테이블/컬렉션에서 merchart_uid에 지정한 주문 고유번호를 통해 주문 정보를 조회하여 결제 되었어야 하는 금액을 알아냈습니다.

그 다음, 실제 결제된 금액인 amount와 결제 되었어야 하는 금액인 amountToBePaid를 비교하여 결제 위변조 여부를 검사했습니다. 결제 금액이 일치하면 결제 내역을 가맹점의 데이터베이스에 저장하고, 결제 상태(status)에 따라 알맞은 응답을 전송합니다.

데이터베이스의 merchant_uid

해당 예제는, 결제 프로세스 시작 전, 즉 IMP.request_pay(param, callback)을 호출하기 전에 상점의 데이터베이스에 주문 정보를 저장했음을 가정했습니다. 이러한 가정은 커머스앱에서 일반적으로 사용하는 주문번호관리 프로세스에서 출발합니다.

먼저 유저가 주문정보를 입력하고 결제를 시도하면 주문정보를 상점의 서버에 전달합니다. 서버는 전달받은 주문정보를 상점의 데이터베이스에 저장한 다음 주문의 unique한 주문번호(데이터베이스 저장시 생성)를 클라이언트에 전달합니다. 클라이언트는 전달받은 주문의 unique한 번호를 param속성의 merchant_uid에 지정하여 결제를 요청하게 됩니다. 해당 프로세스를 구현해야 신뢰할 수 있는 검증을 할 수 있습니다.

서비스의 데이터베이스에 저장하는 이유

결제를 완료한 고객이 여러분의 서비스에 접근하여 결제 내역을 확인할 수 있게 하기 위해서 여러분의 서비스의 데이터베이스에 거래 내역을 저장합니다.

예를 들어, 고객이 결제 후 서비스의 주문내역 페이지에 접근했을 때 고객의 결제 정보를 보여줘야 합니다. 이 결제 정보는 서비스의 데이터베이스에 저장되어 있어야하므로 아임포트에서 거래 데이터를 조회 후 내 서비스의 데이터베이스에 저장하여 데이터를 동기화합니다.
STEP6서버 응답 처리하기
다시 클라이언트로 돌아와서, 4단계의 요청에 대한 응답을 처리하는 로직을 작성합니다.
  IMP.request_pay({
    /* ...중략... */
  }, function (rsp) { // callback
    if (rsp.success) { // 결제 성공 시: 결제 승인 또는 가상계좌 발급에 성공한 경우
        // jQuery로 HTTP 요청
        jQuery.ajax({
          /* ...중략... */
        }).done(function(data) { // 응답 처리
          switch(data.status) {
            case: "vbankIssued":
              // 가상계좌 발급 시 로직
              break;
            case: "success":
              // 결제 성공 시 로직
              break;
          }
        });
    } else {
      alert("결제에 실패하였습니다. 에러 내용: " +  rsp.error_msg);
    }
  });
STEP7Webhook 및 데이터동기화 설정하기
아임포트 서버의 결제 상태를 가맹점의 데이터베이스에 동기화하고 네트워크 불안정성을 보완하기 위해 아임포트 Webhook을 사용할 수 있도록 콜백 URL을 생성하고, 로직을 작성해야 합니다.

신용카드로 결제가 완료되었을 때, 발급한 가상계좌에 고객이 입금했을 때, 그리고 아임포트 관리자 대시보드에서 결제 건이 환불되었을 때 아임포트의 Webhook이 호출되어 지정한 콜백 url에 imp_uidmerchant_uid를 전달하게 됩니다. 가맹점 서버는 전달받은 imp_uid값으로 아임포트 서버에서 결제 정보를 조회한 후, 상태에 따라 알맞게 데이터를 동기화합니다.

아임포트 Webhook

아임포트 Webhook을 활용하여 갑작스러운 브라우저의 오동작으로 인한 네트워크 불안정성을 보완하고, 가상계좌 발급 또는 입금 시점의 데이터 동기화를 수행할 수 있습니다. 아임포트 Webhook 문서에서 아임포트 Webhook의 개념과 콜백 url을 설정하는 방법을 자세히 살펴볼 수 있습니다.
1콜백 URL Endpoint 생성하기
아임포트 webhook이 호출될 때 생성되는 POST 요청을 처리할 Endpoint를 생성합니다.
  app.use(bodyParser.json());
  ...
  // "/iamport-webhook"에 대한 POST 요청을 처리
  app.post("/iamport-webhook", async (req, res) => {
    try {
      const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
    } catch (e) {
      res.status(400).send(e);
    }
  })
/iamport-webhook에 대한 POST 요청을 처리하는 Endpoint를 생성했습니다. HTTP request의 body에 담긴 imp_uidmerchant_uid를 추출했습니다.
2결제 정보 조회하기
그 후, REST API키REST API Secret 을 활용하여 https:\//api.iamport.kr/users/getToken에서 액세스 토큰(access token)을 발급받고, 해당 액세스 토큰과 추출한 imp_uid를 사용하여 https:\//api.iamport.kr/payments/$imp_uid에 대한 요청을 통해 결제정보를 조회합니다.
  app.use(bodyParser.json());
  ...
  // "/iamport-webhook"에 대한 POST 요청을 처리
  app.post("/iamport-webhook", async (req, res) => {
    try {
        const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
        // 액세스 토큰(access token) 발급 받기
        const getToken = await axios({
          url: "https://api.iamport.kr/users/getToken",
          method: "post", // POST method
          headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
          data: {
            imp_key: "imp_apikey", // REST API키
            imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
          }
        });
        const { access_token } = getToken.data.response; // 인증 토큰
        ...
        // imp_uid로 아임포트 서버에서 결제 정보 조회
        const getPaymentData = await axios({
          url: \`https://api.iamport.kr/payments/\${imp_uid}\`, // imp_uid 전달
          method: "get", // GET method
          headers: { "Authorization": access_token } // 인증 토큰 Authorization header에 추가
        });
        const paymentData = getPaymentData.data.response; // 조회한 결제 정보
        ...
    } catch (e) {
      res.status(400).send(e);
    }
  });
위의 코드에서, 결제정보를 조회하기 위해 아임포트 계정에 발급되는 REST API키REST API Secret으로 액세스 토큰을 발급받았습니다. 액세스 토큰 발급은 https:\//api.iamport.kr/users/getTokenContent-Typeapplication/json, 그리고 body에 imp_key: REST API키, imp_secret: REST API Secret을 포함하여 POST방식으로 요청을 생성했습니다.

해당 요청에 대한 응답이 오면, 응답 데이터에 담겨있는 액세스 토큰인 access_token을 저장합니다. 그 다음 https:\//api.iamport.kr/payments/{imp_uid}imp_uid를 작성하고 "Authorization"에 발급받은 액세스 토큰인 access_token을 지정하고 GET방식으로 요청을 생성하여 결제정보를 조회했습니다.
3결제 위변조 여부 검증 후 데이터 동기화하기
5단계에서 안내한 과정과 마찬가지로 거래 금액과 원래 결제되었어야 하는 금액을 비교하여 결제 위변조 여부를 검증하고, 결제 정보를 데이터베이스에 저장합니다.

결제되었어야 하는 금액을 가맹점의 데이터베이스에서 조회하고, 아임포트에서 조회한 실제 결제 금액과 비교합니다. 그 후, 결제 정보를 데이터베이스에 저장한 뒤 결제 상태(status)에 따라 알맞은 응답을 생성합니다.
  app.use(bodyParser.json());
  ...
  // "/iamport-webhook"에 대한 POST 요청을 처리
  app.post("/iamport-webhook", async (req, res) => {
    try {
        const { imp_uid, merchant_uid } = req.body; // req의 body에서 imp_uid, merchant_uid 추출
        // 액세스 토큰(access token) 발급 받기
        /* ...중략... */
        // imp_uid로 아임포트 서버에서 결제 정보 조회
        /* ...중략... */
        const paymentData = getPaymentData.data.response; // 조회한 결제 정보
        ...
        // DB에서 결제되어야 하는 금액 조회
        const order = await Orders.findById(paymentData.merchant_uid);
        const amountToBePaid = order.amount; // 결제 되어야 하는 금액
        ...
        // 결제 검증하기
        const { amount, status } = paymentData;
        if (amount === amountToBePaid) { // 결제 금액 일치. 결제 된 금액 === 결제 되어야 하는 금액
          await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // DB에 결제 정보 저장
          switch (status) {
            case "ready": // 가상계좌 발급
              // DB에 가상계좌 발급 정보 저장
              const { vbank_num, vbank_date, vbank_name } = paymentData;
              await Users.findByIdAndUpdate("/* 고객 id */", { $set: { vbank_num, vbank_date, vbank_name }});
              // 가상계좌 발급 안내 문자메시지 발송
              SMS.send({ text: \`가상계좌 발급이 성공되었습니다. 계좌 정보 \${vbank_num} \${vbank_date} \${vbank_name}\`});
              res.send({ status: "vbankIssued", message: "가상계좌 발급 성공" });
              break;
            case "paid": // 결제 완료
              res.send({ status: "success", message: "일반 결제 성공" });
              break;
          }
        } else { // 결제 금액 불일치. 위/변조 된 결제
          throw { status: "forgery", message: "위조된 결제시도" };
        }
    } catch (e) {
      res.status(400).send(e);
    }
  });
이전 과정의 예제에 이어, 원래 결제 되었어야하는 금액을 서비스의 데이터베이스에서 조회했습니다. 위의 코드에서는 주문내역 테이블/컬렉션에서 merchart_uid에 지정한 주문 고유번호를 통해 주문 정보를 조회하여 결제 되었어야 하는 금액을 알아냈습니다.

그 다음, 실제 결제된 금액인 amount와 결제 되었어야 하는 금액인 amountToBePaid를 비교하여 결제 위변조 여부를 검사했습니다. 결제 금액이 일치하면 결제 내역을 가맹점의 데이터베이스에 저장하고, 결제 상태(status)에 따라 알맞은 응답을 전송합니다.
STEP8모바일 웹 환경에 대응하기
아래 단계는 KG이니시스, LG U+, NHN KCP, 나이스정보통신, JTNet 그리고 KICC를 연동하는 경우에만 적용됩니다.

모바일 웹 환경에서 KG이니시스, LG U+, NHN KCP, 나이스정보통신, JTNet 그리고 KICC는 PC 환경과는 다르게 각 PG사의 웹사이트로 리디렉션되어 결제가 진행됩니다. 따라서 IMP.request_pay(param, callback)를 호출해 결제시, 결제 프로세스 완료 후 callback 함수가 실행되지 않습니다.

리디렉션 방식으로 결제시 callback 함수가 호출되지 않는 이유

브라우저의 URL이 변경되면, IMP.request_pay에 지정한 callback 함수가 메모리에서 해제되기 때문에, 결제 프로세스 완료 후 callback 함수가 실행될 수 없습니다.
아임포트는 이에 대응하기 위해 결제 프로세스 완료 후 특정 URL로 302 리디렉션 될 수 있도록 m_redirect_url 파라메터를 제공하고 있습니다.
1리디렉션 설정하기
아래와 같이 IMP.request_pay(param, callback) 호출시 param 객체의 m_redirect_url 파라메터에 결제 프로세스 완료 후 리디렉션 될 URL을 지정합니다.
  // IMP.request_pay(param, callback) 호출
  IMP.request_pay({
      /* ...중략... */,
      m_redirect_url: "https://www.myservice.com/payments/complete/mobile"
  }, /* callback */); // callback은 실행 안됨

이해를 돕기 위한 예시일 뿐, myservice.com을 귀하의 실제 가맹점 도메인으로 바꿔서 설정하셔야 합니다.
결제 프로세스가 완료되면 웹에서는 콜백 함수가 호출되고, 모바일 웹에서는 m_redirect_url로 302 리디렉션(GET 요청)됩니다. 이때 결제 결과(imp_uid, merchant_uid, imp_success, error_code, error_msg 등)가 쿼리 파라메터로 함께 전달됩니다

결제 프로세스 완료의 의미

결제 프로세스 완료는 아래의 모든 경우를 포함합니다.

  1. 결제 완료(결제 상태: paid, imp_success: true)
  2. 결제 실패(결제 상태: failed, imp_success: false)
  3. PG 모듈 설정이 올바르지 않아, 결제 창 자체가 렌더링 되지 함
  4. 사용자가 임의로 X 버튼이나 취소 버튼을 눌러 결제 프로세스 종료
  5. 카드 정보 불일치, 한도 초과, 잔액 부족 등의 사유로 결제 프로세스가 중단
  6. 가상계좌 발급 완료(결제 상태: ready, imp_success: true)
아래는 m_redirect_urlhttps:\//myservice.com/payments/complete로 설정했다는 가정하에 리디렉션되는 URL 예시입니다.
  curl https://myservice.com/payments/complete?imp_uid=결제건을_특정하는_아임포트_번호&merchant_uid=가맹점_고유_주문번호&imp_success=true
따라서 위 URL에 대한 GET 요청을 처리하는 로직을 작성하고, 전달받은 imp_uidmerchant_uid로 거래 검증 및 데이터 동기화 작업을 수행하면 됩니다.
2imp_success 취급시 주의사항
imp_success 파라메터는 결제 프로세스 정상 종료 여부를 의미합니다. 하지만 클라이언트 영역에서 자바스크립트 함수 호출로 결제창이 호출되는 만큼, 결제 금액이 위/변조되었을 가능성이 있기 때문에, 이 값으로 결제 성공 여부를 판단해서는 안됩니다.

따라서 imp_success값이 true일때는 가맹점 웹서버로 결제 결과(imp_uid, merchant_uid)를 전달(POST 요청)해 결제 금액이 위/변조 되었는지 검증(실제 결제 된 금액과 결제를 의도한 금액이 일치하는지 여부 판단) 후 최종적으로 결제 성공 여부를 판단해야 하고, false일때는 사용자에게 결제가 실패됐다는 사실을 안내하도록 가맹점 재량에 맞게 코드를 작성해주시면 됩니다.

일부 PG사에 한해 imp_success가 아닌 success 파라메터가 전달되거나 아예 전달되지 않는 경우(예) KG이니시스 - 실시간 계좌이체)도 있으니 주의하시길 바랍니다.
3API 생성 및 서버 로직 작성하기
5단계에서 안내한 결제 금액 위변조 검증 및 데이터베이스 저장 등의 서버 로직을 수행하는 코드를 작성합니다.
  app.use(bodyParser.json());
  ...
  // "/payments/complete/mobile/"에 대한 GET 요청을 처리
  app.get("/payments/complete/mobile/", async (req, res) => {
    try {
        const { imp_uid, merchant_uid } = req.query; // req의 query에서 imp_uid, merchant_uid 추출
        // 액세스 토큰(access token) 발급 받기
        const getToken = await axios({
          url: "https://api.iamport.kr/users/getToken",
          method: "post", // POST method
          headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
          data: {
            imp_key: "imp_apikey", // REST API키
            imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
          }
        });
        const { access_token } = getToken.data.response; // 인증 토큰
        ...
        // imp_uid로 아임포트 서버에서 결제 정보 조회
        const getPaymentData = await axios({
          url: \`https://api.iamport.kr/payments/\${imp_uid}\`, // imp_uid 전달
          method: "get", // GET method
          headers: { "Authorization": access_token } // 인증 토큰 Authorization header에 추가
        });
        const paymentData = getPaymentData.data.response; // 조회한 결제 정보
        ...
        // DB에서 결제되어야 하는 금액 조회
        const order = await Orders.findById(paymentData.merchant_uid);
        const amountToBePaid = order.amount; // 결제 되어야 하는 금액
        ...
        // 결제 검증하기
        const { amount, status } = paymentData;
        if (amount === amountToBePaid) { // 결제 금액 일치. 결제 된 금액 === 결제 되어야 하는 금액
          await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // DB에 결제 정보 저장
          switch (status) {
            case "ready": // 가상계좌 발급
              // DB에 가상계좌 발급 정보 저장
              const { vbank_num, vbank_date, vbank_name } = paymentData;
              await Users.findByIdAndUpdate("/* 고객 id */", { $set: { vbank_num, vbank_date, vbank_name }});
              // 가상계좌 발급 안내 문자메시지 발송
              SMS.send({ text: \`가상계좌 발급이 성공되었습니다. 계좌 정보 \${vbank_num} \${vbank_date} \${vbank_name}\`});
              res.send({ status: "vbankIssued", message: "가상계좌 발급 성공" });
              break;
            case "paid": // 결제 완료
              res.send({ status: "success", message: "일반 결제 성공" });
              break;
          }
        } else { // 결제 금액 불일치. 위/변조 된 결제
          throw { status: "forgery", message: "위조된 결제시도" };
        }
      } catch (e) {
        res.status(400).send(e);
      }
  });
위의 코드는 /payments/complete/mobile에 대한 GET요청을 처리하는 API입니다. HTTP request url의 query string에 담긴 imp_uidmerchant_uid를 추출했습니다. 추출한 데이터로 아임포트 REST API에서 거래 정보를 조회한 후 거래 검증과 데이터베이스 저장등의 로직을 수행한 후 적절한 응답을 생성합니다.