Flutter 개발

Hive로 시작하는 Flutter 로컬 저장소 — 10분 입문

withmilk 2025. 8. 25. 00:25

앱을 만들다 보면 “다음에 열었을 때도 기억해야 하는 값”들이 꼭 생겨요. 다크 모드 설정, 온보딩을 봤는지 여부, 최근에 본 아이템 목록처럼요. 이런 것들은 거창한 데이터베이스 없이도 빠르게 저장·읽기가 되면 충분합니다. Hive는 바로 그 목적에 맞게 설계된 가볍고 빠른 Flutter 로컬DB예요. 테이블이나 조인을 배우지 않아도 되고, 키-값으로 넣고 꺼내는 단순한 방식이라 진입 장벽이 아주 낮아요.

 

이번 글은 비전공자도 10분 안에 따라 할 수 있도록 정말 필요한 부분만 콕 집어 진행할게요. hive, hive_flutter를 설치한 뒤 앱 시작 시 한 번 초기화하고, Box를 열어 값 넣고(put) 꺼내고(get) 지우는(delete) 기본기, 변경을 감지해서 UI를 자동 갱신하는 watch 까지 같이 해볼 거예요. 민감한 토큰을 다뤄야 한다면 암호화 Box를 살짝 얹는 방법도 함께 보여드릴게요. 복잡한 관계형 데이터나 고급 쿼리가 필요하면 다음 선택지(Isar/Drift)가 더 맞겠지만, “설정·캐시” 같은 가벼운 저장소라면 Hive만으로도 충분히 행복해집니다 🙂

 

한눈 요약

  • SQL 없이 쓰는 키-값(Key-Value) 로컬DB라서 초보도 금방 적응해요.
  • 설치 → Hive.initFlutter() → Box 열기 → put/get/delete/watch 순으로 바로 실습해요.
  • 앱 설정·최근 본 목록·간단 캐시처럼 가벼운 저장에 딱 좋아요.
  • 보안이 필요하면 암호화 Box(AES) 도 간단히 적용할 수 있어요.
  • 다음 편에서 TypeAdapter(커스텀 모델)·마이그레이션까지 확장해 볼게요.

1) 설치 & 셋업(2분 컷)

  • hive + hive_flutter 를 설치하고, 앱 시작 시 단 한 번 Hive.initFlutter() → Hive.openBox('settings') 만 해요.
  • Box는 파일 하나라고 생각하면 쉬워요. 화면에서 매번 열지 말고 앱 시작 때 열고 전역으로 재사용해요.
  • 처음엔 Box<String> 같은 기본 타입으로 시작하고, 커스텀 모델은 다음 편(TypeAdapter)에서!

Hive를 쓰려면 먼저 패키지를 넣고, 앱이 켜질 때 단 한 번 초기화하면 끝이에요. hive_flutter가 내부 저장 경로를 알아서 잡아주기 때문에, 복잡한 경로 설정은 필요 없어요. 핵심은 Box(박스) 개념인데, 쉽게 말하면 “키-값을 담는 작은 로컬 데이터 파일”이에요. 우리는 settings라는 이름의 박스를 하나 열고, 여기에 테마, 온보딩 여부 같은 값들을 저장해 둘 거예요.

 

처음 입문할 땐 문자열/숫자/bool 같은 기본 타입만으로도 대부분의 설정과 캐시를 커버할 수 있어요. 화면 코드에서 openBox를 반복 호출하면 파일을 계속 여닫느라 느려질 수 있으니, 앱 시작 시 미리 열고 어디서든 Hive.box('settings')로 꺼내 쓰는 방식이 깔끔합니다. 커스텀 모델(Todo, User 등)을 저장하고 싶다면 TypeAdapter를 등록해야 하는데, 이건 다음 글에서 천천히 다뤄볼게요.

 

1) 패키지 설치

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  hive: ^latest
  hive_flutter: ^latest

 

