티스토리 뷰

 

React 프로젝트에서 나이스페이 결제 모듈을 연동하는 과정은 기존 서버 렌더링 환경(JSP 등)과 클라이언트 사이드 렌더링(CSR) 환경의 차이로 인해 몇 가지 도전 과제를 포함합니다. 이 글에서는 React 프로젝트에 나이스페이 결제 기능을 성공적으로 연동한 경험과 해결 방법을 공유합니다.

 

💣 문제 상황

React 프로젝트에서 결제 기능을 구현하는 과정에서, 안정적이고 검증된 결제 솔루션을 도입하기 위해 나이스페이 결제 모듈을 선택했습니다. 나이스페이는 국내 주요 결제 수단(CARD, 계좌이체 등)을 지원하며, 높은 보안성과 안정성을 제공함과 동시에 저렴한 수수료로 비용 효율적인 운영이 가능하다는 장점이 있었습니다.

하지만, 나이스페이 모듈이 기존의 서버 렌더링 환경(JSP 등)에 최적화되어 있어 React와 같은 클라이언트 사이드 렌더링(CSR) 환경에 직접 적용하기 위해 추가적인 설정이 필요했습니다.  

 

🛠️ 해결 과정

나이스페이 결제 API를 사용하는 과정에서, 기본적으로 클라이언트 측에서 스크립트를 직접 import해야 하는 제약이 있었습니다. 하지만 React와 같은 CSR 환경에서는 스크립트를 로드하거나 DOM을 조작하는 작업이 비효율적이거나 보안 문제가 발생할 가능성이 있었습니다. 이를 해결하기 위해, 백엔드에서 나이스페이의 스크립트를 받아 처리하는 새로운 API를 구현하였습니다.

1️⃣ 동적으로 폼 생성 및 제출하기

클라이언트에서 결제창 팝업을 호출하게 될때 결제에 필요한 정보를 goPay 함수에 form 데이터 형식에 담아 전달해야합니다. 하여 다음 단계에서는 폼 데이터를 기반으로 <form> 태그를 동적으로 생성하여 DOM에 추가하고, 플랫폼에 따라 폼을 제출하거나 결제 창을 띄우는 함수를 작성해주었습니다.

  const payform = {
      "GoodsName": member.services?.[0].serviceName, // 결제상품명
      "Amt": totalPrice, // 결제상품금액 (프론트)
      "BuyerName": member.name, // 구매자명
      "BuyerTel": member.phoneNumber, // 구매자연락처
      "Moid": reservationId, // 상품주문번호 -> 예약번호로
      "ReturnURL": "https://naver.com", //리턴시키고 싶은거
      "payMethod": "CARD", // 카드 결제만 가능하게끔 (그냥 넣어두기)
      "SignData": data.hashString,
      "EdiDate": data.ediDate,
      "MID": 'nicepay00m', //상점아이디,
      "GoodsCl": 1,
      "TransType": 0,
      "CharSet": 'utf-8',
      "ReqReserved": '',
    };

const createAndSubmitForm = (payform, actionUrl) => {
  const form = document.createElement('form');
  form.method = 'POST';
  form.action = actionUrl;
  form.acceptCharset = 'euc-kr';

  Object.entries(payform).forEach(([key, value]) => {
    const input = document.createElement('input');
    input.type = 'hidden';
    input.name = key;
    input.value = value;
    form.appendChild(input);
  });

  if (checkPlatform() === 'mobile') {
    document.body.appendChild(form);
    form.submit();
  } else {
    window.goPay(form);
  }
};

 

2️⃣ PC와 모바일 환경 구분

NICEPAY 결제모듈은 모바일과 PC 결제창 두 가지 타입으로 호출하는 방식을 구분되어 있는데, PC 환경에서는 동적 스크립트를 로드하여 작동하고, 모바일에서는 전용 URL을 통해 결제 창을 띄웁니다. 이를 처리하는 checkPlatform 함수를 작성했습니다.

이 함수는 navigator.userAgent 값을 기반으로 현재 플랫폼이 모바일인지 PC인지 판별하도록 로직을 분리했습니다.

const checkPlatform = (ua = window.navigator.userAgent) => {
  const lowerUa = ua.toLowerCase();
  const isMobile = /(ipad|ipod|windows phone|iphone|kindle|silk|android|playbook|bb|blackberry)/.test(lowerUa);
  return isMobile ? 'mobile' : 'pc';
};

.
.
.

function nicepayStart(data){

	// 모바일 결제창 여부 판단
	if (checkPlatform(window.navigator.userAgent) == "mobile") {
      document.body.appendChild(form);
      form.submit();
    } else {
      window.goPay(form);
    }
}

 

3️⃣ 결제 데이터 초기화 및 상태 업데이트

useEffect 훅을 사용해 사용자와 결제 정보를 서버에서 로드하고 상태를 초기화합니다.

useEffect(() => {
  const memberInfo = (data) => {
    if (data?.data?.result) {
      setMember(data.data.result);
      // 결제 총 금액 계산
      const total =
        data.data.result.totalServiceFare +
        (data.data.result.pickup?.fare || 0);
      setTotalPrice(total);
    }
  };

  sendGetRequest(
    `/members/payment/page/get/${reservationId}`, // 서버에서 사용자 정보 가져오기
    memberInfo,
    errorMessage
  );
}, [reservationId]);

 

