← 블로그 목록
Supabase실시간RLSPostgreSQLFastAPI1인개발

Supabase로 실시간 기능 만들기 — Realtime, RLS, 프론트 직접 쿼리까지

2026년 4월 16일

Supabase는 PostgreSQL 위에 Auth, Realtime, Storage, RLS를 얹은 BaaS입니다. 프론트엔드가 직접 DB를 쿼리하더라도 RLS로 보안을 DB 레벨에서 처리하기 때문에, 별도 API 서버 없이 안전한 구조를 만들 수 있습니다.

투자킹을 만들면서 Supabase를 깊게 써봤습니다. 이 글에서는 실제 사용 경험을 중심으로 Supabase로 실시간 기능 만드는 방법프론트에서 직접 DB를 쓰는 패턴을 정리합니다.


Supabase가 제공하는 것

Supabase의 핵심 기능:

기능설명
PostgreSQL완전한 관계형 DB. SQL 그대로 사용
Auth소셜 로그인, 이메일 인증, JWT 토큰
RealtimeDB 변경사항을 WebSocket으로 구독
Storage파일/이미지 저장소 (CDN 포함)
Edge FunctionsDeno 기반 서버리스 함수
RLSRow 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 RealtimeFastAPI 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()
0

댓글 0

Ctrl+Enter