Supabase는 "오픈소스 Firebase 대안"으로 시작해, 2026년 현재 가장 인기 있는 BaaS(Backend as a Service) 플랫폼이 되었습니다. PostgreSQL 기반으로 Firebase의 장점과 SQL의 강력함을 모두 제공합니다.
Supabase vs Firebase
| 항목 | Supabase | Firebase |
|---|---|---|
| 데이터베이스 | PostgreSQL | Firestore (NoSQL) |
| 쿼리 | SQL (무제한) | 제한적 |
| 가격 | 관대한 무료 티어 | 읽기/쓰기당 과금 |
| 오픈소스 | ✅ | ❌ |
| 셀프 호스팅 | ✅ | ❌ |
| Edge Functions | Deno | Node.js |
| 벡터 검색 | pgvector 내장 | 별도 서비스 필요 |
프로젝트 시작하기
1. 프로젝트 생성
- supabase.com 가입
- "New Project" 클릭
- 이름, 비밀번호, 리전 설정
2. 클라이언트 설치
npm install @supabase/supabase-js
3. 클라이언트 초기화
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
4. 타입 생성 (TypeScript)
# Supabase CLI 설치
npm install -g supabase
# 타입 생성
supabase gen types typescript --project-id your-project-id > types/supabase.ts
// lib/supabase.ts (타입 적용)
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
데이터베이스 (PostgreSQL)
테이블 생성
-- Supabase Dashboard > SQL Editor
create table posts (
id uuid default gen_random_uuid() primary key,
title text not null,
content text,
author_id uuid references auth.users(id),
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);
-- 자동 updated_at
create or replace function update_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger posts_updated_at
before update on posts
for each row execute function update_updated_at();
CRUD 작업
// 생성 (Create)
const { data, error } = await supabase
.from('posts')
.insert({
title: '첫 번째 글',
content: '내용입니다',
author_id: userId,
})
.select()
.single()
// 조회 (Read)
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(10)
// 조건 조회
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', userId)
.gte('created_at', '2026-01-01')
// 관계 조회 (JOIN)
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar_url),
comments(id, content, created_at)
`)
// 수정 (Update)
const { data, error } = await supabase
.from('posts')
.update({ title: '수정된 제목' })
.eq('id', postId)
.select()
.single()
// 삭제 (Delete)
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
Row Level Security (RLS)
-- RLS 활성화
alter table posts enable row level security;
-- 누구나 읽기 가능
create policy "Public posts are viewable by everyone"
on posts for select
using (true);
-- 본인만 생성 가능
create policy "Users can create their own posts"
on posts for insert
with check (auth.uid() = author_id);
-- 본인만 수정 가능
create policy "Users can update their own posts"
on posts for update
using (auth.uid() = author_id);
-- 본인만 삭제 가능
create policy "Users can delete their own posts"
on posts for delete
using (auth.uid() = author_id);
인증 (Auth)
이메일/비밀번호
// 회원가입
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword',
options: {
data: { name: '홍길동' }, // 추가 메타데이터
},
})
// 로그인
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword',
})
// 로그아웃
await supabase.auth.signOut()
// 현재 사용자
const { data: { user } } = await supabase.auth.getUser()
// 세션 변경 감지
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('로그인됨:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('로그아웃됨')
}
})
소셜 로그인
// Google
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
// GitHub
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
})
// Kakao
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'kakao',
})
Next.js 미들웨어
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const { data: { session } } = await supabase.auth.getSession()
// 보호된 경로 체크
if (req.nextUrl.pathname.startsWith('/dashboard')) {
if (!session) {
return NextResponse.redirect(new URL('/login', req.url))
}
}
return res
}
스토리지 (Storage)
버킷 설정
-- Dashboard에서 버킷 생성 또는 SQL로
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true);
-- 정책 설정
create policy "Avatar images are publicly accessible"
on storage.objects for select
using (bucket_id = 'avatars');
create policy "Users can upload their own avatar"
on storage.objects for insert
with check (
bucket_id = 'avatars' and
auth.uid()::text = (storage.foldername(name))[1]
);
파일 업로드/다운로드
// 업로드
const file = event.target.files[0]
const filePath = `${userId}/${file.name}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: true,
})
// 공개 URL 가져오기
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
// 서명된 URL (비공개 파일)
const { data: { signedUrl } } = await supabase.storage
.from('private-files')
.createSignedUrl(filePath, 3600) // 1시간 유효
// 다운로드
const { data, error } = await supabase.storage
.from('avatars')
.download(filePath)
// 삭제
const { error } = await supabase.storage
.from('avatars')
.remove([filePath])
실시간 (Realtime)
데이터베이스 변경 구독
// 테이블 변경 구독
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'posts',
},
(payload) => {
console.log('변경:', payload)
if (payload.eventType === 'INSERT') {
setPosts((prev) => [payload.new, ...prev])
}
if (payload.eventType === 'DELETE') {
setPosts((prev) => prev.filter((p) => p.id !== payload.old.id))
}
}
)
.subscribe()
// 정리
return () => {
supabase.removeChannel(channel)
}
프레즌스 (온라인 상태)
// 온라인 사용자 추적
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const users = Object.values(state).flat()
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('참여:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('퇴장:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString(),
})
}
})
브로드캐스트 (채팅)
// 채팅 메시지 전송
const channel = supabase.channel('chat-room')
// 수신
channel
.on('broadcast', { event: 'message' }, ({ payload }) => {
setMessages((prev) => [...prev, payload])
})
.subscribe()
// 전송
channel.send({
type: 'broadcast',
event: 'message',
payload: {
user_id: userId,
text: messageText,
timestamp: new Date().toISOString(),
},
})
Edge Functions (서버리스)
함수 생성
supabase functions new hello-world
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { name } = await req.json()
// 데이터베이스 작업
const { data, error } = await supabase
.from('greetings')
.insert({ name })
.select()
.single()
return new Response(
JSON.stringify({ message: `Hello ${name}!`, data }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
배포 및 호출
# 배포
supabase functions deploy hello-world
# 로컬 테스트
supabase functions serve
// 클라이언트에서 호출
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: '홍길동' },
})
벡터 검색 (pgvector)
-- pgvector 확장 활성화
create extension if not exists vector;
-- 벡터 컬럼이 있는 테이블
create table documents (
id uuid primary key default gen_random_uuid(),
content text,
embedding vector(1536) -- OpenAI 임베딩 차원
);
-- 유사도 검색 함수
create function match_documents(
query_embedding vector(1536),
match_count int default 5
)
returns table (id uuid, content text, similarity float)
as $$
begin
return query
select
id,
content,
1 - (embedding <=> query_embedding) as similarity
from documents
order by embedding <=> query_embedding
limit match_count;
end;
$$ language plpgsql;
// RAG 검색
const embedding = await generateEmbedding(query) // OpenAI
const { data } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_count: 5,
})
프로덕션 체크리스트
□ RLS 정책 설정
□ API 키 환경 변수 분리
□ 서비스 역할 키 보호 (서버만)
□ 데이터베이스 백업 활성화
□ Edge Functions 에러 핸들링
□ Rate Limiting 고려
□ 인덱스 최적화
□ 모니터링 설정
마치며
Supabase 선택 이유:
- PostgreSQL: 검증된 관계형 데이터베이스
- 올인원: Auth, Storage, Realtime, Functions
- 무료 티어: 사이드 프로젝트에 충분
- 오픈소스: 셀프 호스팅 가능
- 타입 안전: 자동 타입 생성
Firebase에서 Supabase로 마이그레이션하는 팀이 늘고 있습니다. SQL의 강력함과 BaaS의 편리함을 동시에 원한다면 Supabase가 정답입니다.