Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Upstash가 PlanTripAI의 급성장을 이끄는 방법: 핵심 전략과 성공 사례

이 글에서는 AI 기반 여행 일정 계획 스타트업 PlanTripAI의 개발 과정에서 Upstash가 수행한 핵심적인 역할을 살펴봅니다. Upstash Redis는 사용자 접근 권한을 위한 라이선스 키 저장, 여행 콘텐츠의 효율적인 저장 및 캐싱과 같은 필수 기능을 관리하는 데 결정적인 역할을 했습니다. 또한 Upstash의 Rate Limit 기능은 시스템 보호에 중요한 역할을 합니다. 요청 빈도를 효과적으로 관리하여 잠재적인 보안 위협으로부터 강력하게 보호하고, 사용자에게 끊김 없는 원활한 서비스를 제공합니다.

plantripai.com은 목적지, 체류 기간, 여행 스타일, 예산 등 사용자의 선호도를 기반으로 맞춤형 여행 일정을 빠르게 생성해 주는 AI 여행 플래너입니다. 무제한 일정 생성과 다양한 다운로드 형식을 지원합니다.

  • @upstash/redis
  • @upstash/ratelimit
  • next.js

작동 방식

AI 기업들은 일반적으로 사용자가 제품을 체험할 수 있는 다양한 방법을 제공합니다. PlanTripAI에서는 프리미엄(Freemium) 플랜과 유료 플랜을 모두 운영하고 있습니다.

가장 인기 있는 프리미엄 플랜에서는 유효한 라이선스 없이 사용자가 여행 일정을 생성할 때마다 새로운 랜덤 키가 생성되어 Redis 해시 (redis.hset(trip:${key}, data))에 여행 콘텐츠와 함께 저장됩니다. 이 키는 해당 여행 상세 페이지에 대한 임시 접근 권한을 부여하며, 접근 권한이 만료되면 사용자는 해당 여행 일정에 대한 영구 접근 권한을 유지할지 여부를 직접 결정할 수 있습니다.

또한 프리미엄 사용자에게는 10초당 3회 요청으로 속도 제한(fixedWindow)이 적용되어 애플리케이션에 대한 잠재적 공격을 사전에 차단합니다.

Upstash가 PlanTripAI의 급성장을 이끄는 방법: 핵심 전략과 성공 사례

유료 플랜의 경우, 사용자는 결제 게이트웨이를 통해 제품을 구매합니다. 결제가 성공적으로 완료되면 결제 게이트웨이의 웹훅(webhook)이 성공 이벤트를 발생시키고, 해당 키가 유효한 라이선스 키로 Redis에 저장됩니다 (redis.set(${licenseKey}, true)).

유효한 라이선스를 보유한 사용자가 여행 일정을 생성하면 마찬가지로 랜덤 키가 생성되어 Redis에 저장됩니다. 아울러 새로 생성된 여행 일정은 사용자 객체에 추가되고 해당 사용자의 라이선스 키와 연결됩니다.

유료 사용자는 프리미엄 사용자보다 더 넉넉한 속도 제한 혜택을 받습니다. 이 향상된 기능은 Redis에서 유효한 키 존재 여부를 확인하는 방식으로 동적으로 적용됩니다.

Upstash가 PlanTripAI의 급성장을 이끄는 방법: 핵심 전략과 성공 사례

여행 데이터 저장

PlanTripAI는 사용자가 생성한 여행 일정을 저장하기 위해 Redis를 활용합니다. Redis는 빠른 키-값(key-value) 저장소로서 뛰어난 효율성을 자랑하기 때문에 우리에게 가장 이상적인 선택이었습니다. 한 번 생성된 여행 일정은 변경되지 않으며(immutable), 해시(hash) 구조를 활용하면 키 데이터와 함께 메타데이터도 손쉽게 저장할 수 있습니다.

여행 데이터는 Redis 해시에 다음과 같은 구조로 저장됩니다:

{
 "itinerary": [
 { "day": "Day 1", "data": [...] }
 ],
 "info": "This itinerary is designed for a city explorer visiting Paris, France for 2 days.",
 "inputs": {
 "city": "Paris, France",
 "days": 2,
 "accommodation": "Paris France Hotel",
 "kind": "city explorer",
 "currency": "USD",
 "budget": 3000,
 "transportation": "bus"
 },
 "createdAt": ...,
 "shareable": false
}

