개발

Supabase 무료 티어로 SaaS 만들기 — 한계와 우회법

Supabase 무료 플랜으로 실제 SaaS를 구축하면서 겪은 한계와 이를 우회하는 실전 전략을 공유합니다. 데이터베이스 크기, API 요청, Auth, Storage 제한을 극복하는 방법.

·8 min read
#Supabase#SaaS#무료 티어#백엔드#PostgreSQL

Supabase를 선택한 이유

Supabase 아키텍처

2025년 중반, 간단한 SaaS 프로젝트를 시작하면서 백엔드를 고민했습니다. 선택지는 크게 세 가지였습니다:

  1. 직접 서버 구축 (Node.js + PostgreSQL)
  2. Firebase
  3. Supabase

Firebase는 NoSQL 기반이라 관계형 데이터가 많은 제 프로젝트와 맞지 않았습니다. 직접 서버를 구축하면 인프라 관리에 시간을 뺏기게 되고. 결국 PostgreSQL 기반 + 무료 티어가 넉넉한 Supabase를 선택했습니다.

결론부터 말하면, MVP 단계에서 Supabase 무료 티어는 충분했습니다. 하지만 사용자가 늘면서 몇 가지 벽에 부딪혔고, 이를 우회하는 방법을 찾아야 했습니다.

무료 티어의 실제 한계

Supabase 공식 Pricing에 나오는 무료 플랜 제한:

항목무료 한도
데이터베이스 크기500MB
Storage1GB
대역폭월 5GB
Edge Functions월 50만 호출
Auth MAU50,000명
프로젝트 수2개
일시 정지7일 미접속 시 자동 정지

마지막 항목이 진짜 문제입니다. 7일 동안 API 호출이 없으면 프로젝트가 자동으로 일시 정지됩니다. SaaS를 운영하는데 백엔드가 갑자기 꺼지면 어떻게 될까요? 사용자 입장에서는 서비스 장애입니다.

한계 #1: 자동 정지 우회

가장 간단한 우회법은 크론 잡으로 주기적 핑을 보내는 겁니다:

// Vercel Cron 또는 GitHub Actions 활용
// cron: 0 */6 * * * (6시간마다)
export async function GET() {
  const { data, error } = await supabase
    .from('health_check')
    .select('id')
    .limit(1);
  
  return Response.json({ status: 'alive', timestamp: new Date() });
}

GitHub Actions를 쓰면 완전 무료로 이 크론을 돌릴 수 있습니다. 저는 6시간마다 한 번씩 health check 쿼리를 날립니다.

한계 #2: 500MB 데이터베이스

500MB가 작아 보이지만, 실제로 써보면 텍스트 기반 SaaS에서는 꽤 넉넉합니다. 제 프로젝트의 경우 사용자 5,000명 데이터가 약 120MB였습니다.

하지만 이미지나 파일을 DB에 직접 저장하면 순식간에 차버립니다. 우회법:

이미지는 Storage로, 메타데이터만 DB에

-- ❌ 이렇게 하지 마세요
CREATE TABLE posts (
  id uuid PRIMARY KEY,
  image bytea  -- DB에 이미지 직접 저장
);

-- ✅ 이렇게 하세요
CREATE TABLE posts (
  id uuid PRIMARY KEY,
  image_url text  -- Storage URL만 저장
);

오래된 데이터 아카이빙

활성 데이터만 Supabase에 두고, 6개월 이상 된 데이터는 주기적으로 내보내서 Cloudflare R2 (무료 10GB)에 보관합니다.

// 월 1회 아카이빙 스크립트
const oldData = await supabase
  .from('logs')
  .select('*')
  .lt('created_at', sixMonthsAgo);

// R2에 JSON으로 업로드
await r2.put(`archive/${year}/${month}.json`, JSON.stringify(oldData));

// Supabase에서 삭제
await supabase
  .from('logs')
  .delete()
  .lt('created_at', sixMonthsAgo);

한계 #3: 프로젝트 2개 제한

무료 플랜에서 프로젝트는 2개까지입니다. 개발(dev)과 프로덕션(prod)으로 나누면 딱 2개라 여유가 없습니다.

제 우회법: 하나의 프로젝트에서 스키마로 환경 분리

-- 개발 환경
CREATE SCHEMA dev;
CREATE TABLE dev.users (...);

-- 프로덕션 환경  
CREATE SCHEMA prod;
CREATE TABLE prod.users (...);

RLS(Row Level Security) 정책도 스키마별로 다르게 적용할 수 있어서, 하나의 프로젝트로도 환경 분리가 가능합니다.

한계 #4: Edge Functions 제한

월 50만 호출이 많아 보이지만, 실시간 기능이 있으면 금방 소진됩니다. 우회법:

  • 클라이언트 사이드 로직 최대한 활용: 단순 계산은 브라우저에서
  • Batch 처리: API 호출을 묶어서 한 번에
  • 캐싱: 같은 요청은 클라이언트에서 캐시
// ❌ 매번 호출
const user = await supabase.auth.getUser();

// ✅ 캐시 활용
const getCachedUser = (() => {
  let cache: User | null = null;
  let lastFetch = 0;
  
  return async () => {
    if (cache && Date.now() - lastFetch < 300000) return cache;
    const { data } = await supabase.auth.getUser();
    cache = data.user;
    lastFetch = Date.now();
    return cache;
  };
})();

RLS 설정: 무료 티어의 숨은 강점

Supabase의 Row Level Security는 무료 티어에서도 완전히 사용 가능합니다. 이게 Firebase보다 Supabase를 선택한 결정적 이유입니다.

-- 사용자 본인 데이터만 접근 가능
ALTER TABLE user_data ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own data"
  ON user_data FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can insert own data"
  ON user_data FOR INSERT
  WITH CHECK (auth.uid() = user_id);

서버 사이드 보안 로직을 DB 레벨에서 처리하니, Edge Function 호출을 줄이는 효과도 있습니다.

실전 아키텍처

제가 운영 중인 SaaS의 아키텍처입니다:

[Next.js on Vercel] ← 프론트엔드
       ↓
[Supabase Free Tier]
  ├── PostgreSQL (데이터)
  ├── Auth (인증)
  ├── Storage (파일)
  └── Realtime (웹소켓)
       ↓
[Cloudflare R2] ← 아카이빙
[GitHub Actions] ← 크론/자동화

이 구조로 월 운영비 0원에 사용자 5,000명을 감당하고 있습니다.

유료 전환 타이밍

무료 티어로 버티다가 언제 유료로 갈아타야 할까요? 제 기준:

  • DB 사용량이 400MB 넘으면 (80% 도달)
  • MAU가 30,000 넘으면
  • 수익이 발생하기 시작하면 (Pro 플랜 월 $25는 충분히 감당 가능)

마무리

Supabase 무료 티어는 "무료치고 괜찮은" 수준이 아니라, MVP부터 초기 성장까지 진지하게 쓸 수 있는 레벨입니다. 자동 정지와 용량 제한만 잘 우회하면, 유료 플랜 없이도 꽤 오래 버틸 수 있습니다.

관련 프로젝트들도 참고하세요:


참고 링크:

관련 글