Supabase 완벽 가이드 — Firebase를 대체하는 오픈소스 백엔드

SupabasePostgreSQL백엔드Firebase대안BaaS풀스택

Supabase는 "오픈소스 Firebase 대안"으로 시작해, 2026년 현재 가장 인기 있는 BaaS(Backend as a Service) 플랫폼이 되었습니다. PostgreSQL 기반으로 Firebase의 장점과 SQL의 강력함을 모두 제공합니다.


Supabase vs Firebase

항목SupabaseFirebase
데이터베이스PostgreSQLFirestore (NoSQL)
쿼리SQL (무제한)제한적
가격관대한 무료 티어읽기/쓰기당 과금
오픈소스
셀프 호스팅
Edge FunctionsDenoNode.js
벡터 검색pgvector 내장별도 서비스 필요

프로젝트 시작하기

1. 프로젝트 생성

  1. supabase.com 가입
  2. "New Project" 클릭
  3. 이름, 비밀번호, 리전 설정

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 선택 이유:

  1. PostgreSQL: 검증된 관계형 데이터베이스
  2. 올인원: Auth, Storage, Realtime, Functions
  3. 무료 티어: 사이드 프로젝트에 충분
  4. 오픈소스: 셀프 호스팅 가능
  5. 타입 안전: 자동 타입 생성

Firebase에서 Supabase로 마이그레이션하는 팀이 늘고 있습니다. SQL의 강력함과 BaaS의 편리함을 동시에 원한다면 Supabase가 정답입니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.