HTTP 기반의 Upstash Redis API를 사용하면 데이터가 무료 Redis 데이터베이스에 저장됩니다. 이 데이터베이스는 하루 10,000건의 요청을 무료로 제공하며, 우리의 서비스 규모에는 그 이상으로 충분한 용량입니다.

import { Redis } from "@upstash/redis";
 
const redis = new Redis({
 url: "..." // UPSTASH_REDIS_REST_URL
 token: "..." // UPSTASH_REDIS_REST_TOKEN
});
 
redis.hset(`trip:${id}`, data);

유효한 라이선스 키 저장

앞서 언급했듯이, PlanTripAI의 모든 기능에 접근하는 한 가지 방법은 유료 구독입니다. 유료 사용자에게는 각각 유효한 라이선스 키가 할당되며, 이 키는 Redis에 저장됩니다. Redis는 키-값 데이터베이스로서 처리 속도가 매우 빠르기 때문에 실시간 검증 확인에 최적화된 환경입니다.

사용자를 유효한 회원으로 등록하려면 다음 명령을 실행합니다:

import { Redis } from "@upstash/redis";
 
const redis = new Redis({
 url: "..." // UPSTASH_REDIS_REST_URL
 token: "..." // UPSTASH_REDIS_REST_TOKEN
});
 
await redis.set(licenseKey, true);

사용자의 라이선스 유효성을 확인할 때는 다음 명령을 사용합니다:

import { Redis } from "@upstash/redis";
 
const redis = new Redis({
 url: "..." // UPSTASH_REDIS_REST_URL
 token: "..." // UPSTASH_REDIS_REST_TOKEN
});
 
const redisLicenseKey = await redis.get(licenseKey);
const valid = Boolean(redisLicenseKey);
 
if (!valid) {
 return new Response(
 JSON.stringify({ message: "The license key is invalid!" }),
 {
 status: 401,
 }
 );
}
 
// successful code here...
 

속도 제한(Rate Limit) 구현

애플리케이션을 보호하는 핵심 기능은 바로 Upstash Rate Limit이 지원하는 속도 제한 로직입니다. 자세한 내용은 공식 문서에서 확인할 수 있습니다. 이 서비스는 무료 사용자와 유료 사용자 모두에게 끊김 없는 경험을 제공하는 데 필수적인 요소로, 전체 애플리케이션 성능에 영향을 주지 않으면서 악의적인 사용자의 공격을 성공적으로 차단해 왔습니다.

아래는 next.js를 미들웨어(middleware)로 활용한 코드 스니펫입니다. 이 코드는 사용자 IP 검사를 처리하고 후속 요청의 진행 여부를 판단하는 역할을 담당합니다.

import {
 NextResponse,
 type NextFetchEvent,
 type NextRequest,
} from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
 
const cache = new Map();
const ratelimit = new Ratelimit({
 redis: Redis.fromEnv(),
 limiter: Ratelimit.fixedWindow(3, "10s"),
 ephemeralCache: cache,
 analytics: true,
});
 
export default async function middleware(
 request: NextRequest,
 event: NextFetchEvent
): Promise<Response | undefined> {
 const id = request.ip ?? "anonymous";
 
 // optional hard coded IPs
 const blockeds = [];
 if (blockeds.includes(id.trim())) {
 new NextResponse(
 JSON.stringify({
 message: "Blocked :)",
 }),
 { status: 429, headers: { "Content-Type": "application/json" } }
 );
 }
 
 const { success, pending, limit, reset, remaining } = await ratelimit.limit(
 id
 );
 event.waitUntil(pending);
 
 request.headers.set("X-RateLimit-Limit", limit.toString());
 request.headers.set("X-RateLimit-Remaining", remaining.toString());
 request.headers.set("X-RateLimit-Reset", reset.toString());
 
 return success
 ? NextResponse.next()
 : new NextResponse(
 JSON.stringify({
 message:
 "Request cannot be processed! You sent too many requests in a given amount of time.",
 }),
 { status: 429, headers: { "Content-Type": "application/json" } }
 );
}
 
export const config = {
 matcher: ["/api/generate-trip", "/api/get-license/(.*)"],
};

마치며

끝까지 읽어주셔서 감사합니다! PlanTripAI는 SaaS 환경에서 Upstash Redis와 Next.js를 함께 활용하는 방법이 궁금해서 시작된 사이드 프로젝트였습니다.

이 글이 여러분께 유익한 정보와 인사이트를 전달했기를 진심으로 바랍니다. 질문이나 피드백은 언제든 환영합니다. 문의 사항이나 의견이 있으시다면 Twitter를 통해 편하게 연락해 주세요.