Supabase란 무엇인가?

오픈소스 Firebase 대안으로 불리는 Supabase의 정체를 다룹니다. PostgreSQL 중심 아키텍처, 테이블만 만들면 생기는 자동 API, Row Level Security, Realtime의 동작 원리까지 애니메이션과 함께 설명합니다.

· 14 min read · PALDYN Team

사이드 프로젝트를 시작할 때 가장 김이 빠지는 순간은 정작 아이디어를 구현하기 전이다. 회원가입·로그인을 붙이고, 데이터베이스를 띄우고, API 서버를 세우고, 파일 업로드를 처리하고… 어떤 앱을 만들든 반복되는 이 “백엔드 밑작업”만으로 며칠이 사라진다. Supabase(수파베이스)는 이 밑작업 전체를 하나로 묶어 제공하는 오픈소스 백엔드 플랫폼(BaaS, Backend as a Service)이다. 흔히 “오픈소스 Firebase 대안”으로 소개되지만, 내부를 들여다보면 Firebase와는 근본부터 다른 선택을 했다 — 중심에 PostgreSQL이 있다.

Firebase 대안, 그런데 데이터베이스가 다르다

Supabase는 2020년에 등장했다. 당시 Firebase는 백엔드 없이 앱을 만들 수 있는 가장 유명한 방법이었지만, 두 가지 불만이 꾸준히 쌓이고 있었다. 하나는 데이터베이스가 Firestore라는 전용 NoSQL이라 조인·집계 같은 관계형 질의가 어렵고, 데이터가 커질수록 모델링이 꼬인다는 것. 다른 하나는 구글 클라우드에 묶이는 벤더 종속(vendor lock-in)이다.

Supabase의 접근은 정반대였다. 새 데이터베이스를 발명하는 대신, 30년 넘게 검증된 PostgreSQL을 그대로 중심에 두고 그 주변에 인증·API·실시간·스토리지를 조립했다. 그래서 Supabase 프로젝트를 만들면 실제로 받는 것은 “Supabase 전용 DB”가 아니라 온전한 PostgreSQL 데이터베이스 한 대다. psql로 직접 접속할 수 있고, 익숙한 SQL이 전부 통하며, 언제든 덤프를 떠서 다른 곳으로 이사할 수 있다. 전체 코드가 오픈소스라 Docker로 셀프 호스팅하는 것도 가능하다.

아키텍처 — PostgreSQL을 중심에 둔 조립식 백엔드

Supabase를 한 장으로 요약하면 아래 그림이다. 클라이언트는 SDK(supabase-js) 하나로 접속하고, 그 뒤의 모든 구성요소가 결국 하나의 PostgreSQL을 바라본다.

Supabase 아키텍처 — 클라이언트에서 PostgreSQL까지 데이터 흐름 애니메이션

각 구성요소는 독립된 오픈소스 프로젝트다.

Auth(인증) — 이메일/비밀번호, 매직 링크, 소셜 로그인(OAuth)을 처리하고 JWT를 발급한다. 발급된 토큰 안의 사용자 ID가 뒤에서 설명할 RLS의 재료가 된다.

REST APIPostgREST가 데이터베이스 스키마를 읽어 테이블마다 REST API를 자동 생성한다. API 서버 코드를 한 줄도 쓰지 않는다.

Realtime — PostgreSQL의 WAL(Write-Ahead Log)을 구독해 데이터 변경을 웹소켓으로 브로드캐스트한다. 채팅, 알림, 협업 기능의 기반이다.

Storage — 이미지·동영상 같은 파일을 저장하고 CDN으로 서빙한다. 파일 접근 권한도 데이터베이스 정책으로 제어한다.

Edge Functions — 결제 웹훅 처리처럼 서버에서만 실행해야 하는 로직을 위한 서버리스 함수(Deno 기반)다.

핵심은 이것들이 별도의 데이터 저장소를 갖지 않는다는 점이다. 인증 사용자도, 스토리지 파일의 메타데이터도 전부 같은 PostgreSQL 안의 테이블이다. 데이터의 진실 공급원이 하나이므로 “인증 DB와 서비스 DB를 어떻게 동기화하지?” 같은 고민이 아예 생기지 않는다.

테이블을 만들면 API가 생긴다

Supabase 개발 경험의 핵심은 단순하다. SQL로 테이블을 만들면, 그 순간 API가 존재한다.

-- 할 일 테이블 하나를 만든다
create table todos (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users (id) not null default auth.uid(),
  title text not null,
  is_done boolean not null default false,
  created_at timestamptz not null default now()
);

이 DDL이 실행되는 순간 /rest/v1/todos 엔드포인트가 생긴다. 클라이언트에서는 supabase-js로 이렇게 쓴다.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// INSERT — 새 할 일 추가
await supabase.from('todos').insert({ title: '장보기' })

// SELECT — 미완료 할 일을 최신순으로
const { data, error } = await supabase
  .from('todos')
  .select('id, title, created_at')
  .eq('is_done', false)
  .order('created_at', { ascending: false })

// UPDATE — 완료 처리
await supabase.from('todos').update({ is_done: true }).eq('id', 1)

메서드 체인이 그대로 SQL로 번역된다고 생각하면 된다(.eq()where =, .order()order by). 관계형 데이터베이스이므로 외래 키를 따라가는 조인도 select('title, projects(name)')처럼 한 줄로 표현할 수 있다 — Firestore에서 컬렉션을 중복 저장하며 씨름하던 것과 대비되는 지점이다.

그런데 여기서 당연한 의문이 생긴다. 브라우저가 데이터베이스에 직접 쿼리를 보낸다고? 보안은?

보안의 핵심 — Row Level Security

