본문 바로가기
튜토리얼/앱

[Flutter] Hive로 메모앱 만들기 — NoSQL 로컬 저장소 + TypeAdapter

by 루까(Luka) 2026. 3. 3.
반응형

왜 Hive인가

Flutter 로컬 저장소로 가장 많이 쓰이는 건 sqflite다. 그런데 sqflite는 SQL을 알아야 하고, 테이블 스키마 정의, 마이그레이션 관리가 필요하다. Hive는 다르다. Dart 객체를 그대로 저장하는 NoSQL key-value 스토어다. 스키마가 없고, 박스(Box)에 넣고 꺼내면 끝이다.

sqflite와 정면 비교:

항목 sqflite Hive
저장 방식 관계형 테이블 Key-Value Box
쿼리 SQL 직접 작성 Dart 코드로 필터링
스키마 정의 CREATE TABLE @HiveType + @HiveField
코드 생성 없음 build_runner (TypeAdapter)
속도 보통 빠름 (바이너리 직렬화)
학습 곡선 SQL 지식 필요 Dart만 알면 됨

앱 구조

lib/
├── main.dart                        # Hive 초기화, 앱 진입
├── models/
│   ├── memo.dart                    # @HiveType 모델
│   └── memo.g.dart                  # 자동 생성 TypeAdapter
└── screens/
    ├── memo_list_screen.dart        # 목록 + 검색
    └── memo_edit_screen.dart        # 추가/수정/삭제

기능:

  • 메모 추가 / 수정 / 삭제 (스와이프 or 삭제 버튼)
  • 6가지 배경 색상 선택
  • 수정일 기준 정렬
  • 실시간 검색 (제목 + 내용)

핵심 구현

Hive 모델 정의

import 'package:hive/hive.dart';

part 'memo.g.dart';

@HiveType(typeId: 0)
class Memo extends HiveObject {
  @HiveField(0)
  late String title;

  @HiveField(1)
  late String content;

  @HiveField(2)
  late int colorValue;

  @HiveField(3)
  late DateTime createdAt;

  @HiveField(4)
  late DateTime updatedAt;
}

@HiveType(typeId: 0) — Box에 저장될 타입을 등록한다. typeId는 앱 전체에서 유일해야 한다. 나중에 모델을 추가하면 1, 2, 3으로 증가시킨다.

@HiveField(n) — 각 필드에 번호를 붙인다. 이 번호가 바이너리 직렬화 시 필드 식별자가 된다. 한 번 배포한 뒤 기존 필드 번호를 바꾸면 기존 데이터를 못 읽는다. 필드 추가는 새 번호로만 해야 한다.

extends HiveObjectmemo.save(), memo.delete() 같은 편의 메서드를 쓸 수 있다. Box를 직접 조작하지 않아도 된다.


TypeAdapter 자동 생성

dart run build_runner build --delete-conflicting-outputs

실행하면 memo.g.dart가 생성된다. MemoAdapter 클래스가 들어있고, Hive가 이걸 이용해 Memo 객체를 바이너리로 직렬화/역직렬화한다. 직접 작성할 필요 없다.

주의: main.dart에 Dart 3 switch expression 같은 신문법이 있으면 hive_generator의 구버전 analyzer가 파싱을 못해 SEVERE 에러가 난다. build_runner 실행 전에 해당 문법을 정리해야 한다.


Hive 초기화 — main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();           // 플랫폼별 저장 경로 자동 설정
  Hive.registerAdapter(MemoAdapter()); // TypeAdapter 등록
  await Hive.openBox<Memo>('memos');  // Box 열기
  runApp(const MemoApp());
}

Hive.initFlutter()hive_flutter 패키지에서 제공한다. iOS는 Documents 디렉터리, Android는 앱 데이터 디렉터리에 .hive 파일을 생성한다.


실시간 반응형 목록 — ValueListenableBuilder

ValueListenableBuilder(
  valueListenable: Hive.box<Memo>('memos').listenable(),
  builder: (context, Box<Memo> box, _) {
    final memos = box.values.toList()
      ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
    // ...
  },
)

box.listenable()ValueListenableBuilder와 연결하면 Box에 변경이 생길 때마다 UI가 자동으로 리빌드된다. setState()나 별도 상태 관리 없이 반응형 목록이 만들어진다.


저장 / 수정 — HiveObject.save()