4️⃣ 결제 요청 실행 (onClickHandler)

사용자가 결제 버튼을 클릭하면 결제 창 호출 로직이 실행됩니다.

const onClickHandler = async () => {
  window.location.href = `http://210.180.118.201:8080/payment/click/3?goodsName=${member.services?.[0].serviceName}&price=${totalPrice}&buyerName=${member.name}&buyerTel=${member.phoneNumber}&moid=${reservationId}`;
};

 

🙌 결과 

 

import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useRecoilState } from "recoil";
import { totalPriceState } from "../store/products";
import useHttpRequest from "../hook/use-http";

// 플랫폼 체크 함수: PC와 모바일 환경 구분
const checkPlatform = (ua = window.navigator.userAgent) => {
  const lowerUa = ua.toLowerCase();
  return /(ipad|ipod|windows phone|iphone|kindle|silk|android|playbook|bb|blackberry)/.test(lowerUa)
    ? "mobile"
    : "pc";
};

const PaymentPage = () => {
  const { reservationId } = useParams(); // URL에서 예약 ID 가져오기
  const [totalPrice, setTotalPrice] = useRecoilState(totalPriceState); // 총 결제 금액 상태
  const [member, setMember] = useState({}); // 사용자 정보 상태
  const { sendGetRequest } = useHttpRequest(); // HTTP 요청 훅

  // 나이스페이 결제 시작 함수
  const nicepayStart = (data) => {
    const form = document.createElement("form");
    form.method = "POST";
    form.action = "{base_URL}/payment/check"; // 결제 API URL
    form.acceptCharset = "euc-kr";

    // 결제 데이터 정의
    const payform = {
      GoodsName: member.services?.[0].serviceName || "", // 상품명
      Amt: totalPrice, // 결제 금액
      BuyerName: member.name || "", // 구매자 이름
      BuyerTel: member.phoneNumber || "", // 구매자 연락처
      Moid: reservationId, // 주문번호
      ReturnURL: "https://naver.com", // 결제 완료 후 리디렉션 URL
      payMethod: "CARD", // 결제 방식
      SignData: data.hashString,
      EdiDate: data.ediDate,
      MID: "nicepay00m", // 상점 ID
      GoodsCl: 1,
      TransType: 0,
      CharSet: "utf-8",
      ReqReserved: "",
    };

    // payform 데이터를 기반으로 hidden input 생성
    Object.entries(payform).forEach(([key, value]) => {
      const input = document.createElement("input");
      input.type = "hidden";
      input.name = key;
      input.value = value;
      form.appendChild(input);
    });

    // PC와 모바일 환경에 따라 폼 제출 방식 구분
    if (checkPlatform() === "mobile") {
      document.body.appendChild(form);
      form.submit(); // 모바일 환경: 폼 제출
    } else {
      window.goPay(form); // PC 환경: 나이스페이 결제 창 호출
    }
  };

  // 초기 사용자 및 결제 정보 로드
  useEffect(() => {
    const fetchMemberInfo = (data) => {
      if (data?.data?.result) {
        const user = data.data.result;
        setMember(user);
        const total = user.totalServiceFare + (user.pickup?.fare || 0);
        setTotalPrice(total); // 총 결제 금액 계산
      }
    };

    sendGetRequest(
      `/members/payment/page/get/${reservationId}`, // 서버에서 사용자 정보 가져오기
      fetchMemberInfo,
      (error) => console.error("Error fetching data:", error) // 에러 처리
    );
  }, [reservationId, sendGetRequest, setTotalPrice]);

  // 결제 버튼 클릭 핸들러
  const handlePayment = () => {
    // 서버에서 결제 데이터를 가져와 `nicepayStart` 호출
    sendGetRequest(
      `/payment/initiate/${reservationId}`,
      (response) => {
        if (response.success) {
          nicepayStart(response.data); // 결제 시작
        } else {
          alert("결제 데이터를 가져오는 중 오류가 발생했습니다.");
        }
      },
      (error) => console.error("Error initiating payment:", error)
    );
  };

  return (
    <div>
      <h2>결제 정보</h2>
      <p>구매자 이름: {member.name}</p>
      <p>총 결제 금액: {totalPrice?.toLocaleString()}원</p>
      <button onClick={handlePayment}>결제하기</button>
    </div>
  );
};

export default PaymentPage;
  • nicepayStart: 결제 데이터를 폼에 담아 제출하는 핵심 로직.
  • checkPlatform: PC/모바일 환경 구분 처리.
  • useEffect: 초기 사용자 및 결제 정보 로드.
  • onClickHandler: 사용자의 결제 요청 버튼 동작 처리.

이러한 로직을 통해 나이스페이 결제 모듈을 React 환경에 효과적으로 연동할 수 있습니다.

 

 

👍 마무리 

React에서 나이스페이와 같은 서버 렌더링 기반 결제 모듈을 연동하는 과정은 기술적인 도전 과제를 포함하지만, 이를 해결하면 CSR 환경에서도 안정적인 결제 시스템을 구축할 수 있습니다. 앞으로 유사한 결제 기능을 구현할 때 이 경험이 도움이 되길 바랍니다.