4주차_ "나만의 MVP 앱 구현해 보기" 에서 결재창 붙이고 DB에 기록해보기 <- 물흐르듯이

소개

갤럭시 워치 전용 AI 러닝 코치 앱 "RunCoach" 의 랜딩페이지에 결제 기능을 붙이고, 실제 구매 이력을 DB에 저장하는 것까지 구현했습니다.

시도한 이유: 토요일 스터디 발표용으로 단순 UI 시연이 아닌, 실제로 결제가 되고 DB에 이력이 쌓이는 완성형 서비스 를 보여주고 싶었습니다.


진행 방법

사용 도구

  • Claude Code — 코드 작성, 오류 분석, Vercel CLI 명령어 실행

  • 토스페이먼츠 — 한국형 결제 SDK

  • Supabase — 결제 이력 저장 DB

  • Vercel — 서버리스 배포

  • Next.js App Router — API Route로 서버리스 함수 구현


1단계: 토스페이먼츠 연동

사용한 프롬프트:

@toss/tosspayments-dev-docs.md 이 문서를 참조해서 결제 기능을 추가하려고 합니다. Vercel에서 배포하므로 별도 서버 없이 Next.js API Route로 구현, 결제 시 secret key가 노출되지 않아야 합니다. 먼저 plan을 작성해주세요.

Claude가 기존 코드를 분석해서 보안 취약점을 발견했습니다.

발견된 취약점: URL 파라미터의 amount를 그대로 Toss API에 전달 → 공격자가 ?amount=1로 조작 가능

수정한 코드 (src/app/api/payment/route.ts):

const EXPECTED_AMOUNT = 9900  // 서버에 하드코딩

// 1. 사전 검증 (Toss 호출 전)
if (Number(amount) !== EXPECTED_AMOUNT) {
  return NextResponse.json({ error: '결제 금액이 올바르지 않습니다.' }, { status: 400 })
}

// 2. Idempotency-Key 추가 (중복 결제 방지)
headers: {
  'Idempotency-Key': orderId,
}

// 3. Toss 응답 금액 재검증 (이중 검증)
if (data.totalAmount !== EXPECTED_AMOUNT) {
  return NextResponse.json({ error: '결제 금액 불일치' }, { status: 400 })
}

2단계: 401 에러 해결

결제 버튼 클릭 시 401 Unauthorized 에러 발생.

원인: .env.local결제위젯 연동 키가 들어있었는데, 코드는 API 개별 연동 방식 (payment())을 사용 중 → 키 종류 불일치

해결: 토스 개발자센터에서 API 개별 연동 키 재발급 후 교체


3단계: Supabase 결제 이력 저장

SQL로 테이블 생성:

create table payments (
  id           uuid primary key default gen_random_uuid(),
  order_id     text not null unique,
  payment_key  text not null,
  amount       integer not null,
  method       text,
  status       text not null default 'DONE',
  paid_at      timestamptz,
  created_at   timestamptz default now()
);

Supabase 클라이언트 (src/lib/supabaseAdmin.ts):

import { createClient } from '@supabase/supabase-js'

export function getSupabaseAdmin() {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL
  const key = process.env.SUPABASE_SERVICE_ROLE_KEY
  if (!url || !key) return null
  return createClient(url, key)
}

결제 완료 후 DB 저장 (route.ts에 추가):

const supabase = getSupabaseAdmin()
if (supabase) {
  const { error } = await supabase.from('payments').upsert(
    {
      order_id:    data.orderId,
      payment_key: data.paymentKey,
      amount:      data.totalAmount,
      method:      data.method,
      status:      data.status,
      paid_at:     data.approvedAt,
    },
    { onConflict: 'order_id' }
  )
}

4단계: Vercel 배포

npx vercel --prod

환경변수는 Vercel CLI로 추가:

echo "값" | npx vercel env add 변수명 production

결과와 배운 점

결과

  • 실제 결제 동작 확인 (카카오페이 테스트)

  • Supabase payments 테이블에 결제 이력 자동 저장 확인

  • Vercel 프로덕션 배포 완료: https://deploy-iota-opal.vercel.app

시행착오

  1. 401 에러: 토스 키가 2종류(결제위젯/API 개별)인 줄 몰랐음 → 문서를 꼼꼼히 읽어야 함

  2. Vercel 환경변수 충돌: 기존 다른 프로젝트 키가 .env.local을 덮어씌움 → vercel env rm 후 재등록

  3. src/lib 파일 누락: route.ts에서 import하는 파일이 없어 빌드 에러 → 파일 생성 후 해결

배운 꿀팁

  • 결제 금액은 무조건 서버에 하드코딩 — URL 파라미터 절대 신뢰 금지

  • Idempotency-Key 설정하면 네트워크 오류로 재시도해도 중복 결제 안 됨

  • Supabase SERVICE_ROLE_KEY는 서버 전용 — 절대 NEXT_PUBLIC_ 붙이면 안 됨

  • DB 저장 실패해도 결제 자체는 성공 처리 (사용자 경험 보호)

앞으로의 계획

  • 구매자 이메일 수집 → 이용권 발급 알림 자동화

  • 라이브 키로 교체 후 실제 판매 시작


도움 받은 글

  • 토스페이먼츠 공식 개발 문서: tosspayments-dev-docs.md

  • 스터디장님 손수만드신 100x 매뉴얼문서 와 멱살캐리 전달사항등~

간만에 코딩맛이 나던 하루였었습니다.~~~ 앞으로 좋은 강의 잘부탁드려요!

  • SuperBase에 결재한 것이 잘 저장됨을 확인하였다.

Google Analytics 대시보드의 스크린샷
1
3개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요