티스토리 뷰

아임포트 결제모듈 만들기

아임포트 결제 모듈 만들기

안녕하세요

이번에 결제 모듈을 만들면서 공부한 내용들을 정리하고자 포스팅합니다. 본인의 서버 환경과 서비스의 특징에 맞게 변경하여 사용하시면 됩니다. 

 

아임포트의 공식 doc을 참고하여 설명합니다. 그림으로 설명되어있는 결제 연동 절차를 알고 가시면, 결제 흐름을 쉽게 파악할 수 있습니다. 아임포트를 사용함으로써 가맹점 서버의 결제 데이터를 동기화하는 방식에 차이가 있습니다. 결제가 발생했을 때, 먼저 아임포트측 데이터베이스에 고객의 결제 데이터가 저장되고, 그 후에 webhook을 통해서 가맹점의 데이터베이스에 결제 정보를 저장합니다. 

 

webhook은 네트워크 및 고객 기기의 불안정성에 대처할 수 있는 방법입니다. 결제 고객의 브라우저가 아임포트 서버의  응답을 수신했으나, 결제 기기의 네트워크 불안정성, 브라우저의 갑작스러운 종료 등으로 인해 해당 url에 대한 GET 요청의 생성에 실패하는 경우, 콜백을 받아왔지만 갑자기 종료되는 경우가 있습니다. 따라서 webhook을 활용하면 아임포트 서버로부터 수신되는 결제 정보를 통해 데이터 동기화 누락을 막을 수 있습니다.

아임포트 결제 연동

 

목차:

1. 아임포트 라이브러리 추가하기

2. 가맹점 식별코드 삽입하기

3. 결제창 호출 코드 추가하기

4. 고객이 결제를 완료한 후 실행되는 함수(callback) 추가

5. 콜백 함수에서 쿼리 파라미터 전달하기

6. 가맹점 서버에서 거래 검증 및 데이터 동기화

7. 서버 응답 처리하기

8. 전체 코드

 

반응형

 

1. 아임포트 라이브러리 추가하기

먼저 아래의 <script>를 삽입합니다. 아임포트 라이브러리는 jQuery 기반으로 동작하기 때문에 꼭 설치되어야 합니다.

<!-- 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>

 

 

 

 

2. 가맹점 식별코드 삽입하기

Iinit 함수의 인자에 가맹점 식별코드를 입력합니다. 가맹점 식별코드는 아임포트 홈페이지 -> 대시보드 -> 로그인 -> 시스템 설정에서 찾을 수 있습니다.

가맹점 식별코드

  var IMP = window.IMP;
  IMP.init("iamport"); // "iamport" 대신 발급받은 "가맹점 식별코드"를 사용합니다.

 

 

 

 

3. 결제창 호출 코드 추가하기

IMP.request_pay(param, callback)을 호출하는 코드를 작성합니다. 함수의 첫 번째 인자인 param에 결제 요청에 필요한 속성과 값을 담습니다. 해당 함수를 호출하면 입력한 속성과 값에 따라 결제창을 보여줍니다.

 

파라미터들은 운영하는 서비스/환경에 맞게 설정해야 합니다.

  // IMP.request_pay(param, callback) 호출
  IMP.request_pay({ // param
    pg: "html5_inicis", // PG사 선택
    pay_method: "card", // 지불 수단
    merchant_uid: 'merchant_' + new Date().getTime(), //가맹점에서 구별할 수 있는 고유한id
    name: "맥북 프로 16인치", // 상품명
    amount: 2500000, // 가격
    buyer_email: "test@gmail.com",
    buyer_name: "tester", // 구매자 이름
    buyer_tel: "010-4242-4242", // 구매자 연락처 
    buyer_addr: "서울특별시 강남구 신사동",// 구매자 주소지
    buyer_postcode: "01181", // 구매자 우편번호
    m_redirect_url : 'https://example.com/mobile/complete', // 모바일 결제시 사용할 url
    digital: true, // 실제 물품인지 무형의 상품인지(핸드폰 결제에서 필수 파라미터)
    app_scheme : '' // 돌아올 app scheme
  }

 

 

 

 

4. 고객이 결제를 완료한 후 실행되는 함수(callback) 추가

IMP.request_pay(param, callback)에서 두 번째 인자 callback은 고객이 결제 완료 후 실행되는 함수입니다. callback함수로 전달되는 rsp인자에는 결제의 성공 여부, 결제 정보, 에러 정보 을 가지고 있습니다. 고객의 결제 프로세스 완료 후 rsp인자를 통해 결과를 확인하고, 확인 이후에 수행하고자 하는 작업을 callback에서 작성합니다.

 // 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 {
        ...,
        // 결제 실패 시 로직,
        ...
    }
  });

 

 

 

 

