플러터 과제

이담현·2026년 5월 26일

[Flutter] Drift(SQLite) DB 연동, GetIt 싱글톤 적용, Form 검증 및 StreamBuilder 실시간 동기화

1. 오늘의 목표

  • Drift(SQLite) 데이터베이스와 Flutter 앱을 완벽하게 연동한다.
  • GetIt을 이용해 DB 인스턴스를 단 한 번만 생성하고, 여러 파일에서 전역으로 공유해 사용한다.
  • FormFormKey를 활용해 사용자 입력값을 유효성 검증(Validation)하고 저장한다.
  • StreamBuilder로 DB 데이터의 변화를 실시간으로 화면에 반영한다.

2. 핵심 개념 정리

Drift (SQLite ORM)

Flutter에서 로컬 데이터베이스를 쉽게 사용할 수 있게 해주는 강력한 패키지입니다.

  • SQL 쿼리를 직접 쓰지 않고 Dart 코드로 DB를 제어할 수 있습니다.
  • watchSchedules()처럼 Stream을 반환하는 메서드를 제공하므로, 데이터가 변경될 때마다 별도의 작업 없이 UI를 자동으로 업데이트할 수 있습니다.

GetIt (서비스 로케이터)

DB 인스턴스를 단 한 번만 생성해 전역 보관함에 등록해두고, 어느 파일에서든 편리하게 꺼내 쓸 수 있도록 돕는 의존성 주입(DI) 패키지입니다.

  • registerSingleton을 사용해 등록하면, 앱 어디서나 GetIt.I<LocalDatabase>()를 통해 동일한 DB 인스턴스에 접근할 수 있습니다.

GlobalKey\<FormState>

Form 위젯을 제어하기 위해 사용하는 고유한 키입니다.

  • formKey.currentState!.validate(): 내부 모든 TextFormFieldvalidator를 한꺼번에 실행합니다.
  • formKey.currentState!.save(): 모든 필드의 onSaved 콜백을 일괄 호출하여 데이터를 수집합니다.

StreamBuilder

지속적으로 들어오는 데이터 흐름(Stream)을 구독(Subscribe)하고, 데이터가 바뀔 때마다 위젯을 자동으로 다시 빌드(Rebuild)해주는 위젯입니다.

  • Drift의 watch~ 메서드와 조합하면 DB 데이터가 추가/삭제되는 순간 즉각 화면이 갱신됩니다.

async / await (비동기 처리)

DB 작업(저장, 조회, 삭제)은 기기의 저장소에 직접 접근하기 때문에 시간이 다소 소요되는 작업입니다.

  • 메인 스레드가 멈추지 않도록 비동기로 처리해야 하며, await를 붙여 해당 작업이 완료될 때까지 기다린 후 다음 코드를 실행하도록 제어합니다.

3. 핵심 코드 및 설명

main.dart

Flutter 엔진 초기화를 보장한 뒤, LocalDatabase 인스턴스를 하나만 만들어 GetIt 보관함에 싱글톤으로 등록합니다.

// main.dart
void main() async {
  // Flutter 프레임워크가 초기화되었는지 확인 (비동기 플러그인 사용 시 필수)
  WidgetsFlutterBinding.ensureInitialized(); 

  final database = LocalDatabase();
  
  // GetIt 전역 보관함에 Singleton으로 등록
  GetIt.I.registerSingleton<LocalDatabase>(database);

  runApp(const MyApp());
}

// custom_text_field.dart
final FormFieldSetter<String> onSaved;
final FormFieldValidator<String> validator;

const CustomTextField({
  required this.label,
  required this.isTime,
  required this.onSaved,
  required this.validator,
  Key? key,
}) : super(key: key);

// ... 중략 ...

TextFormField(
  onSaved: onSaved,
  validator: validator,
  // ... 기타 스타일링 속성
)

schedule_bottom_sheet.dart
GlobalKey를 이용해 입력값을 일괄 검증 및 저장하고, Drift DB에 데이터를 저장(insert)합니다.

  
// schedule_bottom_sheet.dart
void onSavePressed() async {
  // 1. 모든 TextFormField의 유효성 검증 수행
  if (formKey.currentState!.validate()) {
    // 2. 검증 통과 시 모든 필드의 onSaved() 실행
    formKey.currentState!.save();
    
    // 3. GetIt에서 DB 인스턴스를 꺼내와 데이터 비동기 저장
    await GetIt.I<LocalDatabase>().createSchedule(
      SchedulesCompanion(
        startTime: Value(startTime!),
        endTime: Value(endTime!),
        content: Value(content!),
        date: Value(widget.selectedDate),
      ),
    );
    
    // 4. 저장 완료 후 바텀시트 닫기
    Navigator.of(context).pop();
  }
}
  


