Supabase 실전 활용기 — Firebase 대안으로 쓸만한가?
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 기반이라 더 강력하다.
비용 비교 — 정말로 싸나?
우리의 경험:
- Firebase (Firestore 중심): 월 $4000 (10만 사용자)
- Supabase (프로): 월 $50 (기본) + $0.125 per GB (스토리지)
- 우리는 총 월 $80 정도 지출
50배 차이다. 왜 이렇게 쌀까?
- Firestore는 read/write를 모두 카운트함
- Supabase는 데이터베이스 크기 기준임
- 데이터 중복 저장이 없으면 훨씬 효율적
하지만 주의할 점: Supabase는 자신의 서버다. 완전 관리형(fully managed)이 아니다. 백업, 스케일링, 모니터링을 고려해야 한다.
문제점과 제약사항
Supabase가 완벽하지는 않다:
- 오프라인 지원 없음 (Firebase Realtime Database는 있음)
- SQL을 잘 모르면 쿼리가 복잡해짐
- 자체 호스팅 필요 (Supabase Cloud 또는 self-hosted)
- 관계형 데이터베이스이므로, 매우 비정형 데이터에는 부적합
결론
Firebase에서 Supabase로 옮기고 6개월이 지났다. 지금 느끼는 것:
- 비용은 1/50 수준으로 줄었다
- SQL을 쓸 수 있으니 복잡한 쿼리도 가능하다
- 권한 관리(RLS)가 훨씬 안전하고 명확하다
- PostgreSQL의 모든 기능을 쓸 수 있다
단점:
- 팀의 SQL 숙련도가 필요하다
- 데이터베이스 관리 책임이 우리에게 있다
- 스케일링 시 성능 튜닝이 필요하다
결론: 스타트업이거나 중소 서비스라면 Supabase는 정말 좋은 선택이다. Firebase의 과금 체계가 부담스럽다면, Supabase를 한 번 고려해보자.