Flutter Supabase 오프라인 퍼스트: Drift·PowerSync 동기화

네트워크 접속이 불안정한 지하철, 엘리베이터, 음영 지역에서 모바일 앱이 “네트워크 오류” 스피너만 띄우며 멈춰버린다면 사용자는 즉시 앱을 삭제한다. 기존의 ‘네트워크 요청 ➔ 실패 시 캐시 읽기’ 구조는 잦은 로딩 스피너와 오래된 데이터(Stale State) 노출을 피할 수 없다.
2026년 모바일 앱 개발의 세계 표준은 오프라인 퍼스트(Offline-First / Local-First) 아키텍처다. 오프라인 퍼스트 앱에서는 브라우저나 디바이스 내부의 **로컬 SQLite 데이터베이스가 단일 진실 원천(Single Source of Truth)**이 되며, UI는 네트워크 상태를 전혀 몰라도 즉시 반응한다.
이 글은 Flutter와 Supabase(PostgreSQL) 환경에서 Drift SQLite, PowerSync 동기화 엔진, 그리고 Riverpod 3.0을 결합하여 오프라인 반응성과 실시간 서버 동기화를 100% 보장하는 엔터프라이즈 오프라인 퍼스트 아키텍처를 완벽 구현한다.
핵심 요약
- 단일 진실 원천(Single Source of Truth): UI는 Supabase API를 직접 호출하지 않고 로컬 Drift SQLite 데이터베이스만 구독한다. 데이터 변경 시 0ms 반응성을 보장한다.
- PowerSync 엔진의 역할: 로컬 SQLite와 Supabase Postgres 사이에서 부분 동기화(Partial Sync), 충돌 해결(Conflict Resolution), 비동기 아웃박스(Outbox) 처리를 완전 자동화한다.
- Riverpod 3.0 결합:
streamProvider를 통해 로컬 SQLite의 바인딩 스트림을 구독하여 오프라인 상태에서도 UI가 즉시 갱신된다.- 네트워크 투명성: 지하철 등에서 와이파이가 끊겨도 사용자는 데이터 쓰기/수정을 계속할 수 있으며, 재연결 시 백그라운드에서 백엔드로 자동 동기화된다.
- 구형 아키텍처(Brick 등) 탈피: 단순 HTTP 캐싱이나 과도한 래퍼 라이브러리 대신, 2026년 표준인 Drift + PowerSync 조합으로 렌더링 랭크와 데이터 유실을 100% 방지한다.
1. 오프라인 퍼스트 패러다임: 네트워크 캐싱 vs 로컬 퍼스트
기존 캐싱 방식과 2026년 오프라인 퍼스트 아키텍처의 결정적 차이다.
[기존 네트워크 캐싱 방식] ❌
UI ──► Network Fetch (300ms 스피너) ──(실패 시)──► 로컬 캐시 읽기 (오래된 데이터)
[2026 오프라인 퍼스트 아키텍처] ⭕️
UI ──(0ms 즉시 읽기/쓰기)──► [Drift Local SQLite (Single Source of Truth)]
▲
│ (백그라운드 동기화 0ms~네트워크 연결 시)
▼
[PowerSync Engine]
▲
│ (Real-time Sync)
▼
[Supabase PostgreSQL]
오프라인 퍼스트 구조에서는 네트워크 연결이 끊어져도 앱의 동작에 일절 제약이 없다. 모든 데이터 읽기/쓰기는 디바이스 내부의 Drift SQLite로 즉시 수행되고 UI가 0ms만에 반응하며, PowerSync 엔진이 백그라운드 아웃박스 큐(Outbox Queue)에 변경사항을 기록해 두었다가 네트워크가復舊되는 즉시 Supabase로 동기화한다.
2. Drift SQLite + PowerSync + Riverpod 3.0 아키텍처 레이어
| 레이어 | 역할 및 구성 요소 | 주요 특징 |
|---|---|---|
| UI & State Layer | Flutter UI + Riverpod 3.0 (NotifierProvider) |
네트워크 상태와 무관하게 로컬 Drift DB의 Stream만 구독 |
| Local Persistence | Drift (SQLite ORM) | 단일 진실 원천. 타입 안정성이 뛰어난 Dart SQLite 데이터베이스 |
| Sync Engine | PowerSync Client | 로컬 SQLite ↔ Supabase Postgres 간 실시간 차분 동기화 & 충돌 해결 |
| Remote Database | Supabase (PostgreSQL) | RLS(Row Level Security) 기반 멀티테넌트 데이터 보관소 |
3. 오프라인 퍼스트 실전 구현 코드
1단계: Drift SQLite 테이블 정의 (lib/database/app_database.dart)
Drift 공식 문서 규격에 맞춘 타입 안정성 있는 로컬 DB 정의다.
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'app_database.g.dart';
class Todos extends Table {
TextColumn get id => text()();
TextColumn get title => text()();
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
// 로컬 DB 변경 사항을 0ms 스트림으로 노출
Stream<List<Todo>> watchAllTodos() {
return (select(todos)..orderBy([(t) => OrderingTerm.desc(t.updatedAt)])).watch();
}
}
2단계: PowerSync 오프라인 스키마 & 충돌 해결 세팅 (lib/sync/powersync.dart)
PowerSync 공식 문서를 통한 Supabase 오프라인 백그라운드 동기화 커넥터 설정이다.
import 'package:powersync/powersync.dart';
final schema = Schema([
Table('todos', [
Column.text('title'),
Column.integer('is_completed'),
Column.text('updated_at'),
])
]);
late final PowerSyncDatabase db;
Future<void> initPowerSync(String supabaseUrl, String anonKey) async {
db = PowerSyncDatabase(schema: schema, path: 'app_powersync.db');
await db.initialize();
// Supabase 커넥터 연결 (네트워크가 연결되면 백그라운드 동기화 수행)
final connector = SupabaseConnector(url: supabaseUrl, anonKey: anonKey);
await db.connect(connector: connector);
}
3단계: Riverpod 3.0 오프라인 반응형 UI 구독 (lib/providers/todo_provider.dart)
Riverpod 3.0 가이드에서 다룬 AsyncNotifier와 Stream 반응형 패턴을 결합하여, 네트워크 연결 유무에 관계없이 UI가 즉시 반응하도록 구성한다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../database/app_database.dart';
final databaseProvider = Provider<AppDatabase>((ref) => AppDatabase());
// 로컬 Drift DB의 Stream을 실시간 구독하는 Riverpod 3.0 StreamProvider
final todoListProvider = StreamProvider.autoDispose<List<Todo>>((ref) {
final db = ref.watch(databaseProvider);
return db.watchAllTodos();
});
// UI에서 Todo 추가 시: 네트워크 요청 없이 로컬 DB에 즉시 Insert
class TodoNotifier extends AutoDisposeAsyncNotifier<void> {
@override
Future<void> build() async {}
Future<void> addTodo(String title) async {
final db = ref.read(databaseProvider);
await db.into(db.todos).insert(
TodosCompanion.insert(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
updatedAt: DateTime.now(),
),
);
}
}
4. 네트워크 연결 해제 및 재연결 실전 테스트 패턴
오프라인 퍼스트 앱의 정당성을 입증하려면 **네트워크 조작 테스트(Network Manipulation Test)**를 거쳐야 한다.
[테스트 시나리오]
1. 비행기 탑승 모드(오프라인) 활성화 ➔ 앱에서 새 메모 5개 작성 ➔ 0ms 즉시 화면 반영 확인 ⭕️
2. 앱 종료 후 재실행 (오프라인 상태) ➔ 작성한 데이터 100% 로컬 유효성 확인 ⭕️
3. 와이파이 재연결 ➔ PowerSync가 백그라운드에서 Supabase Postgres로 자동 Sync ⭕️
4. Supabase 대시보드 ➔ RLS 정책 적용 및 데이터 충돌 없이 전송 완료 확인 ⭕️
이 구조를 적용하면 Flutter DevTools 메모리 누수 및 CPU 프로파일링 실전 가이드에서 강조한 UI 스레드 렌더링 랭크가 발생하지 않으며, 프레임 레이트 60~120fps가 완벽하게 유지된다.
5. SQLCipher 기반 로컬 SQLite 암호화 실전 세팅
금융, 개인정보, 기밀 데이터를 처리하는 앱은 디바이스 분실이나 루팅(Rooting)/탈옥(Jailbreak) 시 로컬 SQLite 파일이 외부로 유출될 위험이 있다. Drift와 sqflite_sqlcipher 파이프라인을 결합하면 256-bit AES 암호화 데이터베이스를 구성할 수 있다.
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
// 256-bit AES 로컬 DB 암호화 실행기
LazyDatabase _openEncryptedDatabase(String secretPassphrase) {
return LazyDatabase(() async {
final dbFolder = await getDatabasesPath();
final file = File(p.join(dbFolder, 'app_encrypted.sqlite'));
return NativeDatabase(
file,
setup: (rawDb) {
// SQLCipher PRAGMA 암호화 키 설정
rawDb.execute("PRAGMA key = '$secretPassphrase';");
},
);
});
}
이 코드를 적용하면 디바이스의 데이터베이스 파일(app_encrypted.sqlite)을 직접 추출하더라도 암호화 키 없이는 내용을 절대 읽을 수 없으며, PowerSync 클라이언트 역시 암호화된 터널을 통해 안전하게 통신한다.
6. 2026 오프라인 퍼스트 라이브러리 선택 가이드
| 라이브러리 | 오프라인 퍼스트 적합성 | 평가 및 추천 사유 |
|---|---|---|
| PowerSync + Drift | ★★★★★ (추천) | 2026 모범 사례. partial sync, 충돌 해결, Drift 타입 안정성 완벽 조화 |
| Drift + Custom Outbox | ★★★★☆ (커스텀) | 서드파티 서비스 없이 직접 Sync 큐와 REST/GraphQL 커넥터를 만들 때 우수 |
| Supabase Realtime 캐시 | ★★☆☆☆ (비추천) | 오프라인 작성 불가능. 네트워크가 있을 때의 실시간 구독에만 특화됨 |
| Brick ORM | ★☆☆☆☆ (비추천) | 서버 측 삭제 보동기화 실패 및 복잡한 보일러플레이트로 2026년 유지보수 기피 |
자주 묻는 질문
오프라인 상태에서 여러 디바이스가 같은 데이터를 수정하면 충돌이 안 나나요?
PowerSync 엔진은 LWW(Last-Write-Wins) 시계열 규칙이나 CRDT(Conflict-free Replicated Data Type) 알골리즘을 통해 충돌을 백그라운드에서 자동 해결합니다. 특수 비즈니스 로직이 필요한 경우 Supabase Postgres 트리거 함수나 PowerSync 커스텀 충돌 핸들러를 정의하여 손쉽게 제어할 수 있습니다.
로컬 SQLite DB 용량이 너무 커지면 어떻게 하나요?
PowerSync의 부분 동기화(Partial Sync Rules) 기능을 활용하면 사용자가 현재 필요로 하는 데이터(예: 최근 30일 데이터 또는 본인 소유 데이터)만 선택적으로 로컬 디바이스에 다운로드할 수 있어, 디바이스 메모리와 저장공간을 최소화할 수 있습니다.
Supabase 오프라인 데이터 보안은 안전한가요?
로컬 Drift SQLite 데이터베이스 파일에 sqlcipher 암호화 엔진을 적용하면 디바이스 탈옥이나 루팅 환경에서도 데이터베이스 파일을 복호화할 수 없습니다. 또한 Supabase 서버 측 RLS(Row Level Security) 정책이 재연결 시 2차 검증을 수행하므로 보안이 철저히 유지됩니다.
웹(Flutter Web) 플랫폼에서도 동일하게 오프라인 퍼스트가 동작하나요?
동작합니다. Flutter Web 환경에서는 Drift가 Wasm + IndexedDB 백엔드로 자동 전환되어 브라우저 내부에 오프라인 SQLite 데이터베이스를 생성합니다. 웹 환경의 Wasm 렌더링 성능은 Flutter Web Wasm + Skwasm 가이드에서 상세히 다루었습니다.