스타트업에서 백엔드 개발자 1명이 할 수 있는 일은 한계가 있다. 인증, DB, 스토리지, 실시간 기능... 혼자서 이걸 다 만들라고? Supabase를 만난 건 야근에 지쳐 '백엔드를 통째로 대체할 수 있는 게 없나' 검색하다가였다. 2주 만에 MVP를 만들었고, 솔직히 놀랐다.
왜 Supabase인가?
Firebase를 먼저 고려했지만, 몇 가지 이유로 Supabase를 선택했습니다:
- PostgreSQL: NoSQL이 아닌 관계형 DB라서 복잡한 쿼리와 JOIN이 자유로움
- 오픈소스: 벤더 종속 없이 셀프 호스팅 가능
- SQL 활용: 기존 SQL 지식을 그대로 활용 가능
- Row Level Security: DB 레벨에서 접근 제어를 설정할 수 있음
- 무료 티어: 개인 프로젝트에 충분한 무료 용량 제공
프로젝트 초기 설정
Flutter 프로젝트에서 Supabase를 초기화하는 방법입니다.
// pubspec.yaml
dependencies:
supabase_flutter: ^2.3.0
// main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: const String.fromEnvironment('SUPABASE_URL'),
anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
);
runApp(const MyApp());
}
// Supabase 클라이언트 접근
final supabase = Supabase.instance.client;
String.fromEnvironment를 사용하면 빌드 시점에 환경변수를 주입할 수 있어 키가 소스 코드에 하드코딩되는 것을 방지합니다.
인증(Authentication) 구현
Supabase는 이메일/비밀번호, OAuth(Google, Apple, GitHub 등), 매직 링크 등 다양한 인증 방식을 지원합니다.
// 이메일 회원가입
Future<void> signUp(String email, String password) async {
final response = await supabase.auth.signUp(
email: email,
password: password,
);
if (response.user != null) {
// 회원가입 성공 - 이메일 인증 대기
}
}
// 로그인
Future<void> signIn(String email, String password) async {
final response = await supabase.auth.signInWithPassword(
email: email,
password: password,
);
// response.session에 JWT 토큰 포함
}
// Google OAuth 로그인
Future<void> signInWithGoogle() async {
await supabase.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo: 'com.bitroom.app://callback',
);
}
// 로그아웃
Future<void> signOut() async {
await supabase.auth.signOut();
}
// 인증 상태 감지
supabase.auth.onAuthStateChange.listen((data) {
final event = data.event;
if (event == AuthChangeEvent.signedIn) {
// 로그인 상태
} else if (event == AuthChangeEvent.signedOut) {
// 로그아웃 상태
}
});
데이터베이스 CRUD
Supabase의 클라이언트 라이브러리를 사용하면 REST API 호출 없이 직접 DB를 조작할 수 있습니다.
// 테이블 생성 (SQL Editor에서)
CREATE TABLE games (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
play_count INT DEFAULT 0,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
// 데이터 조회
final games = await supabase
.from('games')
.select()
.eq('category', 'puzzle')
.order('play_count', ascending: false)
.limit(20);
// 데이터 삽입
await supabase.from('games').insert({
'title': '숫자 퍼즐',
'description': '간단한 숫자 맞추기 게임',
'category': 'puzzle',
'created_by': supabase.auth.currentUser!.id,
});
// 데이터 수정
await supabase
.from('games')
.update({'play_count': playCount + 1})
.eq('id', gameId);
// 데이터 삭제
await supabase
.from('games')
.delete()
.eq('id', gameId);
Row Level Security(RLS)
RLS는 Supabase의 핵심 보안 기능입니다. DB 레벨에서 누가 어떤 데이터에 접근할 수 있는지를 정의합니다.
-- RLS 활성화
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
-- 모든 사용자가 게임 목록을 읽을 수 있음
CREATE POLICY "games_select_policy" ON games
FOR SELECT USING (true);
-- 로그인한 사용자만 게임을 생성할 수 있음
CREATE POLICY "games_insert_policy" ON games
FOR INSERT WITH CHECK (auth.uid() = created_by);
-- 자신이 만든 게임만 수정할 수 있음
CREATE POLICY "games_update_policy" ON games
FOR UPDATE USING (auth.uid() = created_by);
-- 자신이 만든 게임만 삭제할 수 있음
CREATE POLICY "games_delete_policy" ON games
FOR DELETE USING (auth.uid() = created_by);
RLS를 설정하면 클라이언트 코드에서 별도의 권한 체크 없이도 데이터 접근이 자동으로 제어됩니다. 이는 클라이언트 사이드에서의 권한 우회 시도를 원천적으로 차단합니다.
실시간 구독
Supabase는 PostgreSQL의 Change Data Capture를 활용하여 실시간 데이터 동기화를 제공합니다.
// 실시간 구독 - 게임 점수 업데이트 감지
final channel = supabase
.channel('game_scores')
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'scores',
filter: PostgresChangeFilter(
type: PostgresChangeFilterType.eq,
column: 'game_id',
value: currentGameId,
),
callback: (payload) {
final newScore = payload.newRecord;
// UI 업데이트
updateLeaderboard(newScore);
},
)
.subscribe();
// 구독 해제
await supabase.removeChannel(channel);
스토리지 활용
게임 에셋, 사용자 프로필 이미지 등을 Supabase Storage에 저장합니다.
// 이미지 업로드
final file = File('path/to/image.png');
final path = 'games/${gameId}/thumbnail.png';
await supabase.storage
.from('game-assets')
.upload(path, file);
// 공개 URL 생성
final url = supabase.storage
.from('game-assets')
.getPublicUrl(path);
실무에서 겪은 주의사항
- 무료 티어 제한: 500MB DB, 1GB 스토리지, 일시정지 정책(7일 비활성 시) 주의
- RLS 필수 활성화: RLS를 설정하지 않으면 누구나 데이터에 접근 가능
- Edge Function 활용: 클라이언트에서 처리하기 어려운 로직은 서버사이드 함수 활용
- 마이그레이션 관리: 스키마 변경 시 마이그레이션 파일을 체계적으로 관리해야 함
Supabase, 언제 쓰고 언제 쓰지 말아야 할까
제 경험을 바탕으로 정리하면, Supabase는 MVP 검증, 사이드 프로젝트, 관리자 도구처럼 사용자가 제한적이고 빠른 개발이 중요한 경우에 최적입니다. 반면 응답 시간이 중요한 사용자 대면 서비스, 복잡한 비즈니스 로직이 필요한 경우, 일 트래픽이 수만 건을 넘어가는 경우에는 전통적인 백엔드를 고려하는 것이 낫습니다.
가장 현실적인 전략은 Supabase로 빠르게 시작하되, 서비스가 성장하면 병목이 되는 부분부터 점진적으로 별도 백엔드로 마이그레이션하는 것입니다. PostgreSQL 기반이기 때문에 이 전환이 상대적으로 수월하다는 점이 Supabase의 큰 장점입니다.
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글