#38 #Flutter Drift 로컬 데이터베이스 연동

서영·2026년 5월 11일
post-thumbnail

Flutter Drift로 로컬 DB 만들기 — 일정 앱에 SQLite 연결하기

1. 오늘의 목표

Flutter에서 Drift 패키지를 사용해 SQLite 데이터베이스를 연결하고, 일정(Schedule) 데이터를 저장·조회·삭제하는 구조를 설계했다.


2. 핵심 개념

Drift의 의미

Drift는 Flutter에서 SQLite 데이터베이스를 Dart 코드로 다룰 수 있게 해주는 ORM(Object-Relational Mapping) 패키지다.

필요성

SQLite를 직접 쓰면 쿼리를 문자열로 직접 써야 한다. 오타가 나도 컴파일 타임에 잡히지 않는다. Drift를 쓰면 쿼리를 Dart 코드로 작성할 수 있어서 자동완성도 되고, 에러도 미리 잡힌다.

사용할 때

앱을 껐다 켜도 데이터가 남아있어야 할 때 — 일정, 메모, 즐겨찾기 같은 로컬 저장 기능을 만들 때 사용한다.


3. 핵심 코드 (파일별)

테이블 스키마 — schedule.dart

class Schedules extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get content => text()();
  DateTimeColumn get date => dateTime()();
  IntColumn get startTime => integer()();
  IntColumn get endTime => integer()();
}

데이터베이스 클래스 — drift_database.dart

(tables: [Schedules])
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());

  Stream<List<Schedule>> watchSchedules(DateTime date) =>
      (select(schedules)..where((tbl) => tbl.date.equals(date))).watch();

  Future<int> createSchedule(SchedulesCompanion data) =>
      into(schedules).insert(data);

  Future<int> removeSchedule(int id) =>
      (delete(schedules)..where((tbl) => tbl.id.equals(id))).go();

  
  int get schemaVersion => 1;
}

DB 파일 연결 — _openConnection()

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationCacheDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase(file);
  });
}

4. 코드 설명 (한 줄씩, 내 말로)

테이블 스키마 — schedule.dart

코드역할설명
extends Table테이블 선언Drift에게 이 Dart 클래스가 SQLite 테이블임을 알림
integer().autoIncrement()()자동 증가 기본키INSERT 시 id가 1씩 자동 증가. SQL의 PRIMARY KEY AUTO_INCREMENT와 동일
dateTime()()날짜 컬럼Dart의 DateTime 타입을 저장. SQLite 내부에는 Unix timestamp로 저장되고, Drift가 꺼낼 때 DateTime으로 변환
text()()문자열 컬럼일정 내용(content)을 저장하는 텍스트 컬럼
integer()()정수 컬럼startTime / endTime 저장. 시 단위 정수로 표현

CRUD 메서드 — drift_database.dart

코드역할설명
select(schedules)..where(...)조건부 SELECT.. cascade 연산자로 같은 객체에 메서드를 이어서 호출. 중간 변수 없이 코드가 짧아짐
.watch()실시간 Stream 구독DB 변경 시 자동으로 UI 갱신. StreamBuilder와 함께 사용
.get()1회성 조회쿼리를 한 번만 실행하고 Future 반환. 데이터 변경을 감지하지 않음
SchedulesCompanionINSERT 래퍼 클래스Drift가 자동 생성. Schedule 모델과 달리 일부 필드만 넣어도 됨
into(schedules).insert(data)INSERTCompanion 객체를 받아 DB에 새 행 삽입
(delete(schedules)..where(...)).go()DELETEwhere로 조건 지정 후 .go()로 실행

DB 파일 연결 — _openConnection()

코드역할설명
LazyDatabase지연 연결실제로 DB를 사용하는 시점까지 연결을 미룸. 앱 초기 로딩 속도에 유리
getApplicationCacheDirectory()앱 전용 캐시 경로 조회path_provider 패키지가 iOS / Android / macOS 각 OS에 맞는 경로를 자동으로 반환
p.join(dbFolder.path, 'db.sqlite')OS 독립적 경로 생성path 패키지. / vs \ 같은 OS별 구분자 차이를 자동 처리
NativeDatabase(file)SQLite 파일 열기지정 경로에 db.sqlite 파일을 생성하거나 열어 실제 연결 수립

5. 실행 결과

달력에서 날짜를 선택하면 해당 날짜의 일정 목록이 아래에 표시된다. 일정을 추가하면 리스트에 즉시 반영되고, 삭제하면 바로 사라진다. watchSchedules()Stream을 반환하기 때문에, StreamBuilder와 연결하면 따로 setState()를 호출하지 않아도 UI가 자동 갱신된다.


6. 직접 바꿔본 것

처음에는 .watch() 대신 .get()을 써봤다. get()Future를 반환하기 때문에 일정을 추가해도 화면이 바뀌지 않았다. 다시 새로고침(화면 이동 후 돌아오기)을 해야만 목록이 갱신되었다.

.watch()로 바꾸니까 일정을 추가/삭제하는 순간 화면이 자동으로 갱신되었다. get()은 스냅샷, watch()는 실시간 구독이라고 이해하면 딱 맞다.


7. 오늘 알게 된 점

가장 중요하게 이해한 건 Drift의 코드 생성 구조다.

내가 schedule.dart에서 테이블을 정의하면, build_runnerdrift_database.g.dart를 자동으로 만들어준다. 내가 쓰는 LocalDatabase는 그 생성된 파일의 _$LocalDatabase를 상속한다. 처음엔 _$LocalDatabase가 어디서 나온 건지 몰라서 당황했는데, .g.dart 파일을 보니 거기 있었다.

처음에는 "DB에서 데이터를 가져오면 그냥 Future로 받으면 되는 거 아닌가?" 라고 생각했는데, 알고 보니 일정 앱처럼 데이터가 자주 바뀌는 경우엔 Stream으로 구독하는 게 훨씬 편했다. 데이터가 바뀔 때마다 UI를 직접 갱신하는 코드를 쓸 필요가 없기 때문이다.


8. 아직 헷갈리는 점

schemaVersion을 올릴 때 마이그레이션을 어떻게 작성하는지 아직 모른다. 지금은 버전이 1이라 괜찮지만, 나중에 테이블 컬럼을 추가하거나 변경하면 기존 데이터가 어떻게 되는지 궁금하다.

다음엔 MigrationStrategy를 써서 스키마를 업데이트하는 방법을 공부해봐야겠다.

profile
시대를 따라가는 개발자가 아닌, 시대를 이끄는 개발자

1개의 댓글

comment-user-thumbnail
2026년 5월 14일

시대를 따라가는 개발자가 아닌, 시대를 이끄는 개발자 답네요. 너무너무 너무너무~ 굿~!~~!!!

답글 달기