DevOps

월 0원으로 SaaS 만들기 — Next.js + Supabase + Vercel 실전기

Next.js 14 App Router, Supabase 무료 플랜, Vercel 무료 호스팅으로 월 호스팅비 0원짜리 SaaS를 실제로 운영한 경험을 공유한다. Supabase pause 방지, AuthContext 무한 리로드 버그 해결까지.

·8 min read
#Next.js#Supabase#Vercel#SaaS#무료호스팅#shadcn/ui#Cloudflare

A developer workspace with multiple monitors showing code and dashboards

들어가며 — 진짜 0원이 가능한가

BioAI Market이라는 프로테오믹스 분석 SaaS를 만들면서 가장 먼저 부딪힌 문제는 비용이었다. 사이드 프로젝트에 매달 수십만 원을 태울 수는 없었다. AWS? 프리 티어 끝나면 청구서 폭탄이 날아온다. 그래서 처음부터 "절대 0원"이라는 제약을 걸고 스택을 골랐다.

결론부터 말하면, 실제로 월 ₩0으로 SaaS를 운영하고 있다. 다만 그 과정이 순탄했냐고 물으면... 전혀 아니었다.

스택 선정 — 무료의 조합

최종 스택은 이렇게 결정됐다:

  • Next.js 14 App Router — React 풀스택 프레임워크
  • Supabase Cloud (Free) — PostgreSQL + Auth + Storage
  • Vercel (Hobby) — 호스팅 + 서버리스 함수
  • shadcn/ui — 컴포넌트 라이브러리 (Tailwind 기반)
  • Cloudflare — DNS + SSL + CDN (무료)

각각의 무료 한계를 정리하면:

서비스무료 한계
Supabase500MB DB, 1GB Storage, 50K MAU, 7일 미접속시 pause
Vercel100GB bandwidth, 서버리스 60초 timeout
Cloudflare무제한 DNS, SSL, CDN

여기서 가장 치명적인 건 Supabase의 7일 pause 정책이었다.

Supabase 7일 pause — keep-alive로 생존하기

Supabase 무료 플랜은 7일간 API 요청이 없으면 프로젝트가 자동 pause된다. pause되면 다시 깨우는 데 수 분이 걸리고, 그 사이 사용자는 빈 화면을 보게 된다. 사이드 프로젝트라 매일 접속하지 않을 수도 있으니 이건 반드시 해결해야 했다.

해결책은 단순했다. 6시간마다 Supabase에 ping을 날리는 cron job을 만들었다.

// app/api/keep-alive/route.ts
import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function GET() {
  const { count, error } = await supabase
    .from('profiles')
    .select('*', { count: 'exact', head: true })

  if (error) {
    console.error('Keep-alive failed:', error.message)
    return NextResponse.json({ status: 'error', error: error.message }, { status: 500 })
  }

  return NextResponse.json({
    status: 'alive',
    profiles: count,
    timestamp: new Date().toISOString()
  })
}

Vercel의 Cron Jobs 기능을 이용해서 vercel.json에 등록했다:

{
  "crons": [
    {
      "path": "/api/keep-alive",
      "schedule": "0 */6 * * *"
    }
  ]
}

이것만으로 pause 문제는 완전히 해결됐다. 6개월째 한 번도 pause된 적 없다.

AuthContext 무한 리로드 버그 — 이틀 날린 삽질

이건 진짜 고통스러웠다. Supabase Auth를 사용하는 AuthContext를 만들었는데, 페이지가 무한 리로드되는 현상이 발생했다. 브라우저 콘솔을 보면 onAuthStateChange가 초당 수십 번 호출되고 있었다.

[AuthContext] Session fetched
[AuthContext] Session fetched
[AuthContext] Session fetched
... (무한 반복)

원인을 추적해보니, onAuthStateChange 콜백 안에서 state를 업데이트하면 → 컴포넌트 리렌더 → useEffect 재실행 → onAuthStateChange 재등록 → 또 콜백 → 무한루프가 되는 구조였다.

Before (버그 코드):

