본문 바로가기
flutter/Package of the Week

Flutter[플러터] / sqflite 패키지 사용법 (데이터베이스, SQite, CRUD, db) sqflite (Flutter Package of the Week)

by ch5c 2025. 7. 13.
반응형

package:sqflite

Flutter 용 SQLite 플러그인입니다. iOS, Android, macOS를 지원합니다.

https://youtu.be/HefHf5B1YM0


가끔은 장치에 중요한 앱 데이터를 저장해야 할 때도 있다. 간단한 데이터라면 shared_preferences 같은 패키지를 사용하는 것도 좋은 방법일 수 있지만 구조화된 데이터를 저장해야 한다면 SQL 데이터베이스를 사용하게 될 것이다. 그러한 상황에서 Flutter 개발자들을 위해 SQLite의 사용을 위한 다양한 옵션이 있다. 그중 가장 유명한 패키지 중 하나인 sqflite를 알아보자.

sqflite은 SQlite 데이터베이스를 사용할 수 있게 해주는 패키지이다. 이 패키지를 사용하면 로컬 디바이스에 영구적으로 데이터를 저장하고 SQL을 통해 데이터를 삽입/조회/업데이트/삭제할 수 있게 된다.

 

일단 사용하기 위해선 먼저 프로젝트의 pubspec.yaml파일 안에 sqflite패키지를 추가해야 할 것이다.

일단 지원되는 형식을 보면 알겠지만 웹에서는 실행이 불가능하다.

암튼 프로젝트에 추가해 준다.

flutter pub add sqflite

근데 여기서 패키지를 하나 더 깔아줄 것이다. 추가할 패키지는 path 패키지로 DB 경로 설정에 무조건 필요하기 때문에 추가해 주는 것이다.

flutter pub add path

이제 필요한 패키지를 다 추가해 줬으니 바로 사용해 주겠다.

근데 sqflite를 사용하려면 가장 먼저 데이터베이스를 초기화시켜줘야 한다. 그 작업을 해줄 것인데 함수 형태로 제작하여 호출하면 DB를 초기화할 수 있게 만들어 주겠다.

먼저 비동기 함수로 제작을 해줄 것인데 SQfliteDatabase 객체를 그 리턴값으로 넣어줄 것이다.

Future<Database> initDatabase() async {}

이렇게 만들어줬으면 iniState에서 호출해서 DB를 초기화하고 얻을 수 있게 될 것이다.

그다음은 저장 경로를 가져와 주겠다.

final dbPath = await getDatabasesPath();

이것은 디바이스(스마트폰)에서 SQLite 데이터베이스가 저장되는 기본 경로를 받아올 수 있게 만들어 준 것인데 예를 들면 /data/user/0/com.example.app/databases 이런 경로로 말이다.

이제 그다음에는 path 패키지를 사용하여 경로를 완성해 주고 파일명을 조합해 주면 된다.

final path = join(dbPath, 'my_database.db');

위에서 받은 dbPath'my_database.db'라는 파일명을 붙여서 전체 경로 완성해 주면 된다.

참고로 여기서 path 패키지의 join 함수를 사용함으로써 올바른 문자열 경로를 만들어주고 있는 것이다.

그래서 예를 들어 현재 완성된 경로는 이렇게 될 것이다. /data/user/0/com.example.app/databases/my_database.db

이제 DB를 열거나 테이블을 생성해 주는 동작을 넣어주면 되겠다.

먼저 openDatabase를 리턴해주겠다.

return openDatabase(path);

그런 다음 파라미터를 추가해 줄 것인데 version이라는 파라미터이다.

return openDatabase(
  path,
  version: 1,
);

여기서 version 파라미터는 DB 구조가 바뀌면 버전 올려서 onUpgrade로 마이그레이션을 가능하게 만들어주는 기능을 갖고 있다.

그다음엔 onCreate 콜백 함수를 추가해 줄 것이다.

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

onCreate는 DB가 처음 생성될 때 실행되는 콜백 함수로 이제 여기서 테이블을 만들어나가야 하겠다.

그다음에 타입으로 Database값을 갖고 있는 db인자에서 SQL 명령어를 실행해 주는 db.execute생성자를 사용해 주겠다.

이렇게 db.execute를 가지고 SQL 명령어를 실행해 주면 items라는 테이블이 생성되게 되는데 여기 문자열에서 id는 정수형 기본 키이고 name은 문자열 컬럼이 되시겠다.

