티스토리 뷰

Flutter 애플리케이션 개발 시, 데이터를 영구적으로 저장해야 하는 경우가 많습니다. 예를 들어, 메모 앱, 할 일 리스트, 또는 사용자 설정을 저장할 때 SQLite를 사용하는 것은 매우 효과적인 선택입니다. 이번 블로그 포스트에서는 Flutter에서 SQLite를 활용하여 데이터 영구 저장을 구현하는 방법을 자세히 살펴보겠습니다.

참고. Persist data with SQLite

SQLite란 무엇인가?

SQLite는 경량의 관계형 데이터베이스 관리 시스템(RDBMS)으로, 모바일 애플리케이션에서 널리 사용됩니다. Flutter에서 SQLite는 sqflite 패키지를 사용하여 구현할 수 있으며, 간단하면서도 강력한 기능을 제공합니다.

왜 SQLite를 선택해야 할까?

  1. 로컬 데이터 저장:
    • 네트워크 연결이 없어도 데이터를 저장하고 사용할 수 있습니다.
  2. SQL 쿼리 지원:
    • 관계형 데이터베이스의 장점을 활용하여 데이터 조회와 관리가 효율적입니다.
  3. 빠른 속도와 경량성:
    • SQLite는 빠르고 효율적이며, 모바일 환경에 적합합니다.

Flutter에서 SQLite를 사용하는 방법

SQLite를 Flutter 애플리케이션에 통합하려면 다음 단계를 따르면 됩니다.

1. sqflite 패키지 설치

pubspec.yaml 파일에 다음 의존성을 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.0.0
  path: ^1.8.0

그런 다음, 패키지를 설치합니다.

flutter pub get

2. 데이터베이스 설정

path_provider 패키지를 사용하여 데이터베이스 파일 경로를 지정합니다.

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

Future<Database> openDatabaseConnection() async {
  final databasePath = await getDatabasesPath();
  final path = join(databasePath, 'example.db');

  return openDatabase(
    path,
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT, description TEXT)',
      );
    },
    version: 1,
  );
}

3. 데이터 추가

데이터를 추가하기 위해 INSERT 쿼리를 실행합니다.

