【로컬 RDB】고속!Drift의 개요와 사용법을 소개합니다!!

앱 설계 시 특정 데이터를 영구적으로 저장할 필요가 있습니다.
이때 사용자의 스마트폰 내에 생성되는 로컬 DB를 설계합니다.

지난번에 Isar를 소개했지만, 개인적인 평가로는 Isar의 업데이트 빈도가 낮아 불안합니다.
그래서 이번에는 Isar에도 뒤지지 않는 고속 RDB인 Drift에 대해 소개합니다.

로컬 DB는 도대체 언제 필요한가?

보통 앱에서 사용하는 변수나 상태(State)는 앱이 종료되면 데이터도 함께 사라지지만, 메모나 채팅 기록 등
다음 번 실행 시에도 사용하고 싶은 데이터가 있는 경우가 있습니다.

이러한 데이터를 저장하기 위해 로컬 DB를 사용합니다.

Drift란?

Drift는 Flutter 및 Dart용 강력한 로컬 데이터베이스 라이브러리입니다.
SQLite를 기반으로 하며, 타입 안전한 쿼리 빌더, 반응형 스트림,
직관적인 데이터 조작을 제공합니다. 이전 명칭: Moor입니다.

Drift의 주요 특징

데이터를 로컬 DB에 저장함으로써,
다음 실행 시에도 데이터를 사용할 수 있습니다.

Drift에는 다음과 같은 특징이 있습니다.

  1. 타입 안전한 쿼리
    Drift는 Dart의 타입 시스템을 활용하여 컴파일 시 쿼리 오류를 감지합니다.
    코딩 중에 오류를 감지할 수 있는 것은 매우 유용하죠!
  2. 반응형 스트림
    데이터베이스의 변경을 실시간으로 감지하여 UI를 자동으로 업데이트할 수 있습니다.
  3. 확장성과 커스터마이징
    Drift는 확장성이 높아 커스텀 함수나 트리거를 쉽게 추가할 수 있습니다.
    복잡한 쿼리나 트랜잭션도 손쉽게 다룰 수 있어 자유도가 높습니다!
  4. 풍부한 문서
    Drift는 상세한 문서와 활발한 커뮤니티의 지원을 받아,
    문서를 참고하면 대부분의 것을 해결할 수 있을 것입니다!?
    공식 문서)https://drift.simonbinder.eu/setup/
  5. 활발한 업데이트 빈도
    Drift는 업데이트 빈도가 높아 최근에는 2주에서 1개월마다 버전 업이 이루어지고 있습니다.
    Stream사가 Drift의 후원사이기 때문에 활발할 수도 있습니다.

Drift의 사용법

Drift의 사용 방법에 대해 소개합니다.

환경 준비

“pubspec.yaml”에 필요한 패키지를 추가합니다.
버전에 대해서는 자신의 환경에 맞는 것을 사용하세요.

dependencies:
 ~기타 기존 패키지~
 drift: ^2.23.1
 sqlite3_flutter_libs: ^0.5.28

dev_dependencies:
 build_runner: ^2.4.14
 drift_dev: ^2.23.1
  • drift: Drift의 코어 라이브러리
  • sqlite3_flutter_libs: Flutter에서 SQLite3를 사용하기 위한 추가 라이브러리
  • build_runner: Dart의 코드 생성을 위한 도구
  • drift_dev: Drift의 코드 자동 생성 도구

데이터베이스와 테이블 정의

먼저, Drift는 RDB이기 때문에 데이터베이스와 테이블을 정의할 필요가 있습니다.
이번에는 파일을 아래와 같이 분리했지만, 테이블 정의와 데이터베이스 정의를
한 파일에 해도 괜찮습니다.

  • 테이블 정의 파일: todo_tbl.dart
  • 데이터베이스 정의 파일: app_db.dart
  • 자동 생성 파일: app_db.g.dart

테이블 정의

테이블을 정의하고 관리할 데이터를 설정합니다.
예를 들어, ToDo 정보를 관리하는 테이블: ToDos를 정의해보겠습니다.

part of 'app_db.dart';

class Todos extends Table {
 IntColumn get id => integer().autoIncrement()(); // autoIncrement()는 자동 채번
 TextColumn get title => text().withLength(min: 1, max: 50)();
 BoolColumn get completed => boolean().withDefault(Constant(false))();
}

테이블 이름 끝에 “s”를 붙이면 자동으로 레코드 이름은 “s”가 제외된 이름이 됩니다.
위의 경우 테이블 이름: ToDos, 레코드 이름: ToDo입니다.

데이터베이스 정의

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:io';

part 'todo_tbl.dart'; // 위에서 정의한 테이블 파일
part 'app_db.g.dart'; // 제너레이터로 자동 생성되는 파일