5. 콜백 함수에서 쿼리 파라미터 전달하기

callback에서 결제가 성공하면 rsp의 imp_uid와 merchant_uid를 가맹점 서버에 인자로 전달합니다. 추가적으로 전달할 데이터가 있으면 data: 부분에 넣으시면 됩니다. 위의 인자를 전달하여 결제 위변조 검증 절차를 진행합니다.

  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);
    }
  });

 

 

 

 

6. 가맹점 서버에서 거래 검증 및 데이터 동기화

결제가 완료되면 가맹점 서버에서 해당 거래가 결제금액이 위변조 되었는지 검증한 후 거래 데이터를 가맹점의 서비스의 데이터베이스에 저장하여 데이터를 동기화시킵니다.

 

엑세스 토큰 발급 -> 아임포트 서버에서 결제 정보 조회 -> 가맹점 db에서 결제되어야 하는 금액 조회 -> 결제 검증 -> 검증이 유효하면 가맹점 db에 결제 정보 저장(위변조 결제가 감지될 경우 예외처리 작성)

  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; // 조회한 결제 정보
      
      // 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);
    }
  });

 

 

 

7. 서버 응답 처리하기

클라이언트로 돌아와서, "4. 고객이 결제를 완료한 후 실행되는 함수(callback)"에 대한 코드를 작성합니다. 위변조 검증을 거친 후 해당 응답을 어떻게 작성할지 고민하면 됩니다.

  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);
    }
  });

 

 

8. 전체 코드

위에서 설명한 코드를 첨부합니다. 클라이언트 측 코드와 가맹점 서버 코드로 나뉘어 있습니다. 코드의 파라미터와 url 부분 등 본인 서비스에 맞게 바꿔서 사용하시면 됩니다.

클라이언트측 코드

  IMP.request_pay({
    pg: "html5_inicis", // PG사 선택
    pay_method: "card", // 지불 수단
    merchant_uid: 'merchant_' + new Date().getTime(), //가맹점에서 구별할 수 있는 고유한id
    name: "맥북 프로 16인치", // 상품명
    amount: 2500000, // 가격
    buyer_email: "test@gmail.com",
    buyer_name: "tester", // 구매자 이름
    buyer_tel: "010-4242-4242", // 구매자 연락처 
    buyer_addr: "서울특별시 강남구 신사동",// 구매자 주소지
    buyer_postcode: "01181", // 구매자 우편번호
    m_redirect_url : 'https://example.com/mobile/complete', // 모바일 결제시 사용할 url
    digital: true, // 실제 물품인지 무형의 상품인지(핸드폰 결제에서 필수 파라미터)
    app_scheme : '' // 돌아올 app scheme
  }, 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) { // 응답 처리
          switch(data.status) {
            case: "vbankIssued":
              // 가상계좌 발급 시 로직
              break;
            case: "success":
              // 결제 성공 시 로직
              break;
          }
        });
    } else {
      alert("결제에 실패하였습니다. 에러 내용: " +  rsp.error_msg);
    }
  });
가맹점서버 코드

  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; // 조회한 결제 정보
      
      // 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);
    }
  });

 

반응형