Future<Database> initDatabase() async {
  final dbPath = await getDatabasesPath();
  final path = join(dbPath, 'my_database.db');
  return openDatabase(
    path,
    version: 1,
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT)',
      );
    },
  );
}

이제 이 이 함수, initDatabase()를 앱 시작 시 한번 호출하면 SQLite DB가 준비되고 그 안에 items 테이블도 자동으로 생성되게 된다.

이제 기본 사용을 위한 초기화 코드는 다 작성을 했으니 한번 데이터를 삽입하는 동작을 만들어보겠다.

먼저 사용을 위해 변수를 만들어주겠다.

late Future<Database> _database;
List<Map<String, dynamic>> _items = [];

두 가지 변수를 만들었는데 여기서 _databaseFuture<Database>형태로 비동기 DB 인스턴스를 저장하는 역할을 해줄 것이다. 그다음은 화면에 표시할 데이터(DB에서 받아온 데이터)를 담아주는 용도인 _items이다.

이제 데이터를 DB에 삽입하는 동작을 하는 수 있는 함수를 만들어주겠다.

Future<void> insertItem(Database db, Map<String, dynamic> item) async {
  await db.insert(
    'items', // 테이블 이름
    item, // 삽입할 데이터 (예: {'id': 1, 'name': 'Flutter'})
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

보면 Map<String, dynamic> 타입으로 키-값 쌍을 전달해주고 있는데 나중에 이 함수를 호출하면서 원하는 데이터를 넣어줄 것이기 때문이다. (코드 블록에 주석으로 예를 들어놨다.)

삽입하는 동작은 Database 객체인 db에서 insert()를 호출해 주면 사용이 되는데 insert()는 그 파라미터로 String 타입의 tableMap<String, Object?> 타입의 values를 받게 된다.

이제 여기서 table에는 테이블의 이름을 넣어주면 되고 values에는 말 그대로 삽입할 값을 넣어주면 되겠다. 위에서 말했듯이 들어갈 값은 이 함수를 호출하는 곳에서 Map<String, dynamic> 타입으로 넣어줄 예정이다.

이제 여기에 conflictAlgorithm 파라미터를 추가해서 넣어줄 것인데 현재 ConflictAlgorithm.replace라는 값을 넣어주고 있다. 이제 이렇게 정의해 줌으로써 같은 id가 있다면 기존 값을 덮어써지게 할 수 있다.

이제 데이터를 삽입하는 함수를 제작 완료 했으니 데이터를 불러오는 함수도 하나 만들어주겠다.

Future<void> _loadItems() async {
  final db = await _database;
  final data = await db.query('items'); // 모든 데이터 조회
  setState(() {
    _items = data; // 상태 갱신 → 화면 다시 그림
  });
}

아까 위에서 만들어 뒀던 Future<Database> 객체인 _database를 갖고 와서 인스턴스화시켜주고 그 인스턴스에 우리가 만든 테이블에 이름을 넣어줌으로써 불러와주면 아주 간단하게 동작 끝이다.

이제 실질적인 데이터를 넣을 함수를 만들어주겠다.

Future<void> _insertSample() async {
  final db = await _database;
  await insertItem(db, {'id': 1, 'name': 'Flutter'});
  await insertItem(db, {'id': 2, 'name': 'Dart'});
  _loadItems();
}

먼저 _loadItems에서 처럼 _database를 인스턴스로 만들어준다. 그다음에 아래에서 우리가 만들어놓은 insertItem()을 호출해 줄 건데 아까 말했듯이 이제 여기다가 데이터를 넣어주면 된다. 먼저 인스턴스로 만든 db를 넣어줌으로써 이제 특정 테이블에다가 데이터를 넣어줄 수 있게 되고 파라미터는 Map<string, dynamic> 타입을 받고 있기 때문에 이제 그냥 그대로 넣어주면 된다. 이제 마지막으로 iniState에서 호출해 주면 끝이다.

@override
void initState() {
  super.initState();
  _database = initDatabase();
  _insertSample();
}

_databaseinitDatabase()를 대입해 줌으로써 초기화 작업을 진행해 주고 _insertSample을 넣어서 화면이 그려지면 바로 데이터가 삽입되게 하였다.

이제 UI를 대충 만들어주면 간단하게 데이터를 삽입한 후 불러올 수 있게 되었다.

(전체 코드)

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

void main() => runApp(MaterialApp(home: Test()));

class Test extends StatefulWidget {
  const Test({super.key});

  @override
  State<Test> createState() => _TestState();
}

class _TestState extends State<Test> {
  late Future<Database> _database;
  List<Map<String, dynamic>> _items = [];

  Future<Database> initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'my_database.db');

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

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

  Future<void> _loadItems() async {
    final db = await _database;
    final data = await db.query('items');
    setState(() {
      _items = data;
    });
  }

  Future<void> _insertSample() async {
    final db = await _database;
    await insertItem(db, {'id': 1, 'name': 'Flutter'});
    await insertItem(db, {'id': 2, 'name': 'Dart'});
    _loadItems();
  }

  @override
  void initState() {
    super.initState();
    _database = initDatabase();
    _insertSample();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          final item = _items[index];
          return ListTile(
            title: Text(item['name']),
            subtitle: Text('ID: {item['id']}'),
          );
        },
      ),
    );
  }
}