@DriftDatabase(tables: [Todos]) // 테이블이 여러 개일 경우 [ToDos, Users]와 같이 쉼표로 구분하여 지정
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());
  
  @override int get schemaVersion => 1; // 스키마 버전
  // 처리
  Future<List<Todo>> getAllTodos() => select(todos).get();// 모든 레코드 가져오기
  Future<List<Todo>> fetchTodos(String word) async {
     return await (select(todos)..where((tbl) => tbl.title.equals(word))).get();
  }
  Future insertTodo(TodosCompanion todo) => into(todos).insert(todo); // 레코드 삽입
  Future updateTodo(Todo todo) => update(todos).replace(todo); // 레코드 업데이트
  Future deleteTodo(int id) => (delete(todos)..where((tbl) => tbl.id.equals(id))).go(); // 레코드 삭제
  Stream<List<Todo>> watchAllTodos() => select(todos).watch(); // (참고) 레코드 감시
}

// 로컬 DB의 실제 데이터 저장 위치
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase(file);
  });
} 

가져오고 있는 “app_db.g.dart”는 이 시점에서는 존재하지 않기 때문에 오류가 발생합니다.
아래 코드를 터미널에서 실행하면 “app_db.g.dart”가 생성됩니다.

flutter pub run build_runner build --delete-conflicting-outputs

Drift의 기본적인 사용법

Drift를 사용하여 데이터베이스 조작을 수행하는 클라이언트 처리를 소개합니다.
기본적으로 위에서 설정한 데이터베이스의 처리를 호출하기만 하면 됩니다.

데이터 가져오기

import 'app_db.dart';
final db = AppDatabase();

Future<List<Todo>> fetchAllTodos() async {
  return await db.getAllTodos();
}

조건부 데이터 가져오기

import 'app_db.dart';
final db = AppDatabase();

Future<List<Todo>> fetchTodos(String word) async {
  return await db.fetchTodos(word);
}

데이터 삽입

import 'app_db.dart';
final db = AppDatabase();

Future<void> addTodo(String title) async {
  await db.insertTodo(TodosCompanion(
    title: Value(title),
  ));
}

데이터 업데이트

import 'app_db.dart';
final db = AppDatabase();

Future<void> toggleTodoStatus(Todo todo) async {
  final updatedTodo = todo.copyWith(completed: !todo.completed);
  await db.updateTodo(updatedTodo);
}

데이터 삭제

import 'app_db.dart';
final db = AppDatabase();

Future<void> deleteTodoItem(int id) async {
  await db.deleteTodo(id);
}

데이터의 도메인 모델과 매핑

보통 앱 내에서 다루는 데이터는 단순하지 않고 도메인 모델로 정의되는 경우가 일반적입니다.
이 경우 도메인 모델과 데이터베이스 모델 간의 데이터 변환이 필요합니다.

도메인 모델 정의

도메인 모델의 ToDo 클래스가 다음과 같다고 가정합니다.

/// 도메인 모델: ToDo
class ToDo {
  final int? id;
  final String title;
  final bool isCompleted;

  ToDo({
    this.id,
    required this.title,
    this.isCompleted = false,
  });
}

도메인 모델과 데이터베이스 모델 간 변환 처리

확장 메소드를 사용하여 도메인 모델과 데이터베이스 간의 변환 처리를 구현합니다.

/// 도메인 모델 → Drift의 데이터베이스 모델로의 변환
extension ToDoToDatabase on ToDo {
  TodosCompanion toInsertCompanion() {
    return TodosCompanion.insert(
      // 삽입 시 id는 자동 설정됩니다
      title: title,
      completed: completed,
    );
  }

  TodosCompanion toUpdateCompanion() {
    return TodosCompanion(
      id: Value(id),
      title: Value(title),
      completed: Value(isCompleted),
    );
  }
}

/// Drift의 데이터베이스 모델 → 도메인 모델로의 변환
extension DatabaseToDo on Todo {
  ToDo toDomain() {
    return ToDo(
      id: id,
      title: title,
      isCompleted: completed,
    );
  }
}

매핑을 활용한 데이터 조작

예를 들어, 위의 변환 처리를 사용한 데이터 조작을 소개합니다.

import 'app_db.dart';
final db = AppDatabase();

// ToDo 삽입
Future<void> addToDoItem(ToDo toDo) async {
  await db.insertTodo(toDo.toInsertCompanion());
}

// ToDo 전체 가져오기
Future<List<ToDo>> getToDoItems() async {
  final dbTodos = await db.getAllTodos();
  return dbTodos.map((dbTodo) => dbTodo.toDomain()).toList();
}

Drift의 주의사항

Drift는 RDB이기 때문에, 테이블 정의나 스키마를 변경할 때는 버전 관리가 필요합니다.
또한, 데이터 항목을 추가할 경우 각 버전마다 마이그레이션 처리를 작성해야 합니다.

자세한 내용은 공식 문서를 참고하세요.
공식 문서)https://drift.simonbinder.eu/Migrations/step_by_step/?h=migration

결론

Drift의 처리 속도는 실제 기기에서 Isar와 큰 차이가 없었습니다.
패키지의 업데이트 빈도가 높아 다른 패키지와의 의존성도 조정하기 쉬워 사용하기 편리합니다.
전반적으로 Isar의 좋은 대안이라고 생각했습니다.

여러분도 꼭 Drift를 사용하여 개발해 보세요!

제목과 URL을 복사했습니다