복잡한뇌구조마냥

[Next.js] 토스페이먼츠 결제 위젯 연동하기 (+ 사업자번호 없이 ) 본문

FE/Next.js

[Next.js] 토스페이먼츠 결제 위젯 연동하기 (+ 사업자번호 없이 )

지금해냥 2025. 10. 22. 00:36

프로젝트에서 토스페이먼츠 결제를 붙이면서 정리한 내용입니다.
Next.js(App Router) + 토스 페이먼츠 결제 위젯 SDK 기반으로 구현했습니다.
사업자번호 없이도 테스트 가능한 방법도 함께 정리했습니다.


1. 🧾 들어가며 — 왜 토스 페이먼츠를 사용했나?

프리랜서 매칭 플랫폼을 개발하면서, 의뢰자가 프리랜서에게 비용을 지불하는 결제 기능이 필요했다.
여러 PG를 검토했지만 위젯 방식 + 문서 품질 + 간단한 테스트 환경 때문에 결국 토스 페이먼츠(TossPayments) 를 선택했다.

특히 좋았던 점은:

  • 사업자 등록이 없어도 테스트 결제 전 과정이 가능
  • React 기반 프로젝트에서 매우 쉽게 적용 가능
  • payment-widget-sdk로 UX 좋은 결제 UI를 그대로 가져올 수 있음
  • Next.js App Router에서도 문제 없이 동작

2. 🧰 사용한 라이브러리

Next.js 클라이언트 컴포넌트에서 아래 두 패키지를 사용했다.

 
"@tosspayments/payment-widget-sdk": "^0.12.0",
"@tosspayments/tosspayments-sdk": "^2.4.0"

역할은 다음과 같다:

패키지설명
payment-widget-sdk 결제 UI 위젯 렌더링용 SDK
tosspayments-sdk 결제 요청(requestPayment) 처리용 SDK

3. 🧪 사업자 번호 없이 토스 결제를 테스트하는 방법

토스페이먼츠는 개발자용 테스트 계정만 있으면 누구나 결제 테스트를 진행할 수 있다.

테스트 환경 특징은 다음과 같다:

✔️ 1) 테스트용 key만 있으면 모든 기능 사용 가능

  • test_client_api_key
  • test_secret_api_key

테스트 키는 절대 돈이 빠져나가지 않는다.
즉, 진짜 카드번호 입력해도 결제가 되지 않는다 ✔️

✔️ 2) 테스트 카드번호 제공

결제 테스트는 카드사별로 제공되는 테스트 카드번호를 사용한다.
모든 승인/실패 상황을 시뮬레이션할 수 있다.

예:

  • 성공 케이스
  • 잔액 부족
  • 한도 초과
  • 도난 카드
  • OTP 인증 실패

등을 카드 번호만 바꿔가며 재현 가능하다.

✔️ 3) 실제 결제 페이지와 동일한 UI 제공

테스트 환경에서도 실제 서비스와 동일한 결제 페이지가 뜬다.

이 덕분에 UX 테스트도 손쉽게 가능했다.


4. 🚀 Next.js에서 토스 결제 위젯 연동하기

아래는 내가 실제로 프로젝트에 적용했던 구조다.

구성은 크게 3단계:

  1. 결제 위젯 로드 및 렌더링
  2. 결제 버튼에서 requestPayment 호출
  3. 결제 성공/실패 페이지 이동

5. 🧩 Step 1 — 결제 위젯 로딩 컴포넌트 (TossPayments.tsx)

이 컴포넌트는 위젯을 로딩하고, UI를 특정 DOM에 렌더링한다.

  • loadTossPayments(clientKey)로 SDK를 로드
  • 위젯 인스턴스 생성 후 결제·약관 영역에 렌더링
  • 금액 변경 시 위젯도 다시 업데이트
 
"use client";

import { useEffect } from "react";
import { loadTossPayments } from "@tosspayments/payment-widget-sdk";
import { useTossWidgetStore } from "@/store/payment-store";

const clientKey = process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY!;
const customerKey = "ANONYMOUS";

export default function TossPayments({ amount }: { amount: number }) {
  const { setWidgets, setReady, widgets } = useTossWidgetStore();

  useEffect(() => {
    let mounted = true;

    (async () => {
      const tossPayments = await loadTossPayments(clientKey);
      if (!mounted) return;

      const widgetsInstance = tossPayments.widgets({ customerKey });
      setWidgets(widgetsInstance);
    })();

    return () => {
      mounted = false;
      setWidgets(null);
      setReady(false);
    };
  }, [setWidgets, setReady]);

  useEffect(() => {
    if (!widgets) return;

    (async () => {
      await widgets.setAmount({ currency: "KRW", value: amount });

      await widgets.renderPaymentMethods({
        selector: "#payment-method",
        variantKey: "DEFAULT",
      });

      await widgets.renderAgreement({
        selector: "#agreement",
        variantKey: "AGREEMENT",
      });

      setReady(true);
    })();
  }, [widgets, amount, setReady]);

  return (
    <div>
      <div id="payment-method" />
      <div id="agreement" />
    </div>
  );
}