전통적인 구조에서는 API 서버가 경비원 역할을 한다. “이 요청을 보낸 사용자가 이 데이터를 봐도 되는가”를 서버 코드의 if문이 판단한다. Supabase에는 그 서버가 없으므로, 경비원이 데이터베이스 안으로 들어간다. 그 장치가 PostgreSQL의 내장 기능인 RLS(Row Level Security, 행 수준 보안)다.

Row Level Security — 같은 쿼리라도 사용자에 따라 다른 행이 반환되는 과정 애니메이션

동작 순서는 이렇다. 클라이언트의 모든 요청에는 Auth가 발급한 JWT가 실려 온다. PostgREST는 토큰을 검증한 뒤 그 안의 사용자 ID를 데이터베이스에 전달하고, 데이터베이스는 모든 행을 정책(Policy)에 통과시켜 걸러낸 결과만 돌려준다.

-- 1) 테이블에 RLS를 켠다 — 이제 정책 없이는 아무 행도 안 보인다
alter table todos enable row level security;

-- 2) 조회 정책: 자기 행만 볼 수 있다
create policy "내 할 일만 조회" on todos
  for select using (auth.uid() = user_id);

-- 3) 삽입 정책: 자기 이름으로만 쓸 수 있다
create policy "내 이름으로만 추가" on todos
  for insert with check (auth.uid() = user_id);

auth.uid()는 현재 요청의 JWT에서 사용자 ID를 꺼내는 함수다. 이제 사용자 A가 select * from todos를 실행하든, 악의적으로 API를 직접 호출하든 결과에는 user_id = A인 행만 존재한다. 권한 검사가 애플리케이션 코드가 아니라 데이터 계층에 있으므로, 어떤 경로로 접근해도 우회할 수 없다는 것이 이 모델의 힘이다.

반대로 말하면 RLS를 켜지 않은 테이블은 누구나 읽고 쓸 수 있다. Supabase 보안 사고의 대부분이 “RLS 켜는 걸 잊었다”에서 나온다. 테이블을 만들면 정책부터 작성하는 습관이 필수다.

Realtime은 어떻게 동작하나

Supabase의 실시간 기능은 폴링이 아니다. PostgreSQL은 모든 변경 사항을 WAL(Write-Ahead Log)이라는 로그에 먼저 기록하는데, Realtime 서버(Elixir로 작성)가 이 로그 스트림을 구독하고 있다가 변경이 감지되면 웹소켓으로 연결된 구독자들에게 밀어 보낸다.

Realtime — INSERT가 WAL을 거쳐 구독 클라이언트들에게 전파되는 과정 애니메이션

클라이언트 코드는 이벤트 리스너를 하나 등록하는 것이 전부다.

// todos 테이블의 INSERT를 실시간 구독
supabase
  .channel('todos-feed')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'todos' },
    (payload) => {
      console.log('새 할 일 도착:', payload.new)
      // 화면에 바로 반영
    }
  )
  .subscribe()

다른 사용자가 할 일을 추가하면, 별도의 새로고침 없이 payload가 도착한다. 여기서도 RLS가 함께 동작해서 자신이 볼 권한이 있는 변경만 전달받는다. 데이터베이스 변경 감지 외에도, DB를 거치지 않고 클라이언트끼리 메시지를 주고받는 Broadcast, 접속자 목록을 공유하는 Presence 기능도 같은 채널 위에서 제공된다.

Firebase와 무엇이 다른가

구분SupabaseFirebase
데이터베이스PostgreSQL (관계형, SQL)Firestore (NoSQL, 문서형)
조인·집계SQL로 자연스럽게 지원클라이언트에서 조합하거나 중복 저장
권한 모델RLS — SQL 정책Security Rules — 전용 문법
소스 공개오픈소스, 셀프 호스팅 가능비공개, 구글 클라우드 전용
데이터 이전pg_dump로 표준 백업/이전전용 내보내기 도구 필요
모바일 오프라인 동기화제한적강력함 (Firestore의 강점)

정리하면 관계형 데이터 모델과 SQL이 필요하거나, 특정 벤더에 묶이기 싫다면 Supabase가 유리하고, 모바일 중심에 오프라인 동기화가 핵심이라면 여전히 Firebase가 강하다.

언제 Supabase를 선택하면 좋을까

잘 맞는 경우 — 백엔드 인력 없이 빠르게 MVP를 만들어야 할 때, 데이터가 본질적으로 관계형일 때(게시판·주문·팔로우 관계 등), 이미 SQL에 익숙한 팀일 때, 훗날 셀프 호스팅으로 이전할 가능성을 열어두고 싶을 때. 무료 티어가 있어 사이드 프로젝트의 진입 장벽도 낮다.

신중해야 하는 경우 — 복잡한 도메인 로직이 많아 결국 두꺼운 서버 계층이 필요한 서비스라면, Supabase는 “잘 만든 데이터베이스 + 인증” 정도로 역할이 줄어든다. 또 권한 로직이 RLS 정책(SQL)에 쌓이는 구조이므로, 정책이 수십 개로 늘어나면 SQL로 보안 로직을 관리하는 부담이 생긴다는 점도 알아둘 필요가 있다.

마무리

Supabase의 본질은 화려한 신기술이 아니라 검증된 PostgreSQL을 최대한 활용하는 조립술이다. 테이블을 만들면 API가 생기고(PostgREST), 행 단위 보안은 데이터베이스가 직접 지키며(RLS), 변경 사항은 WAL을 타고 실시간으로 흐른다(Realtime). 그래서 Supabase를 잘 쓰는 지름길은 결국 PostgreSQL과 SQL을 잘 이해하는 것이다 — 이 블로그의 SQL 글들이 그대로 무기가 되는 이유다.



읽어주셔서 감사합니다. 😊