if (_isEditing) {
  widget.memo!
    ..title = title
    ..content = content
    ..colorValue = _selectedColorValue
    ..updatedAt = now;
  await widget.memo!.save();  // HiveObject 메서드 — Box 업데이트
} else {
  await box.add(Memo(           // 새 항목 추가
    title: title,
    content: content,
    colorValue: _selectedColorValue,
    createdAt: now,
    updatedAt: now,
  ));
}

HiveObject.save()는 객체의 현재 상태를 Box에 즉시 반영한다. box.put(key, object) 를 쓰지 않아도 되는 이유가 extends HiveObject 덕분이다.


스와이프 삭제 — Dismissible

Dismissible(
  key: Key(memo.key.toString()),
  direction: DismissDirection.endToStart,
  background: Container(/* 삭제 배경 */),
  onDismissed: (_) => memo.delete(),
  child: _MemoCard(/* ... */),
)

memo.delete()는 HiveObject 메서드. Box key를 몰라도 객체 자신을 Box에서 제거할 수 있다.


색상을 int로 저장하는 이유

Hive는 Dart의 기본 타입(String, int, double, bool, DateTime, List, Map)을 별도 설정 없이 저장할 수 있다. Color는 기본 타입이 아니다. Color를 직접 저장하려면 별도 TypeAdapter가 필요하다.

가장 간단한 방법은 Color.toARGB32()로 int로 변환해서 저장하고, 꺼낼 때 Color(value)로 복원하는 것이다.

// 저장
colorValue: _colorOptions[0].toARGB32()

// 복원
Color _colorFromValue(int value) =>
    _colorOptions.firstWhere(
      (c) => c.toARGB32() == value,
      orElse: () => _colorOptions[0],
    );

비교 테이블: 로컬 저장소 선택 기준

상황 추천
단순 key-value (설정값, 토큰) shared_preferences
구조화된 객체, 빠른 읽기/쓰기 Hive
복잡한 관계형 쿼리 (JOIN, 인덱스) sqflite / drift
타입 안전 SQL + 코드 생성 drift
대용량 파일/바이너리 파일시스템 직접

메모앱처럼 Dart 객체를 그대로 저장하고 전체 목록을 가져오는 용도엔 Hive가 가장 적합하다.


삽질 기록

build_runner SEVERE: Expected an identifier

hive_generator 2.0.1은 내부적으로 analyzer 6.4.1을 쓴다. Flutter 3.32 기본 프로젝트 템플릿의 main.dart에는 Dart 3.10 switch expression이 포함돼 있는데, 이게 구버전 analyzer로는 파싱이 안 된다.

해결: build_runner build 전에 main.dart를 실제 앱 코드로 교체하면 된다. 기본 카운터 앱 코드에 의존하는 게 아니라 처음부터 실제 코드를 쓰면 발생하지 않는다.

Color.value deprecated

Flutter 3.x에서 Color.value (int 반환)가 deprecated됐다. toARGB32()를 쓰면 동일한 값을 얻을 수 있다. Hive에 색상 int를 저장할 때, 팔레트에서 색상을 비교할 때 모두 toARGB32()로 교체해야 flutter analyze가 깨끗하게 통과한다.

HiveObject.save() vs box.put()

extends HiveObject를 쓰면 object.save(), object.delete()로 Box를 직접 참조하지 않아도 된다. 처음엔 box.put(memo.key, memo)로 업데이트하려다 HiveObject의 편의 메서드가 있다는 걸 알고 코드를 줄였다.


마무리

Hive의 핵심은 "Dart 객체 그대로 저장"이다. SQL도 없고, 스키마 마이그레이션도 없다. @HiveType + @HiveField로 모델을 정의하고, build_runner로 어댑터를 생성하면 끝. box.listenable()로 ValueListenableBuilder와 연결하면 상태 관리 없이도 반응형 UI가 된다.

sqflite와 비교해서 단순한 CRUD 앱엔 Hive가 훨씬 빠르게 개발할 수 있다. 복잡한 관계형 쿼리가 필요해지는 시점에 sqflite나 drift로 넘어가면 된다.


기술 스택: Flutter 3.32 · Dart 3.8 · hive_flutter 1.1.0 · hive_generator 2.0.1 · build_runner · iOS · Android
소스 코드: GitHub

반응형