// schedule_bottom_sheet.dart
void onSavePressed() async {
// 1. 모든 TextFormField의 유효성 검증 수행
if (formKey.currentState!.validate()) {
// 2. 검증 통과 시 모든 필드의 onSaved() 실행
formKey.currentState!.save();

// 3. GetIt에서 DB 인스턴스를 꺼내와 데이터 비동기 저장
await GetIt.I<LocalDatabase>().createSchedule(
  SchedulesCompanion(
    startTime: Value(startTime!),
    endTime: Value(endTime!),
    content: Value(content!),
    date: Value(widget.selectedDate),
  ),
);

// 4. 저장 완료 후 바텀시트 닫기
Navigator.of(context).pop();

}
}

Tip (TimeValidator 로직 흐름): Null 체크 ➡️ 숫자 파싱 실패(Format) 체크 ➡️ 시간 범위 체크(0~24시) 순서로 꼼꼼하게 검증을 수행합니다.

home_screen.dart

watchSchedules가 리턴하는 Stream을 StreamBuilder가 실시간으로 구독하여 화면에 리스트를 그려줍니다.**

  1. 실행 결과
  1. 오늘 알게 된 점
    GetIt의 싱글톤 보장: registerSingleton을 쓰면 앱 전체에서 단 하나의 객체 인스턴스만 공유되므로, DB 연결 객체처럼 리소스를 무겁게 먹고 통일성이 중요한 객체를 관리할 때 매우 유용합니다.

**Form의 편리함: formKey.currentState 제어를 통해 수많은 입력 필드의 컨트롤러를 일일이 만들지 않고도 일괄적으로 validation과 data 컬렉션을 수행할 수 있어 코드가 획기적으로 줄어듭니다.

Reactive UI 구현: Drift의 Stream 기반 메서드(watch~)와 StreamBuilder를 조합하면, 개발자가 수동으로 setState()를 호출하지 않아도 데이터 변경이 감지되어 UI가 실시간 반응형으로 바뀝니다.

비동기 흐름 제어: DB 작업은 반드시 async/await 처리를 확실히 해주어야 합니다. 만약 저장이 끝나기도 전에 Navigator.pop()이 먼저 실행되면 데이터 누락이나 예기치 못한 사이드 이펙트가 발생할 수 있습니다.**

  1. 아직 헷갈리는 점 & 해결 완료!
    Q. SchedulesCompanion에서 값들을 굳이 Value()로 감싸서 넣는 이유가 무엇인가요?
    A. Drift에서 일반 raw 데이터 타입 대신 Value()로 감싸는 이유는 '값이 null인 것'과 '값 자체가 전달되지 않은 것(Absent)'을 명확히 구분하기 위해서입니다.
    기본값이나 AutoIncrement(자동 증가 ID) 필드가 있을 때, 값을 안 주면(absent) DB가 알아서 처리해야 하는데, 그냥 null을 넣어버리면 DB 컬럼에 진짜 NULL을 덮어씌우려는 것으로 오해할 수 있습니다. 그래서 명확하게 값을 전달할 때는 Value(값) 형태로 감싸서 넘겨야 합니다!

Q. snapshot.hasData가 false가 되는 시점은 언제인가요? 데이터가 0개일 때인가요, 아니면 로딩 중일 때인가요?

A. 'DB 응답을 기다리는 로딩 중(연결 중)'일 때 false가 됩니다. > 데이터가 조회되었는데 결과가 0개(빈 리스트 [])인 경우에는 데이터 자체는 존재하는 것이기 때문에 snapshot.hasData는 true가 됩니다! (이때 snapshot.data!.isEmpty가 true가 되는 구조입니다.) 따라서 데이터가 아예 안 들어온 최초 로딩 시점을 걸러내기 위해 !snapshot.hasData 체크가 필수적입니다.

profile
최고 최선의 코드를 생성하는 개발자

0개의 댓글