Supabase는 PostgreSQL 위에 Auth, Realtime, Storage, RLS를 얹은 BaaS입니다. 프론트엔드가 직접 DB를 쿼리하더라도 RLS로 보안을 DB 레벨에서 처리하기 때문에, 별도 API 서버 없이 안전한 구조를 만들 수 있습니다.
투자킹을 만들면서 Supabase를 깊게 써봤습니다. 이 글에서는 실제 사용 경험을 중심으로 Supabase로 실시간 기능 만드는 방법과 프론트에서 직접 DB를 쓰는 패턴을 정리합니다.
Supabase가 제공하는 것
Supabase의 핵심 기능:
| 기능 | 설명 |
|---|---|
| PostgreSQL | 완전한 관계형 DB. SQL 그대로 사용 |
| Auth | 소셜 로그인, 이메일 인증, JWT 토큰 |
| Realtime | DB 변경사항을 WebSocket으로 구독 |
| Storage | 파일/이미지 저장소 (CDN 포함) |
| Edge Functions | Deno 기반 서버리스 함수 |
| RLS | Row Level Security — 사용자별 데이터 접근 제어 |
무료 플랜으로 시작할 수 있고, 프로젝트 규모가 커지면 유료로 전환하면 됩니다.
공식 문서: supabase.com/docs
Realtime 구독 — DB 변경사항을 실시간으로 받기
Supabase Realtime은 PostgreSQL의 논리적 복제(Logical Replication)를 기반으로 합니다. 테이블에 INSERT, UPDATE, DELETE가 발생하면 WebSocket으로 즉시 클라이언트에 알려줍니다.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// 특정 테이블 변경 구독
const channel = supabase
.channel('feed_changes')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'feeds' },
(payload) => {
console.log('새 피드:', payload.new)
}
)
.subscribe()
// 구독 해제
channel.unsubscribe()
주의: Realtime은 RLS를 우회할 수 있습니다. 구독 시 filter 옵션으로 본인 데이터만 받도록 제한하거나, Realtime 전용 정책을 설정해야 합니다.
// 본인 데이터만 구독
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'profiles',
filter: `user_id=eq.${userId}`
}, handler)
언제 Realtime을 쓰고, 언제 SSE를 쓰나
투자킹에서는 Supabase Realtime 대신 FastAPI SSE(Server-Sent Events)로 실시간 가격을 스트리밍합니다.
두 방식의 차이:
| Supabase Realtime | FastAPI SSE | |
|---|---|---|
| 방식 | DB 변경 구독 | 서버 → 클라이언트 push |
| 적합한 경우 | DB 데이터가 바뀔 때마다 알림 | 외부 API 데이터를 지속 스트리밍 |
| 코인 가격 | ❌ (Binance API → DB → Realtime은 지연 큼) | ✅ (Binance → Worker → SSE, 3초 갱신) |
| 알림/채팅 | ✅ | 가능하지만 복잡 |
코인 가격처럼 외부 API에서 가져온 데이터를 빠르게 스트리밍할 때는 SSE가 낫습니다. 반면 사용자가 남긴 피드, 댓글, 좋아요 같은 DB 기반 이벤트는 Realtime이 더 적합합니다.
Row Level Security (RLS) — 프론트에서 직접 쿼리해도 되는 이유
Supabase를 쓸 때 제일 헷갈리는 부분이 "프론트에서 직접 DB를 쿼리해도 괜찮습니까?"입니다.
SUPABASE_ANON_KEY가 클라이언트 코드에 노출되는데, 보안 문제가 없는지 의문스러울 수 있습니다.
RLS가 제대로 설정되어 있으면 괜찮습니다.
RLS는 PostgreSQL의 기능으로, 각 행(Row)에 대한 접근을 사용자별로 제어합니다.
-- 본인 데이터만 조회 가능
create policy "users can read own data"
on profiles
for select
using (auth.uid() = user_id);
-- 본인 데이터만 수정 가능
create policy "users can update own data"
on profiles
for update
using (auth.uid() = user_id);
auth.uid()는 JWT 토큰에서 자동으로 추출됩니다. 익명 요청(로그인 안 한 사용자)은 auth.uid()가 null이라 위 정책에서 걸립니다.
프론트에서 직접 Supabase 쿼리하는 패턴
투자킹에서 쓰는 방식입니다. 단순한 CRUD는 FastAPI를 거치지 않고 프론트에서 Supabase를 직접 씁니다.
// 프론트엔드 (React)
import { supabase } from '~/lib/supabase'
// 뉴스 목록 조회 (RLS로 보호)
const { data, error } = await supabase
.from('news_articles')
.select('uuid, title, summary, published_at, tags')
.eq('visible', true)
.order('published_at', { ascending: false })
.range(offset, offset + 19)
// 사용자 설정 저장
const { error } = await supabase
.from('user_settings')
.upsert({ user_id: userId, theme: 'dark' })
FastAPI를 거쳐야 하는 경우:
- 거래 실행 (복잡한 검증 로직)
- 레버리지 청산 계산
- 여러 테이블을 원자적으로 수정해야 할 때
- 외부 API 호출이 필요할 때
Supabase 직접 쿼리가 괜찮은 경우:
- 단순 조회 (뉴스 목록, 랭킹 등)
- 본인 데이터 수정 (프로필, 설정)
- 파일 업로드/다운로드
Supabase Storage — 이미지 업로드
// 이미지 업로드
const { data, error } = await supabase.storage
.from('blog-images')
.upload(`posts/${fileName}`, file, {
contentType: 'image/png',
upsert: true
})
// Public URL 가져오기
const { data: urlData } = supabase.storage
.from('blog-images')
.getPublicUrl(`posts/${fileName}`)
console.log(urlData.publicUrl)
// https://xxx.supabase.co/storage/v1/object/public/blog-images/posts/image.png
버킷을 public으로 설정하면 인증 없이 URL로 직접 접근할 수 있습니다. CDN이 자동으로 붙어서 전 세계 어디서 접근해도 빠릅니다.
React에서 Supabase 클라이언트 설정
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: false // 쿠키 기반 세션 관리 시
}
})
.env:
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
ANON_KEY는 공개되어도 됩니다. RLS가 실제 보안을 담당합니다. SERVICE_ROLE_KEY는 RLS를 우회하기 때문에 절대 클라이언트에 노출하면 안 됩니다.
FastAPI에서 Supabase 연동
백엔드에서 DB를 직접 조작할 때는 SERVICE_ROLE_KEY를 씁니다.
from supabase import create_client
supabase = create_client(
os.environ["SUPABASE_URL"],
os.environ["SUPABASE_SERVICE_KEY"] # RLS 우회 가능
)
# 데이터 조회
result = await supabase.table("news_articles").select("*").eq("visible", True).execute()
# 데이터 삽입
result = await supabase.table("blog_posts").insert({
"title": "제목",
"content": "내용",
"published_at": "2026-04-16T10:00:00+09:00"
}).execute()