플러터(Flutter) 데이터 저장하는 방법 - 로컬 데이터베이스

안녕하세요. 오늘은 플러터로 앱을 개발하면서 꼭 필요한 데이터 저장에 대해 알아보려고 합니다. 데이터 저장 방법은 여러 가지가 있지만, 이 글에서는 가장 많이 사용되는 세 가지 방법인 Shared_Preferences, SQLite, path_provider 패키지를 사용해서 각각 예제를 만들어볼게요!

 

1. Shared Preferences

간단한 데이터를 저장하기에 적합한 방법이에요. 키(Key)와 값(Value)의 쌍으로 이루어진 작은 정보들을 저장하고, 사용자 설정이나 로그인 정보 같은 간단한 데이터를 저장할 때 자주 사용된답니다. 가볍고 빠르게 데이터에 접근할 수 있는 장점이 있어요.

 

단점: 매우 간단한 데이터 저장 방식이기 때문에, 복잡한 데이터 구조를 저장하거나 관리하기에는 부적합해요. 또한, 대용량 데이터를 저장하기에는 속도가 느려요. 파일이 아닌 XML 형태로 데이터를 저장하고, 모든 데이터를 메모리에 적재하기 때문이에요.

 

한번 예제를 만들어볼게요. 이 저장 방법은 안드로이드 및 플러터 개발을 할 때 가장 많이 사용되기 때문에 알아두면 유용하게 사용하실 수 있어요! 개발 환경에서 바로 실행 가능하도록 전체 코드로 올렸습니다.

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.20
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _textEditingController = TextEditingController();
  late SharedPreferences _prefs; // SharedPreferences 객체

  @override
  void initState() {
    super.initState();
    _initSharedPreferences(); // SharedPreferences 초기화
  }

  // SharedPreferences 초기화 함수
  Future<void> _initSharedPreferences() async {
    _prefs = await SharedPreferences.getInstance();
  }

  // 데이터를 저장하는 함수
  Future<void> _saveData() async {
    _prefs.setString('myData', _textEditingController.text);  // 'myData' 키에 데이터 저장
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('저장완료')),  // 저장 완료 메시지 출력
    );
  }

  // 데이터를 로드하는 함수
  Future<void> _loadData() async {
    final myData = _prefs.getString('myData'); // 'myData' 키에 저장된 데이터 로드
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('로드완료: $myData')), // 로드 완료 메시지와 함께 데이터 출력
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('쭈미로운 생활'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              controller: _textEditingController, // 입력한 데이터를 가져오기 위한 컨트롤러
              decoration: InputDecoration(
                hintText: '저장할 데이터를 입력하세요.', // 힌트 텍스트
              ),
            ),
            SizedBox(height: 16.0),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: _saveData, // 데이터 저장 버튼
                  child: Text('저장하기'),
                ),
                ElevatedButton(
                  onPressed: _loadData, // 데이터 로드 버튼
                  child: Text('불러오기'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

실행 화면 ▼

Shared Preferences 저장1Shared Preferences 저장2Shared Preferences 저장3
Shared Preferences 예제

 

2. SQLite

플러터에서 관계형 데이터베이스가 필요할 때 사용돼요. 복잡한 데이터 구조와 관계를 저장하고 관리할 수 있으며, 쿼리를 이용해서 데이터를 검색, 정렬, 필터링하는 것도 가능하죠. 테이블 형태로 데이터를 저장하고 관리하고 싶을 때 사용하면 됩니다.

 

단점: 관계형 데이터베이스이기 때문에, 초기 설정과 테이블 구조를 정의하는 것이 복잡할 수 있어요. 또한, 간단한 데이터 저장에 비해 오버헤드가 클 수 있습니다. 비관계형 데이터를 저장하고 관리하기에는 제한적인 면이 있어요.

 

이번엔 sqlite 예제를 간단하게 만들어볼게요. 전체 코드는 너무 길어서 View와 DB 제어 부분을 따로 만들겠습니다.

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.0.0+5
  path: ^1.8.1

 

DBHelper.dart ▼

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class DBHelper {
  static final DBHelper _instance = DBHelper._(); // DBHelper의 싱글톤 객체 생성
  static Database? _database; // 데이터베이스 인스턴스를 저장하는 변수

  DBHelper._(); // DBHelper 생성자(private)

  factory DBHelper() => _instance; // DBHelper 인스턴스 반환 메소드

  // 데이터베이스 인스턴스를 가져오는 메소드
  Future<Database> get database async {
    if (_database != null) {
      // 인스턴스가 이미 존재한다면
      return _database!; // 저장된 데이터베이스 인스턴스를 반환
    }
    _database = await _initDB(); // 데이터베이스 초기화
    return _database!; // 초기화된 데이터베이스 인스턴스 반환
  }

  // 데이터베이스 초기화 메소드
  Future<Database> _initDB() async {
    final dbPath = await getDatabasesPath(); // 데이터베이스 경로 가져오기
    final path = join(dbPath, 'example.db'); // 데이터베이스 파일 경로 생성
    return await openDatabase(
      path, // 데이터베이스 파일 경로
      version: 1, // 데이터베이스 버전
      onCreate: (db, version) async {
        await db.execute(
          // SQL 쿼리를 실행하여 데이터베이스 테이블 생성
          'CREATE TABLE example(id INTEGER PRIMARY KEY, name TEXT, value INTEGER)',
        );
      },
    );
  }

  // 데이터 추가 메소드
  Future<void> insertData(String name, int value) async {
    final db = await database; // 데이터베이스 인스턴스 가져오기
    await db.insert(
      'example', // 데이터를 추가할 테이블 이름
      {
        'name': name,
        'value': value,
      }, // 추가할 데이터
      conflictAlgorithm: ConflictAlgorithm.replace, // 중복 데이터 처리 방법 설정
    );
  }

  // 데이터 조회 메소드
  Future<List<Map<String, dynamic>>> selectData() async {
    final db = await database; // 데이터베이스 인스턴스 가져오기
    return await db.query('example'); // 데이터베이스에서 모든 데이터 조회
  }

  // 데이터 수정 메소드
  Future<void> updateData(int id, String name, int value) async {
    final db = await database; // 데이터베이스 인스턴스 가져오기
    await db.update(
      'example', // 수정할 테이블 이름
      {
        'name': name,
        'value': value,
      }, // 수정할 데이터
      where: 'id = ?', // 수정할 데이터의 조건 설정
      whereArgs: [id], // 수정할 데이터의 조건 값
    );
  }
  // 데이터 삭제 메소드
  Future<void> deleteData(int id) async {
    final db = await database; // 데이터베이스 인스턴스 가져오기
    await db.delete(
      'example', // 삭제할 테이블 이름
      where: 'id = ?', // 삭제할 데이터의 조건 설정
      whereArgs: [id], // 삭제할 데이터의 조건 값
    );
  }
}

앱 전체에서 하나의 데이터베이스 인스턴스만 생성되어 사용하도록 싱글톤 패턴으로 만들었어요. 이제 테스트해 볼 화면을 만들어 볼까요? 화면 코드는 너무 길어서 파일로 올리겠습니다. 필요하신 분은 다운로드해 주세요.

main.dart
0.01MB

 

실행 화면 ▼

SQLite 저장1SQLite 저장2SQLite 저장3
SQLite 예제

 

참고로 안드로이드 앱에서 내부 저장소 파일을 사용하는 경우 크롬 브라우저에서는 실행이 불가능해요. 따라서 에뮬레이터를 사용하여 테스트를 진행해야 합니다!

 

3. path_provider 파일 저장

로컬 파일 시스템에 직접 데이터를 저장하고 싶을 때 사용해요. 이미지, 텍스트, 바이너리 등 다양한 형태의 데이터를 저장할 수 있어요. 파일로 관리되는 데이터를 다루기에 적합한 방법이죠.

 

단점: 파일 저장 방식은 직접 파일 시스템에 접근해야 하기 때문에, 데이터 관리에 있어서 좀 더 신경을 써야 해요. 예를 들어, 파일 이름이나 경로를 관리하거나, 파일을 읽고 쓸 때 발생할 수 있는 오류에 대비해야 해요. 또한, 파일을 사용하여 데이터를 저장할 때 검색, 정렬, 필터링 등의 작업이 상대적으로 어렵고 복잡할 수 있습니다.

 

path_provider 활용한 파일 저장 또한 쉽게 사용이 가능해요. 이 패키지는 앱의 내부에서만 접근할 수 있는 디렉토리에 파일을 생성하기 때문에 데이터를 안전하게 보관할 수 있어요. 앱의 내부 경로를 이용해서 파일을 생성하면 파일 권한 문제도 해결됩니다.

dependencies:
  path_provider: ^2.0.5
import 'dart:io';
import 'package:path_provider/path_provider.dart';

// 파일 경로를 생성하는 함수
Future<File> _getFile(String fileName) async {
  // 앱의 디렉토리 경로를 가져옴
  final directory = await getApplicationDocumentsDirectory();
  // 파일 경로와 파일 이름을 합쳐서 전체 파일 경로를 만듬
  return File('${directory.path}/$fileName');
}

// 파일을 저장하는 함수
Future<void> saveToFile(String fileName, String content) async {
  // 파일 경로를 생성함
  final file = await _getFile(fileName);
  // 파일에 내용을 저장함
  await file.writeAsString(content);
}

 //파일을 불러오는 함수 
 Future<String> _loadFile(String fileName) async {
    try {
      //파일을 불러옴
      final file = await _getFile(fileName);
      //불러온 파일의 데이터를 읽어옴
      String fileContents = await file.readAsString();
      return fileContents;
    } catch (e) {
      return '';
    }
 }

앱의 디렉토리 경로에 my_file.txt 파일을 생성하고, Hello, world! 문자열을 파일에 저장하는 코드예요. 

 

Android: /data/user/0//app_flutter

iOS: /Library/:Application Support//

 

저장 위치는 기본적으로 위 경로에 저장이 됩니다. 하지만 앱 내부에서만 접근할 수 있는 디렉토리라서 일반적으로는 확인이 안 돼요. 안드로이드 스튜디오에서 디바이스를 연결한 후 Device File Explorer에서만 접근이 가능하답니다.

 

 

자 이렇게 플러터에서 데이터를 저장하는 방법에 대해 알아봤는데요. 위에서 소개한 방법들은 플러터에서 데이터를 저장하는 대표적인 방법들이에요. 사용 목적과 데이터의 종류에 따라 각각의 상황에 맞게 선택하여 사용하면 됩니다.  - 끝 -