이제 데이터를 삽입하는 동작을 알아봤는데 이번에는 업데이트하는 동작을 알아보자.

이제 위에서 이미 다 만들어 놨으니 업데이트 동작을 하는 함수 하나만 만들어주면 되겠다.

Future<void> updateItem(Database db, Map<String, dynamic> item) async {}

형식은 다르지 않다. 이제 단지 다른 것은 아까는 db.insert()를 호출했다면 이번에는 db.update를 호출한다는 것이다.

Future<void> updateItem(Database db, Map<String, dynamic> item) async {
  await db.update(
    'items',
    item,
    where: 'id = ?', // 어떤 id의 데이터를 수정할지 조건
    whereArgs: [item['id']], // 조건에 사용할 값
  );
}

 

이제 이 코드에서 where 파라미터와 whereArgs파라미터가 궁금할 수 있다. 쉽게 말해 데이터베이스의 행을 업데이트하기 위한 편의 방법이라고 할 수 있는데 역할이 무엇인지 설명해 주겠다.

sqflite에서 데이터를 수정하거나 삭제할 때는 대상 조건을 지정해줘야 한다. 예를 들어서 

UPDATE items SET name = 'Updated' WHERE id = 1;

이런 SQL을 Flutter에서 쓰려고 한다면 아래와 같이 쪼개서 넣는 방식을 사용해야 한다.

await db.update(
  'items',
  {'name': 'Updated Flutter'},
  where: 'id = ?',
  whereArgs: [1],
);

근데 여기서 궁금할 수 있다. 'id = ?' 이것은 무엇인가? 이건 그냥 SQL 조건문이다. 그니까 지금 id 값은 아직 정해지지 않았고 ?를 넣음으로써 나중에 대입하겠다고 적어놓은 것이다.

이제 다시 위의 코드를 보면 whereArgs에서 [Item['id']]이렇게 사용해주고 있는데 여기 들어갈 값은 우의 where에서 ?에 들어갈 실제 값이 되시겠다.

where은 조건이고 whereArgs는 값이다. 이렇게 해줘야 SQL Injection 방지도 가능하고 유연하게 사용해 줄 수 있다.

이제 끝이다.

UI에 데이터를 넣는 동작만 추가해 주면 된다.

ElevatedButton(
  onPressed: () async {
    final db = await _database;
    await updateItem(db, {'id': 1, 'name': 'Updated Flutter'});
    _loadItems(); // 갱신
  },
  child: Text('ID 1 수정'),
)

이제 마지막으로 삭제하는 기능을 만들어보자. 이게 제일 쉽다.

Future<void> deleteItem(Database db, int id) async {
  await db.delete(
    'items',
    where: 'id = ?', // 어떤 행을 지울지 조건
    whereArgs: [id], // 조건에 사용할 값
  );
}

이렇게 간단하게 db.delete()를 사용하여 만들어주면 특정 id를 가진 행을 삭제해 줄 수 있게 된다.

마찬가지로 UI에서 삭제할 데이터를 넣어주겠다.

ElevatedButton(
  onPressed: () async {
    final db = await _database;
    await deleteItem(db, 2); // id 2 삭제
    _loadItems(); // 갱신
  },
  child: Text('ID 2 삭제'),
),

이제 이 버튼 누르면 테이블이 삭제가 되는 것을 볼 수 있을 것이다.

이렇게 sqflite 패키지의 사용법에 대해서 간단하게 알아보았다. 이 패키지를 사용해 보면서 느낀 점은 SQL을 사용해 본 적 없는 사람들에게 있어서 굉장히 좋은 입문 발판이 되어주는 패키지가 아닌가..라는 느낌이었다. 솔직히 이거 쓸빠에 그냥 SQLite를 쓰지..

암튼 도움이 되었길 바라며 마치겠다.

반응형