2) 앱 시작 시 초기화 & Box 열기

// main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized(); // 비동기 초기화 준비
  await Hive.initFlutter();                  // ✅ Hive 한 번만 초기화
  await Hive.openBox<String>('settings');    // ✅ Box 열기(없으면 생성)
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // 어디서든 Hive.box('settings')로 접근 가능
    return MaterialApp(
      title: 'Hive Starter',
      home: const HomePage(),
    );
  }
}

팁: 제네릭으로 Box<String>처럼 타입을 고정하면 실수를 줄일 수 있어요(다음 편에서 커스텀 타입).

 

3) 자주 하는 실수 체크리스트

  • 초기화 누락: Hive.initFlutter()를 호출하지 않거나 WidgetsFlutterBinding.ensureInitialized() 없이 비동기 코드를 쓰면 에러가 나요.
  • Box를 화면에서 매번 열기: openBox는 앱 시작에서 한 번만! 화면에선 Hive.box('settings')로 재사용하세요.
  • 키 네이밍 제각각: settings.theme, settings.onboarding처럼 네임스페이스 느낌으로 통일하면 관리가 편해요.
  • 커스텀 객체를 바로 put: 기본 타입만 먼저 사용하세요. 모델은 TypeAdapter 등록 후 저장(다음 편에서 다룸).

2. CRUD 빠르게: put / get / delete / watch

  • 핵심 API는 딱 4개: put(저장), get(읽기), delete(삭제), watch/listenable(변화 감지).
  • 키는 일관되게 네임스페이스 형태로 관리하면 좋아요(예: settings.theme).
  • UI 자동 갱신은 box.listenable() + ValueListenableBuilder 조합이 가장 간단해요.

Hive 사용감의 핵심은 “키 하나에 값 하나”를 가볍게 담고 꺼내는 흐름이에요.

예를 들어 다크 모드 설정은 settings.theme = 'dark', 온보딩 체크는

settings.onboarding = false 같은 식으로 저장해 두면, 앱을 껐다 켜도 그대로 유지돼요.

읽을 때는 기본값을 함께 지정하면(예: 'light', true) 앱 첫 실행에서도 안전하게 동작합니다.

 

화면이 값 변화에 자동으로 반응하길 원하면 두 가지 루트를 쓸 수 있어요.

(1) 이벤트 스트림인 box.watch(key: ...)를 구독하거나,

(2) 위젯 빌드에 특화된 box.listenable()을 ValueListenableBuilder와 함께 쓰는 방법이에요.

보통은 두 번째가 더 간단하고, 위젯 트리에 딱 맞게 갱신돼서 입문자에게 추천해요.

 

1) 가장 자주 쓰는 6줄 예제

final box = Hive.box<String>('settings');

// 저장(생성/업데이트)
await box.put('settings.theme', 'dark');
await box.put('settings.onboarding', false.toString()); // Box<String> 예시

// 읽기(기본값 제공)
final theme = box.get('settings.theme', defaultValue: 'light');
final seen  = box.get('settings.onboarding', defaultValue: 'true') == 'true';

// 삭제
await box.delete('settings.onboarding');

// 전체 키 순회(디버깅/마이그레이션 체크)
for (final key in box.keys) {
  debugPrint('$key = ${box.get(key)}');
}

팁: 처음엔 Box<String>처럼 타입을 고정한 뒤, 다음 편에서 TypeAdapter로 커스텀 모델을 다뤄요.

 

2) UI 자동 갱신: listenable + ValueListenableBuilder

class SettingsTile extends StatelessWidget {
  const SettingsTile({super.key});