핵심 포인트

  • 금액 변경 시 항상 setAmount → 렌더링 순서로 진행해야 한다
  • 클라이언트 컴포넌트에서만 실행해야 함 ("use client" 필수)
  • useEffect로 위젯 초기화/재렌더링 관리

6. 🧨 Step 2 — 결제 버튼 (TossPaymentsButton.tsx)

사용자가 금액 확인 후 “결제하기”를 누르면 실행된다.

  • 서버에 먼저 orderId 생성 요청
  • 이후 widgets.requestPayment() 호출
  • 결제 성공 시 successUrl로 이동
 
"use client";

import { useTossWidgetStore } from "@/store/payment-store";
import { crypto } from "crypto";

export default function TossPaymentsButton({ freelancer, qty }) {
  const { widgets, ready } = useTossWidgetStore();

  const handlePayment = async () => {
    if (!widgets || !ready) return;

    const orderId = crypto.randomUUID();

    await widgets.requestPayment({
      method: "CARD",
      amount: { currency: "KRW", value: freelancer.salary * qty },
      orderId,
      orderName: `${freelancer.title} ${qty}건`,
      successUrl: `${window.location.origin}/payment/success?orderId=${orderId}`,
      failUrl: `${window.location.origin}/payment/fail?orderId=${orderId}`,
    });
  };

  return (
    <button disabled={!ready} onClick={handlePayment}>
      결제하기
    </button>
  );
}

핵심 포인트

  • widgets.requestPayment()가 실제 결제 창을 띄움
  • 반드시 서버에서 같은 orderId로 금액 검증이 필요함
  • URL은 반드시 배포 환경 기준으로 작성해야 함

7. 🧾 Step 3 — 금액 계산 + 결제 페이지 구성

수량(qty)에 따라 금액을 계산 → 그 값을 TossPayments에 전달 → 위젯이 해당 금액으로 다시 렌더링됨.

사용자가 UI에서 즉시 변경 사항을 확인할 수 있어 경험이 매우 자연스럽다.


8. ✔️ 실제 서비스에서 유의해야 할 점

1) 프론트 계산 금액을 절대 그대로 신뢰하면 안 됨

결제 검증은 반드시 서버에서 해야함

렌더링 전에 결제 금액을 위젯에 전달해야함.

2) 테스트 키 관리

- 테스트용 키 사용하면 됨.

NEXT_PUBLIC_TOSS_CLIENT_KEY=API 키
NEXT_PUBLIC_TOSS_SECRET_KEY=시크릿 키

3) successUrl에서 서버 검증 로직 수행

  • orderId
  • amount
  • paymentKey
    등을 조합해 서버에서 승인 요청 후 DB에 저장

4) 재결제 / 중복 승인 대비 필요


✨ 마무리

Next.js에서 토스 페이먼츠 결제를 연동하는 과정은 생각보다 간단했고,
테스트 환경도 훌륭해서 사업자 등록 전에도 기능 개발·QA를 충분히 할 수 있었던 점이 정말 좋았다.

 

👉 참고 문서:
🔗 사업자 없이 토스페이먼츠 테스트하는 법
https://docs.tosspayments.com/blog/how-to-test-toss-payments

 

회원가입, 사업자번호 없이 결제 테스트하기 | 토스페이먼츠 개발자센터

오늘은 계약 전에 토스페이먼츠의 테스트 환경에서 온라인 결제를 연동하고 시뮬레이션하는 방법을 쉽고 간단하게 소개할게요.

docs.tosspayments.com

 

🔗 결제 위젯 공식 문서
https://docs.tosspayments.com/sdk/v2/js#%EA%B2%B0%EC%A0%9C%EC%9C%84%EC%A0%AF

 

토스페이먼츠 JavaScript SDK | 토스페이먼츠 개발자센터

토스페이먼츠 JavaScript SDK를 추가하고 메서드를 사용하는 방법을 알아봅니다.

docs.tosspayments.com

 

LIST

'FE > Next.js' 카테고리의 다른 글

[Next.js] Supabase 연동하기  (0) 2025.07.14