Supabase
"수파베이스"로 발음
오픈소스 Firebase 대안으로, PostgreSQL을 기반으로 실시간 데이터 구독, Row Level Security(RLS), Edge Functions, 인증 등을 제공하는 BaaS(Backend-as-a-Service) 플랫폼입니다.
"수파베이스"로 발음
오픈소스 Firebase 대안으로, PostgreSQL을 기반으로 실시간 데이터 구독, Row Level Security(RLS), Edge Functions, 인증 등을 제공하는 BaaS(Backend-as-a-Service) 플랫폼입니다.
Supabase는 2020년 Y Combinator 출신 스타트업이 만든 오픈소스 BaaS로, "Firebase의 오픈소스 대안"을 표방합니다. Firebase와 달리 표준 PostgreSQL을 데이터베이스로 사용하여 SQL의 모든 기능(JOIN, Transaction, 복합 쿼리)을 활용할 수 있고, Vendor Lock-in 없이 언제든 셀프 호스팅으로 전환할 수 있습니다.
PostgreSQL 기반이므로 표준 SQL, Foreign Key, 복합 인덱스, 전체 텍스트 검색 등 관계형 DB의 모든 기능을 BaaS 환경에서 사용 가능합니다.
| 특성 | Supabase | Firebase |
|---|---|---|
| 데이터베이스 | PostgreSQL (관계형) | Firestore (Document DB) |
| 쿼리 언어 | SQL (JOIN, 서브쿼리 가능) | 독자 API (제한적 쿼리) |
| 오픈소스 | 완전 오픈소스, 셀프 호스팅 가능 | 클로즈드 소스 |
| 보안 모델 | PostgreSQL RLS (SQL 기반) | Security Rules (독자 문법) |
| 실시간 | PostgreSQL LISTEN/NOTIFY | Firestore onSnapshot |
| Vector 검색 | pgvector 네이티브 지원 | 지원 안 함 |
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
);
// CREATE - 데이터 삽입
async function createPost(post) {
const { data, error } = await supabase
.from('posts')
.insert({
title: post.title,
content: post.content,
author_id: post.authorId,
created_at: new Date().toISOString()
})
.select()
.single();
if (error) throw error;
return data;
}
// READ - 조건부 조회 (SQL WHERE처럼)
async function getPosts(filters) {
let query = supabase
.from('posts')
.select(`
id,
title,
content,
created_at,
author:users!author_id (
id,
name,
avatar_url
),
comments (
id,
content
)
`)
.eq('published', true);
if (filters.category) {
query = query.eq('category', filters.category);
}
if (filters.search) {
query = query.ilike('title', `%${filters.search}%`);
}
const { data, error } = await query
.order('created_at', { ascending: false })
.range(0, 9); // LIMIT 10
if (error) throw error;
return data;
}
// UPDATE - 데이터 수정
async function updatePost(postId, updates) {
const { data, error } = await supabase
.from('posts')
.update({
title: updates.title,
content: updates.content,
updated_at: new Date().toISOString()
})
.eq('id', postId)
.select()
.single();
if (error) throw error;
return data;
}
// DELETE - 데이터 삭제
async function deletePost(postId) {
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId);
if (error) throw error;
}-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 누구나 공개 게시글 읽기 가능
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
-- 로그인 사용자만 작성 가능
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = author_id);
-- 본인 게시글만 수정 가능
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- 본인 게시글만 삭제 가능
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
TO authenticated
USING (auth.uid() = author_id);
-- 조직 멤버만 접근 가능한 테이블
CREATE POLICY "Organization members only"
ON org_documents FOR ALL
TO authenticated
USING (
org_id IN (
SELECT org_id FROM org_members
WHERE user_id = auth.uid()
)
);// 테이블 변경 실시간 구독
function subscribeToMessages(roomId, onMessage) {
const channel = supabase
.channel(`room:${roomId}`)
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`
},
(payload) => {
console.log('Change received:', payload);
switch (payload.eventType) {
case 'INSERT':
onMessage({ type: 'new', data: payload.new });
break;
case 'UPDATE':
onMessage({ type: 'updated', data: payload.new });
break;
case 'DELETE':
onMessage({ type: 'deleted', data: payload.old });
break;
}
}
)
.subscribe();
// 구독 해제 함수 반환
return () => {
supabase.removeChannel(channel);
};
}
// React에서 사용
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 초기 데이터 로드
loadMessages();
// 실시간 구독
const unsubscribe = subscribeToMessages(roomId, (event) => {
if (event.type === 'new') {
setMessages(prev => [...prev, event.data]);
}
});
return unsubscribe;
}, [roomId]);
}// 이메일/비밀번호 회원가입
async function signUp(email, password, metadata) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: metadata.fullName,
avatar_url: metadata.avatarUrl
}
}
});
if (error) throw error;
return data.user;
}
// OAuth 소셜 로그인 (Google, GitHub 등)
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) throw error;
}
// Magic Link (비밀번호 없는 로그인)
async function signInWithMagicLink(email) {
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/dashboard`
}
});
if (error) throw error;
}
// 세션 상태 감지
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event);
if (event === 'SIGNED_IN') {
console.log('User signed in:', session.user);
} else if (event === 'SIGNED_OUT') {
console.log('User signed out');
} else if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed');
}
});import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
// CORS 헤더
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// 환경 변수에서 Supabase 설정
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! }
}
}
);
// 인증된 사용자 확인
const { data: { user }, error: authError } = await supabaseClient.auth.getUser();
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// 요청 처리
const { action, data } = await req.json();
// 비즈니스 로직 (외부 API 호출 등)
const result = await processAction(action, data, user);
return new Response(
JSON.stringify(result),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector;
-- 임베딩 컬럼이 있는 테이블
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536), -- OpenAI ada-002 차원
metadata JSONB
);
-- 인덱스 생성 (HNSW 또는 IVFFlat)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
-- 유사도 검색 함수
CREATE OR REPLACE FUNCTION match_documents(
query_embedding vector(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
-- JavaScript에서 사용
async function searchSimilarDocuments(queryText) {
// OpenAI로 쿼리 임베딩 생성
const embedding = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: queryText
});
// Supabase RPC로 유사도 검색
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: embedding.data[0].embedding,
match_threshold: 0.7,
match_count: 5
});
return data;
}Row Level Security는 강력하지만 복잡합니다. 잘못된 정책은 데이터 유출이나 완전한 차단을 일으킬 수 있으니, supabase test db 명령어나 별도 테스트 환경에서 반드시 검증하세요.
무료 플랜에서 동시 Realtime 연결 수가 제한됩니다. 대규모 실시간 기능은 연결 관리 전략을 세우고 Pro 플랜 이상을 고려하세요.
Edge Functions는 Deno 기반으로 Node.js 패키지 호환성이 제한적입니다. npm 패키지 중 일부는 동작하지 않을 수 있으니 esm.sh나 skypack CDN을 활용하세요.
SQL, 인덱싱, 트랜잭션, RLS 등 PostgreSQL 지식이 필요합니다. Firebase처럼 "쿼리 몰라도 됨" 수준은 아니므로 DB 기초 학습이 선행되어야 합니다.