payment-integration
๐ณ Next.js ๊ฒฐ์ ์์คํ ํตํฉ ์คํฌ
์ด ์คํฌ์ BSD ๋ฐ์ด๋ธ์ฝ๋ฉ ์๊ฐ์๋ค์ด Next.js์ Node.js๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํ๊ณ ์ ๋ฌธ์ ์ธ ๊ฒฐ์ ์์คํ ์ ๊ตฌ์ถํ ์ ์๋๋ก ๋์ต๋๋ค.
๐ ์ด ์คํฌ์ด ํ๋ ์ผ
- ํ๋ซํผ ์ ๋ต: ๋น์ฆ๋์ค ๋ชจ๋ธ(์ผํ์ฑ/๊ตฌ๋ )์ ๋ง๋ ์ต์ ์ PG์ฌ(Toss, Stripe) ์ ์
- Next.js ํตํฉ: ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๊ฒฐ์ ์์ฒญ ๋ฐ ์๋ฒ ์ฌ์ด๋ ์น์ธ ๋ก์ง ๊ตฌํ
- ์นํ (Webhook) ์ฐ๋: ๊ฒฐ์ ์๋ฃ, ์ทจ์ ๋ฑ ์ด๋ฒคํธ๋ฅผ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ์ฌ DB ๋๊ธฐํ
- ๋ณด์ ๋ฐ ๊ฒ์ฆ: ๋ฐ์ดํฐ ์๋ณ์กฐ ๋ฐฉ์ง ๋ฐ ์๋ฒ ์ฌ์ด๋ ๊ธ์ก ๊ฒ์ฆ ๋ก์ง ๊ตฌํ
- ๊ตฌ๋ ๊ด๋ฆฌ: Stripe Billing ๋ฑ์ ํ์ฉํ ์๋ ๊ฒฐ์ ๋ฐ ๋ฉค๋ฒ์ญ ์ฐ๋
๐ฏ ์ธ์ ์ด ์คํฌ์ ์ฌ์ฉํ๋์?
- "Next.js ์ผํ๋ชฐ์ ํ ์คํ์ด๋จผ์ธ ์ ์ ฏ(Payment Widget)์ ๋ฃ๊ณ ์ถ์ด์"
- "Stripe๋ฅผ ์ฌ์ฉํด ๊ธ๋ก๋ฒ ๊ตฌ๋ ์๋น์ค๋ฅผ ๋ง๋ค๊ณ ์ถ์ด์"
- "๊ฒฐ์ ๊ฐ ์๋ฃ๋๋ฉด ์๋์ผ๋ก ์๋ฆผํก์ ๋ณด๋ด๊ณ ์๊ฐ ๊ถํ์ ์ด์ด์ฃผ๊ณ ์ถ์ด์"
๐ ๏ธ ์ถ์ฒ ๊ธฐ์ ์คํ
1. Payment Gateways
- Toss Payments: ๊ตญ๋ด ๊ฒฐ์ ์ต๊ฐ์, ์์ ฏ ๊ธฐ๋ฐ์ ๊ฐํธํ ์ฐ๋
- Stripe: ๊ธ๋ก๋ฒ ๋ฐ ๊ตฌ๋ ๊ฒฐ์ ํ์ค
2. Implementation
- Next.js App Router: API Route Handlers๋ฅผ ํ์ฉํ ๊ฒฐ์ ์น์ธ
- Server Actions: ๋ณด์์ด ๊ฐํ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
๐ป ๊ตฌํ ์์ (Next.js + Toss Payments)
1. Server Side (app/api/payment/confirm/route.ts)
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { paymentKey, orderId, amount } = await req.json();
// 1. ์๋ฒ ์ฌ์ด๋ ๊ธ์ก ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ ํ์
// const product = await db.product.findUnique({ where: { orderId } });
// if (product.price !== amount) throw new Error('Invalid amount');
const secretKey = process.env.TOSS_SECRET_KEY;
const basicToken = Buffer.from(`${secretKey}:`).toString("base64");
const response = await fetch(
"https://api.tosspayments.com/v1/payments/confirm",
{
method: "POST",
headers: {
Authorization: `Basic ${basicToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ paymentKey, orderId, amount }),
}
);
const result = await response.json();
if (response.ok) {
// DB ์
๋ฐ์ดํธ ๋ฑ ํ์ฒ๋ฆฌ ์ํ
return NextResponse.json(result);
} else {
return NextResponse.json(result, { status: response.status });
}
}
2. Client Side (components/Checkout.tsx)
"use client";
import { loadTossPayments } from "@tosspayments/payment-sdk";
export default function Checkout() {
const handlePayment = async () => {
const tossPayments = await loadTossPayments(
process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY!
);
await tossPayments.requestPayment("์นด๋", {
amount: 50000,
orderId: "ORDER_123",
orderName: "Next.js ๋ง์คํฐํด๋์ค",
successUrl: `${window.location.origin}/success`,
failUrl: `${window.location.origin}/fail`,
});
};
return <button onClick={handlePayment}>50,000์ ๊ฒฐ์ ํ๊ธฐ</button>;
}
โ ๏ธ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
- ๊ธ์ก ๊ฒ์ฆ: ๋ฐ๋์ ์๋ฒ ์ฌ์ด๋์์ ์ค์ DB์ ๊ฐ๊ฒฉ๊ณผ ๋น๊ต ๊ฒ์ฆ
- API Key ๊ด๋ฆฌ: Secret Key๋ ์ ๋ ํด๋ผ์ด์ธํธ์ ๋
ธ์ถํ์ง ๋ง๊ณ
.env๋ก ๊ด๋ฆฌ - ๋ฉฑ๋ฑ์ฑ(Idempotency): ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง๋ฅผ ์ํ
Idempotency-Keyํ์ฉ
๐ฌ ์์ ๋ํ
์ฌ์ฉ์: "ํ ์คํ์ด๋จผ์ธ ๋ก ์ ๊ธฐ ๊ฒฐ์ ๊ธฐ๋ฅ์ Next.js์ ๋ฃ๊ณ ์ถ์ด"
Claude: "Next.js App Router์ ํ ์คํ์ด๋จผ์ธ ๋น๋ง API๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํ ์ ๊ธฐ ๊ฒฐ์ ์์คํ ์ ๊ตฌ์ถํด๋๋ฆฌ๊ฒ ์ต๋๋ค. ์นด๋ ๋ฑ๋ก๋ถํฐ ์์ฝ ๊ฒฐ์ ๋ก์ง, ๊ทธ๋ฆฌ๊ณ ๊ฒฐ์ ๊ฒฐ๊ณผ ์นํ ์ฒ๋ฆฌ๊น์ง ํฌํจ๋ ์ฝ๋๋ฅผ ์์ฑํด ๋๋ฆด๊ฒ์..."
๐ฏ ํต์ฌ ์ ๋ฆฌ
์ด ์คํฌ์ ์ฌ์ฉํ๋ฉด: โ ์์ ํ ๊ฒฐ์ ์ํคํ ์ฒ: ์๋ฒ ์ฌ์ด๋ ๊ฒ์ฆ์ด ํฌํจ๋ ๊ฒฌ๊ณ ํ ์์คํ โ ์ต์ ์ฐ๋ ๋ฐฉ์: Toss UI Widget, Stripe Checkout ๋ฑ ์ต์ ํธ๋ ๋ ๋ฐ์ โ ๋น์ฆ๋์ค ํ์ฅ์ฑ: ์ ๊ธฐ ๊ตฌ๋ , ๋ถ๋ถ ์ทจ์, ์์คํฌ๋ก ๋ฑ ๋ณต์กํ ๊ธฐ๋ฅ ์ง์ โ ์์ ์ ์ธ ์ด์: ์นํ ์ฒ๋ฆฌ๋ฅผ ํตํ ๋ฐ์ดํฐ ๋ถ์ผ์น ๋ฌธ์ ํด๊ฒฐ
BSD ํ์์ด๋ผ๋ฉด: ์ด์ ๋จ์ํ ์น์ฌ์ดํธ๋ฅผ ๋์ด, ์ค์ ์์ต์ด ๋ฐ์ํ๋ ๋น์ฆ๋์ค ํ๋ซํผ์ ์ง์ ๊ฐ๋ฐํ ์ ์์ต๋๋ค! ๐ณ