스타트업에서 백엔드 개발자 1명이 할 수 있는 일은 한계가 있다. 인증, DB, 스토리지, 실시간 기능... 혼자서 이걸 다 만들라고? Supabase를 만난 건 야근에 지쳐 '백엔드를 통째로 대체할 수 있는 게 없나' 검색하다가였다. 2주 만에 MVP를 만들었고, 솔직히 놀랐다.

왜 Supabase인가?

Firebase를 먼저 고려했지만, 몇 가지 이유로 Supabase를 선택했습니다:

프로젝트 초기 설정

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);

실무에서 겪은 주의사항

실무 경험 공유: 개인 사이드 프로젝트에서 Supabase를 써본 경험이 있습니다. PostgreSQL 기반이라 기존 SQL 지식을 그대로 활용할 수 있어서 좋았고, 인증이나 스토리지를 직접 구축하지 않아도 되니 개인 프로젝트의 생산성이 확실히 올라갔습니다. 다만 실무에 적용하려면 성능이나 보안 측면에서 추가 검토가 필요할 것 같습니다.

Supabase, 언제 쓰고 언제 쓰지 말아야 할까

제 경험을 바탕으로 정리하면, Supabase는 MVP 검증, 사이드 프로젝트, 관리자 도구처럼 사용자가 제한적이고 빠른 개발이 중요한 경우에 최적입니다. 반면 응답 시간이 중요한 사용자 대면 서비스, 복잡한 비즈니스 로직이 필요한 경우, 일 트래픽이 수만 건을 넘어가는 경우에는 전통적인 백엔드를 고려하는 것이 낫습니다.

가장 현실적인 전략은 Supabase로 빠르게 시작하되, 서비스가 성장하면 병목이 되는 부분부터 점진적으로 별도 백엔드로 마이그레이션하는 것입니다. PostgreSQL 기반이기 때문에 이 전환이 상대적으로 수월하다는 점이 Supabase의 큰 장점입니다.

Jaeseong
Jaeseong

10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.

GitHub →

💬 댓글