Back To Articles

스트라이프 구독 결제 서비스 구현

Written by Sangmin on June 29, 2024

Article Image

스트라이프 구독 시스템 구축하기

페이팔로 구독 서비스를 구축한적이 있는데 실제 상황에서 사용할때 제한된 부분이 좀 있어서 결국 스트라이프로 다시 구현하기로 했다.

이번 포스트에서는 스트라이프 API와 웹훅을 활용해 구독 결제 시스템을 만드는 방법을 알아보겠다.

기본 세팅

우선 한국계좌로는 스트라이프를 사용할 수 없다. 이걸 우회하는 방법은 호주계좌를 만들어서 등록하는 것이다. 페이오니아에서 호주계좌를 만든 후 이 계좌로 스트라이프 인증을 하면 된다. 계좌 등록을 안해도 테스트 모드로 우선 개발을 해놓을 수 있으니 신청을 해놓고 개발을 하면 될 듯 하다.

테스트 단계에서는 카드번호를 4242 4242 4242 4242로 넣고 테스트하면 된다. 다른 값들은 아무거나 넣어도 상관없다. 승인거절의 상황을 테스트하고 싶으면 4000 4000 4000 0002 로 넣으면 된다. 여러 상황에 대한 카드번호들이 다큐멘테이션에 나와있으니 필요하면 찾아보는 걸 추천한다.

결제 구현

먼저 스트라이프 체크아웃 세션을 생성하는 API 라우트를 만들었다. 이 라우트는 프론트엔드에서 호출되며, 가격 ID와 사용자 ID를 전달받는다.

import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(request) {
  try {
    const body = await request.json();
    const { priceId, customId } = body;

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      success_url: `${request.headers.get('origin')}/profile`,
      cancel_url: `${request.headers.get('origin')}/profile`,
      metadata: {
        customId: customId,
      },
    });

    return NextResponse.json({ sessionId: session.id });
  } catch (error) {
    console.error('Error creating checkout session:', error);
    return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 });
  }
}

이 라우트에서는 스트라이프 체크아웃 세션을 생성하고, 세션 ID를 반환한다. 프론트엔드에서는 이 세션 ID를 사용해 스트라이프 체크아웃 페이지로 리디렉션할 수 있다.

다음으로 스트라이프 웹훅을 처리하는 API 라우트를 만들었다. 이 라우트는 스트라이프에서 이벤트를 받아 처리한다.


export async function POST(req) {
  try {
    const body = await req.json();
    const custom_id = body.data.object.metadata.customId
    const sub_id = body.data.object.subscription

    switch (body.type) {
      case 'customer.subscription.updated':
        const subscription = body.data.object;
        await handleSubscriptionUpdated(subscription, custom_id);
        break;

      case 'checkout.session.completed':
        await handlePaid(custom_id, sub_id);
        break;

      default:
        console.log(`Unhandled event type: ${body}`);
    }

    return NextResponse.json({ status: 'processing' }, { status: 200 });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ message: "Error", err }, { status: 500 });
  }
}

async function handlePaid(userId, subId) {
  try {
 // logic

    return NextResponse.json({ status: 'completed' });
  } catch (error) {
    console.log('Error handling subscription activation:', error);
    throw error;
  }
}

async function handleSubscriptionUpdated(subscription, userId) {
  if (subscription.cancel_at_period_end) {
    try {
      //logic
    } catch (error) {
      console.log('Error handling subscription cancellation:', error);
      throw error;
    }
  } else if (subscription.status === 'active') {
    await updateDatabase(subscription.customer, subscription.id, 'active');
  }
}

이 라우트에서는 웹훅 이벤트의 타입에 따라 다른 로직을 실행한다. checkout.session.completed 이벤트가 오면 handlePaid 함수를 호출해 사용자의 구독 상태를 활성화한다. customer.subscription.updated 이벤트가 오면 handleSubscriptionUpdated 함수를 호출해 구독 상태를 업데이트한다.

마지막으로 프론트엔드에서 구독 버튼을 클릭하면 스트라이프 체크아웃 페이지로 리디렉션되도록 했다.

'use client';

import { Box } from '@mui/material';
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

export default function SubscribeButton({ priceId, userId }) {
  const handleSubscribe = async () => {
    try {
      const stripe = await stripePromise;
      const response = await fetch('/api/checkout-session', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          priceId: priceId,
          customId: userId
        }),
      });
      const { sessionId } = await response.json();
      const result = await stripe.redirectToCheckout({ sessionId });
      if (result.error) {
        console.error(result.error);
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <Box sx={{ display: 'flex', justifyContent: 'center' }}>
      <button onClick={handleSubscribe}
        style={{
          justifyContent: 'center',
          backgroundColor: '#635BFF',
          color: 'white',
          padding: '10px 20px',
          border: 'none',
          borderRadius: '4px',
          fontSize: '16px',
          cursor: 'pointer',
        }}>
        Subscribe</button>;
    </Box>
  )
}

마무리

스트라이프와 페이팔을 둘다 만들어본 결과 스트라이프가 훨씬 개발자 경험이 좋긴하다. 스트라이프는 test clock이라는 것도 지원하는데 쉽게말하면 여러가지 시뮬레이션을 만들어서 테스트할 수 있게 하는 것이다. 다큐멘테이션도 잘나와있고 구현할 수 있는 기능들도 더 많다. 스트라이프가 한국에 진출한다는 소식이 예전부터 있었는데 한국계좌로 등록할 수 있는 날이 빨리 왔으면 좋겠다.