// ❌ 무한 리로드 발생
useEffect(() => {
  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    async (event, session) => {
      setSession(session)
      if (session?.user) {
        const profile = await fetchProfile(session.user.id) // 매번 fetch
        setProfile(profile)
      }
    }
  )
  return () => subscription.unsubscribe()
}, []) // deps가 비어있어도 fetchProfile이 매번 새로 생성됨

After (해결 코드):

// ✅ fetchCacheRef + 5초 TTL로 해결
const fetchCacheRef = useRef<{ data: Profile | null; timestamp: number } | null>(null)
const CACHE_TTL = 5000 // 5초

useEffect(() => {
  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    async (event, session) => {
      setSession(session)
      if (session?.user) {
        const now = Date.now()
        const cache = fetchCacheRef.current
        if (cache && (now - cache.timestamp) < CACHE_TTL) {
          setProfile(cache.data)
          return
        }
        const profile = await fetchProfile(session.user.id)
        fetchCacheRef.current = { data: profile, timestamp: now }
        setProfile(profile)
      }
    }
  )
  return () => subscription.unsubscribe()
}, [])

핵심은 useRef로 캐시를 만들고 5초 TTL을 건 것이다. 5초 안에 같은 요청이 오면 캐시된 프로필을 리턴한다. 이것만으로 무한 리로드가 깔끔하게 사라졌다. 이 버그에 이틀을 날렸다.

Cloudflare로 SSL과 CDN을 무료로

Vercel에 커스텀 도메인을 연결하면 SSL은 자동으로 제공되지만, Cloudflare를 앞에 두면 추가 이점이 있다:

  1. DDoS 방어 — 무료 플랜에서도 기본 제공
  2. 글로벌 CDN — 정적 자산 캐싱으로 Vercel bandwidth 절약
  3. Analytics — 무료 트래픽 분석

설정 순서는:

  1. 도메인 DNS를 Cloudflare 네임서버로 변경
  2. Cloudflare에서 CNAME 레코드로 cname.vercel-dns.com 연결
  3. SSL/TLS → Full (strict) 모드 설정
  4. Vercel 대시보드에서 도메인 추가 → DNS 검증 완료

주의할 점은 Cloudflare proxy (주황색 구름)를 켜면 Vercel의 자동 SSL과 충돌할 수 있다는 것이다. SSL 모드를 반드시 **Full (strict)**로 설정해야 한다. 처음에 Flexible로 했다가 리다이렉트 루프에 빠졌었다.

월 0원의 현실 — 한계와 트레이드오프

6개월간 운영하면서 느낀 점을 정리한다.

잘 되는 것

  • 소규모 SaaS (MAU 수백 명)에는 충분
  • 개발 속도가 빠름 (Supabase + Next.js 조합이 생산적)
  • 장애가 거의 없음 (Vercel과 Supabase 인프라 안정적)

주의할 것

  • Supabase 500MB는 빠르게 찬다 (특히 임베딩 데이터)
  • Vercel 60초 timeout은 무거운 분석에 치명적 (다음 글에서 다룸)
  • 무료 플랜은 언제든 정책이 바뀔 수 있음

실제 월 비용 내역

항목비용
Vercel Hobby₩0
Supabase Free₩0
Cloudflare Free₩0
도메인 (연간)~₩15,000/12 = ₩1,250/월
합계₩0 (도메인 제외)

마치며

"돈 없으면 못 만든다"는 말은 2026년에는 더 이상 통하지 않는다. Next.js + Supabase + Vercel + Cloudflare 조합이면 진짜 ₩0으로 프로덕션 수준의 SaaS를 돌릴 수 있다. 물론 scale-up하면 비용이 발생하겠지만, MVP를 검증하는 단계에서는 이보다 좋은 조합을 아직 못 찾았다.

다음 글에서는 이 무료 인프라에서 프로테오믹스 분석처럼 무거운 작업을 어떻게 돌렸는지, Vercel 60초 timeout과의 전쟁기를 다룬다.


참고 링크:

관련 글