  @override
  Widget build(BuildContext context) {
    final box = Hive.box<String>('settings');

    return ValueListenableBuilder(
      valueListenable: box.listenable(keys: ['settings.theme']),
      builder: (context, _, __) {
        final theme = box.get('settings.theme', defaultValue: 'light')!;
        final isDark = theme == 'dark';

        return SwitchListTile(
          title: const Text('다크 모드'),
          value: isDark,
          onChanged: (value) {
            box.put('settings.theme', value ? 'dark' : 'light');
          },
          subtitle: Text('현재: $theme'),
        );
      },
    );
  }
}

 

  • box.listenable(keys: [...])로 해당 키가 바뀔 때만 빌드를 유연하게 트리거합니다.
  • 여러 설정을 한 화면에서 관리한다면 키 배열을 늘리거나, 섹션별로 ValueListenableBuilder를 쪼개면 깔끔해요

3) 이벤트 스트림으로 변화 감지: watch

final box = Hive.box<String>('settings');

final sub = box.watch(key: 'settings.theme').listen((event) {
  debugPrint('테마 변경: ${event.value}');
});

// 필요 없을 때 해제
sub.cancel();

 

 

  • 스트림 구독은 서비스/리포지토리 레이어에서 로깅·동기화(예: 서버에 선호도 저장) 같은 비 UI 로직에 유용해요.
  • 화면 위젯에선 listenable 쪽이 더 단순하고 안전합니다.

 

자주 하는 실수 체크리스트

  • 키 뒤죽박죽: settings. / cache. 처럼 접두사를 정해 관리하기.
  • null 기본값 미처리: get(..., defaultValue: ...)로 첫 실행 안전 확보.
  • 과도한 openBox 호출: 앱 시작에 한 번만! 화면에선 Hive.box(...) 사용.
  • 큰 덩어리 저장: 이미지·대형 JSON은 별도 캐시/파일이 더 적합(다음 편에서 구조화 예정).

3. 보안·성능 한 스푼(짧게)

  • 민감한 값(토큰·이메일 등)은 암호화 Box(AES) 로 보관하고, 키는 안전 저장소(secure storage)에 보관해요.
  • 쓰기가 몰릴 땐 배치 쓰기(putAll)·일괄 삭제(deleteAll) 로 I/O를 줄여요.
  • 큰 덩어리(이미지·대형 JSON)는 피하고, 대용량 캐시는 LazyBox 로 메모리를 아껴요.
  • 주기적으로 compact() 해주면 삭제된 공간을 정리해 파일 크기를 관리할 수 있어요.

로컬 DB라도 보안은 필수예요. 사용자 토큰·이메일·주소처럼 노출되면 곤란한 값은 암호화 Box에 따로 넣고,

암복호화에 쓰는 키는 앱 코드가 아니라 플랫폼 보안 저장소(예: flutter_secure_storage)에 보관하세요.

키는 한 번 생성해 저장해 두고, 앱 실행 시 꺼내서 HiveAesCipher에 넣으면 됩니다.

이렇게 하면 평소엔 일반 Box만 쓰다가, 민감한 값만 안전 구역에 보관하는 패턴으로 깔끔하게 분리돼요.

 

성능 팁은 간단해요. 잦은 단건 쓰기보다 한 번에 몰아서 쓰면 디스크 I/O가 확 줄어요.

또한 리스트 스크롤 중에 계속 쓰기를 트리거하지 말고, 사용자 행동 한 덩어리가 끝났을 때

저장하는 식으로 묶어 주세요.

캐시가 커지면 모든 값을 메모리에 올려두는 일반 Box 대신 LazyBox 를 쓰면

“필요할 때만 디스크에서 읽는” 방식이라 앱이 가벼워져요.

마지막으로, 삭제가 많았다면 가끔 box.compact() 를 호출해 파일 크기를 다이어트하면 관리가 쉬워집니다.

 

A) 암호화 Box 예제(키는 secure storage에)

import 'dart:convert';
import 'dart:math';
import 'package:hive/hive.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

