Supabase 실전 활용기 — Firebase 대안으로 쓸만한가?

게시일: 2025년 10월 17일 · 15분 읽기

Firebase 과금 폭탄과 Supabase 발견

2024년 5월, 우리 스타트업 서비스가 월 활성 사용자 수 10만을 넘으면서 문제가 터졌다. Firebase 청구서가 갑자기 $800에서 $4000으로 치솟았다. 이유는 Firestore의 read 작업이 자동으로 카운트되는 방식 때문이었다. 읽기가 쓰기보다 100배 비싸다는 걸 모르고 있었던 것이다.

그때 팀원이 "Supabase가 있다"고 제안했다. Supabase는 Firebase의 오픈소스 대안이라고 불리는데, 실제로 쓰면서 깨달은 건 "완전 다른 철학"이라는 것이었다. Firebase는 "관리형 서비스", Supabase는 "데이터베이스 중심"이다.

1단계: Auth 설정 — 5분이면 끝난다

Supabase auth는 PostgreSQL 기반이다. 클라이언트 라이브러리가 매우 간단하다:

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

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-public-anon-key'
)

// 이메일 로그인
const { data, error } = await supabase.auth.signUpWithPassword({
  email: 'user@example.com',
  password: 'secure-password'
})

// Google 로그인
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://yourapp.com/auth/callback'
  }
})

// 세션 확인
const { data: { session } } = await supabase.auth.getSession()
console.log(session?.user.id) // 사용자 UUID

매우 깔끔하다. Firebase와 달리, JWT 토큰을 명확하게 관리할 수 있다.

2단계: Row Level Security (RLS) — 백엔드 없이 보안을

이 부분이 정말 혁신적이었다. 기존 Firebase를 썼을 때는 Firestore Rules로 권한을 관리했는데, Supabase는 PostgreSQL의 RLS를 사용한다.

-- 테이블 생성
CREATE TABLE posts (
  id bigserial primary key,
  user_id uuid references auth.users,
  title text,
  content text,
  created_at timestamp default now()
);

-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 정책 1: 자신의 글만 읽을 수 있음
CREATE POLICY "Users can read own posts"
  ON posts
  FOR SELECT
  USING (auth.uid() = user_id);

-- 정책 2: 자신의 글만 업데이트할 수 있음
CREATE POLICY "Users can update own posts"
  ON posts
  FOR UPDATE
  USING (auth.uid() = user_id);

-- 정책 3: 다른 사람의 글은 읽을 수 있음 (공개된 경우)
CREATE POLICY "Anyone can read published posts"
  ON posts
  FOR SELECT
  USING (true)
  WITH CHECK (true);

이제 클라이언트에서 아무리 해킹을 시도해도, 자신의 글만 수정할 수 있다:

// 사용자 1이 사용자 2의 글을 수정하려고 시도
const { error } = await supabase
  .from('posts')
  .update({ title: 'Hacked' })
  .eq('id', 999)  // 사용자 2의 글 ID

// error: "Not found or not permitted"
// RLS 정책이 자동으로 막는다

Firebase Rules보다 훨씬 강력하고, SQL 지식이 있으면 정교한 권한 관리가 가능하다.

3단계: 데이터베이스 쿼리 — SQL의 강력함

Supabase는 PostgreSQL이므로, SQL의 모든 기능을 쓸 수 있다:

// 복잡한 조인 쿼리
const { data, error } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    content,
    author:user_id(id, email, full_name),
    comments(id, content, comment_author:user_id(email))
  `)
  .eq('published', true)
  .order('created_at', { ascending: false })
  .limit(10)

// Firebase에서는 이런 쿼리가 불가능함
// 여러 번의 쿼리와 클라이언트 조인이 필요했음

Window 함수도 쓸 수 있다:

-- 각 사용자별로 글의 순서를 매김
SELECT
  id,
  title,
  user_id,
  ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as post_number
FROM posts;

-- Firebase에서는 이런 게 불가능함
-- Firestore는 단순한 document store일 뿐

4단계: Storage — 파일 업로드

Supabase Storage는 S3 호환이다:

// 이미지 업로드
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.jpg`, imageFile, {
    cacheControl: '3600',
    upsert: true
  })

// 공개 URL 생성
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/avatar.jpg`)

console.log(publicUrl)
// https://your-project.supabase.co/storage/v1/object/public/avatars/user-id/avatar.jpg

Firebase Storage보다 직관적이고, 권한 관리도 명확하다.

5단계: Edge Functions — 서버리스 함수

Supabase Edge Functions는 Deno 기반이다:

// supabase/functions/send-email/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

serve(async (req) => {
  const { email, subject, body } = await req.json()

  // 이메일 발송 (예: SendGrid API)
  const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      personalizations: [{ to: [{ email }] }],
      from: { email: 'noreply@yourapp.com' },
      subject,
      content: [{ type: 'text/html', value: body }]
    })
  })

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' }
  })
})

클라이언트에서 호출:

const { data, error } = await supabase.functions.invoke('send-email', {
  body: {
    email: 'user@example.com',
    subject: 'Welcome!',
    body: '<h1>환영합니다</h1>'
  }
})

Firebase Cloud Functions보다 빠르고 가볍다.

6단계: 실시간 구독 — 리알타임 데이터 동기화

Supabase는 PostgreSQL의 LISTEN/NOTIFY를 사용한 실시간 기능을 제공한다:

// 사용자 온라인 상태 추적
const subscription = supabase
  .channel('users:online')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'user_status'
    },
    (payload) => {
      console.log('User status changed:', payload)
      // UI 업데이트
    }
  )
  .subscribe()

// 채팅 메시지 실시간 동기화
const chatSubscription = supabase
  .channel(`messages:room-${roomId}`)
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages'
    },
    (payload) => {
      const newMessage = payload.new
      addMessageToUI(newMessage)
    }
  )
  .subscribe()

// 정리
chatSubscription.unsubscribe()

Firebase의 실시간 리스너와 비슷하지만, SQL 기반이라 더 강력하다.

비용 비교 — 정말로 싸나?

우리의 경험:

50배 차이다. 왜 이렇게 쌀까?

하지만 주의할 점: Supabase는 자신의 서버다. 완전 관리형(fully managed)이 아니다. 백업, 스케일링, 모니터링을 고려해야 한다.

문제점과 제약사항

Supabase가 완벽하지는 않다:

결론

Firebase에서 Supabase로 옮기고 6개월이 지났다. 지금 느끼는 것:

단점:

결론: 스타트업이거나 중소 서비스라면 Supabase는 정말 좋은 선택이다. Firebase의 과금 체계가 부담스럽다면, Supabase를 한 번 고려해보자.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.