Future<void> insertItem(Database db, Map<String, dynamic> item) async {
  await db.insert(
    'items',
    item,
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

// 사용 예제
final db = await openDatabaseConnection();
await insertItem(db, {'id': 1, 'name': 'Flutter', 'description': 'A powerful UI toolkit'});

4. 데이터 조회

저장된 데이터를 불러오기 위해 SELECT 쿼리를 사용합니다.

Future<List<Map<String, dynamic>>> fetchItems(Database db) async {
  return await db.query('items');
}

// 사용 예제
final items = await fetchItems(db);
print(items);

5. 데이터 업데이트

기존 데이터를 업데이트하려면 UPDATE 쿼리를 실행합니다.

Future<void> updateItem(Database db, int id, Map<String, dynamic> updatedData) async {
  await db.update(
    'items',
    updatedData,
    where: 'id = ?',
    whereArgs: [id],
  );
}

// 사용 예제
await updateItem(db, 1, {'name': 'Flutter 3.0', 'description': 'Updated description'});

6. 데이터 삭제

데이터를 삭제하려면 DELETE 쿼리를 실행합니다.

Future<void> deleteItem(Database db, int id) async {
  await db.delete(
    'items',
    where: 'id = ?',
    whereArgs: [id],
  );
}

// 사용 예제
await deleteItem(db, 1);

실전 예제: Flutter 애플리케이션에서 SQLite 구현하기

다음은 SQLite를 활용하여 간단한 할 일 관리 앱을 만드는 예제입니다.

주요 코드 스니펫

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

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

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

class ToDoList extends StatefulWidget {
  @override
  _ToDoListState createState() => _ToDoListState();
}

class _ToDoListState extends State<ToDoList> {
  late Database database;
  List<Map<String, dynamic>> tasks = [];

  @override
  void initState() {
    super.initState();
    initializeDatabase();
  }

  Future<void> initializeDatabase() async {
    database = await openDatabase(
      join(await getDatabasesPath(), 'todo.db'),
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT, description TEXT)',
        );
      },
      version: 1,
    );
    loadTasks();
  }

  Future<void> loadTasks() async {
    final data = await database.query('tasks');
    setState(() {
      tasks = data;
    });
  }

  Future<void> addTask(String title, String description) async {
    await database.insert(
      'tasks',
      {'title': title, 'description': description},
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
    loadTasks();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('할 일 목록')),
      body: ListView.builder(
        itemCount: tasks.length,
        itemBuilder: (context, index) {
          final task = tasks[index];
          return ListTile(
            title: Text(task['title']),
            subtitle: Text(task['description']),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          addTask('새로운 할 일', '설명 입력');
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

SQLite 사용 시 유의할 점

  1. 스키마 변경:
    • 데이터베이스 구조가 변경되면 버전을 증가시키고 onUpgrade 메서드를 사용해야 합니다.
  2. 데이터 보안:
    • SQLite 파일은 로컬에 저장되므로 민감한 데이터는 암호화해야 합니다.
  3. 성능 최적화:
    • 대규모 데이터를 처리할 때는 적절한 인덱싱과 페이징 처리를 고려해야 합니다.

결론

SQLite는 Flutter에서 로컬 데이터를 저장하는 데 있어 매우 강력한 도구입니다. 위에서 소개한 단계를 따라 하면 SQLite를 활용하여 데이터베이스를 쉽게 구현할 수 있습니다. 이를 통해 애플리케이션의 기능성과 사용자 경험을 한 단계 더 향상시킬 수 있습니다.

 

import 'dart:async';

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

void main() async {
  // Avoid errors caused by flutter upgrade.
  // Importing 'package:flutter/widgets.dart' is required.
  WidgetsFlutterBinding.ensureInitialized();
  // Open the database and store the reference.
  final database = openDatabase(
    // Set the path to the database. Note: Using the `join` function from the
    // `path` package is best practice to ensure the path is correctly
    // constructed for each platform.
    join(await getDatabasesPath(), 'doggie_database.db'),
    // When the database is first created, create a table to store dogs.
    onCreate: (db, version) {
      // Run the CREATE TABLE statement on the database.
      return db.execute(
        'CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
      );
    },
    // Set the version. This executes the onCreate function and provides a
    // path to perform database upgrades and downgrades.
    version: 1,
  );

  // Define a function that inserts dogs into the database
  Future<void> insertDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Insert the Dog into the correct table. You might also specify the
    // `conflictAlgorithm` to use in case the same dog is inserted twice.
    //
    // In this case, replace any previous data.
    await db.insert(
      'dogs',
      dog.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  // A method that retrieves all the dogs from the dogs table.
  Future<List<Dog>> dogs() async {
    // Get a reference to the database.
    final db = await database;

    // Query the table for all the dogs.
    final List<Map<String, Object?>> dogMaps = await db.query('dogs');

    // Convert the list of each dog's fields into a list of `Dog` objects.
    return [
      for (final {
            'id': id as int,
            'name': name as String,
            'age': age as int,
          } in dogMaps)
        Dog(id: id, name: name, age: age),
    ];
  }

  Future<void> updateDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Update the given Dog.
    await db.update(
      'dogs',
      dog.toMap(),
      // Ensure that the Dog has a matching id.
      where: 'id = ?',
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [dog.id],
    );
  }

  Future<void> deleteDog(int id) async {
    // Get a reference to the database.
    final db = await database;

    // Remove the Dog from the database.
    await db.delete(
      'dogs',
      // Use a `where` clause to delete a specific dog.
      where: 'id = ?',
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [id],
    );
  }

  // Create a Dog and add it to the dogs table
  var fido = Dog(
    id: 0,
    name: 'Fido',
    age: 35,
  );

  await insertDog(fido);

  // Now, use the method above to retrieve all the dogs.
  print(await dogs()); // Prints a list that include Fido.

  // Update Fido's age and save it to the database.
  fido = Dog(
    id: fido.id,
    name: fido.name,
    age: fido.age + 7,
  );
  await updateDog(fido);

  // Print the updated results.
  print(await dogs()); // Prints Fido with age 42.

  // Delete Fido from the database.
  await deleteDog(fido.id);

  // Print the list of dogs (empty).
  print(await dogs());
}

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({
    required this.id,
    required this.name,
    required this.age,
  });

  // Convert a Dog into a Map. The keys must correspond to the names of the
  // columns in the database.
  Map<String, Object?> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // Implement toString to make it easier to see information about
  // each dog when using the print statement.
  @override
  String toString() {
    return 'Dog{id: $id, name: $name, age: $age}';
  }
}