Future<List<int>> _loadOrCreateKey() async {
  const storage = FlutterSecureStorage();
  final saved = await storage.read(key: 'hive_aes_key');
  if (saved != null) return base64Url.decode(saved);

  final key = List<int>.generate(32, (_) => Random.secure().nextInt(256)); // 256-bit
  await storage.write(key: 'hive_aes_key', value: base64UrlEncode(key));
  return key;
}

Future<Box<String>> openSecureBox() async {
  final key = await _loadOrCreateKey();
  return Hive.openBox<String>(
    'secure',
    encryptionCipher: HiveAesCipher(key),
  );
}

// 사용 예시
final secure = await openSecureBox();
await secure.put('accessToken', 'secret-xxx');
final token = secure.get('accessToken');

 

 

  • 키는 절대 Git에 올리지 마세요. 에뮬레이터/실기기 간 초기화 정책도 미리 정해 둡니다.
  • 민감 데이터는 필요 최소한만 저장하고, 만료/로그아웃 시 즉시 삭제하세요.

B) 쓰기 최적화 & 대용량 캐시

final box = Hive.box<String>('settings');

// 1) 배치 쓰기 / 일괄 삭제
await box.putAll({
  'settings.theme': 'dark',
  'settings.locale': 'ko',
});
await box.deleteAll(['cache.lastSeen', 'cache.tmp']);

// 2) LazyBox로 대용량 캐시(메모리 절약)
final lazy = await Hive.openLazyBox<String>('bigCache');
await lazy.put('product:42', '{"id":42,"name":"..."}');
final json = await lazy.get('product:42'); // 필요할 때만 디스크에서 로딩

// 3) 파일 크기 다이어트
await box.compact(); // 삭제된 공간을 정리해 파일 크기 축소

 

 

  • 스크롤 중 매 키 입력마다 저장 X → 사용자 액션 단위로 저장.
  • 캐시는 만료 정책(TTL)을 두고 주기적으로 정리.
  • 이미지/바이너리는 파일 캐시가 더 적합(경로만 Hive에 저장).

4. 마무리하며..

  • Hive는 가볍고 빠른 키-값 로컬DB로, 설정·최근 본 목록·간단 캐시에 최적이에요.
  • 오늘 한 것: 설치 → initFlutter() → openBox → put/get/delete/watch암호화/성능 팁.
  • 앱 시작 시 한 번만 초기화, 키 네이밍 일관성, 배치 쓰기와 LazyBox로 효율 업!
  • 다음 편에서는 TypeAdapter(커스텀 모델)·마이그레이션·리포지토리 패턴까지 확장해요.

오늘은 복잡한 이론 없이 바로 써먹는 최소 셋업과 CRUD에 집중했어요.

Hive의 장점은 단순함과 속도예요. 초보자도 Box 하나 열어서 키-값으로 저장해 보면

“아, 이게 로컬 저장의 감이구나”를 금방 잡을 수 있어요.

여기에 box.listenable()을 써서 화면이 자동으로 갱신되도록 만드는 순간, “

설정 화면”이나 “토글 스위치” 같은 UI가 훨씬 자연스러워집니다.

민감한 값만 암호화 Box로 분리하고, 쓰기 몰릴 땐 배치로 처리하면 실무에서도 충분히 탄탄해요.

 

Hive 학습 단계에 대한 설명

 

다음 단계로는 커스텀 모델을 저장해 앱 구조를 더 단단하게 만들 차례예요.

다음 편에서 TypeAdapter를 코드 생성기로 만드는 법, 스키마가 바뀔 때(마이그레이션)

데이터를 안전하게 올리는 법, 그리고 Repository 패턴으로 화면 코드와 저장소를 분리해

테스트하기 쉽게 만드는 팁까지 차근차근 이어갈게요.

필요하면 Hive에서 시작해 Isar/Drift로 확장할 때의 선택 기준도 비교해 드릴 예정이에요.

오늘 예제를 프로젝트에 바로 붙여 보고, 다음 편에서 한 단계 업그레이드해 봅시다!