Flutter 개발-5: 로컬 저장소 추가

Flutter를 사용한 모바일 앱 개발 시리즈의 다섯 번째 포스팅에 오신 것을 환영합니다. 이번 포스팅에서는 이전에 만든 To-Do 리스트 앱에 로컬 저장소를 추가하여 앱의 데이터 지속성을 구현해 보겠습니다.

왜 로컬 저장소가 필요한가?

모바일 앱에서 사용자 데이터의 지속성은 매우 중요합니다. 사용자가 앱을 종료하고 다시 열었을 때 이전의 데이터가 그대로 유지되어야 좋은 사용자 경험을 제공할 수 있습니다. 특히 To-Do 리스트와 같은 앱에서는 이 기능이 필수적입니다.

SharedPreferences 소개

Flutter에서는 간단한 키-값 쌍의 데이터를 저장하기 위해 ‘SharedPreferences’라는 플러그인을 제공합니다. 이 플러그인은 iOS의 NSUserDefaults와 Android의 SharedPreferences를 Flutter에서 쉽게 사용할 수 있게 해줍니다.

1. 패키지 추가하기

먼저, shared_preferences 패키지를 추가해야 합니다. pubspec.yaml 파일에 다음 줄을 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.8

그리고 터미널에서 다음 명령어를 실행합니다:

flutter pub get

2. 코드 수정하기

이제 lib/main.dart 파일을 다음과 같이 수정합니다:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  List<Todo> todos = [];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My To-Do List'),
      ),
      body: TodoList(todos: todos, onTodoToggle: _toggleTodo),
      floatingActionButton: FloatingActionButton(
        onPressed: _addTodo,
        child: Icon(Icons.add),
      ),
    );
  }

  void _addTodo() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        String newTodo = "";
        return AlertDialog(
          title: Text('Add a new todo'),
          content: TextField(
            onChanged: (value) {
              newTodo = value;
            },
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Add'),
              onPressed: () {
                setState(() {
                  todos.add(Todo(title: newTodo));
                  _saveTodos();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _toggleTodo(Todo todo) {
    setState(() {
      todo.isCompleted = !todo.isCompleted;
      _saveTodos();
    });
  }

  void _loadTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String? todosString = prefs.getString('todos');
    if (todosString != null) {
      List<dynamic> todoList = jsonDecode(todosString);
      setState(() {
        todos = todoList.map((item) => Todo.fromJson(item)).toList();
      });
    }
  }

  void _saveTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String todosString = jsonEncode(todos.map((todo) => todo.toJson()).toList());
    await prefs.setString('todos', todosString);
  }
}

class TodoList extends StatelessWidget {
  final List<Todo> todos;
  final Function(Todo) onTodoToggle;

  TodoList({required this.todos, required this.onTodoToggle});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(
            todos[index].title,
            style: TextStyle(
              decoration: todos[index].isCompleted ? TextDecoration.lineThrough : null,
            ),
          ),
          trailing: Checkbox(
            value: todos[index].isCompleted,
            onChanged: (_) => onTodoToggle(todos[index]),
          ),
        );
      },
    );
  }
}

class Todo {
  String title;
  bool isCompleted;

  Todo({required this.title, this.isCompleted = false});

  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      title: json['title'],
      isCompleted: json['isCompleted'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'isCompleted': isCompleted,
    };
  }
}

3. 코드 설명

  • _loadTodos() 메서드: 앱이 시작될 때 SharedPreferences에서 저장된 To-Do 항목들을 불러옵니다. 이 메서드는 initState()에서 호출되어 앱 시작 시 데이터를 로드합니다.
  • _saveTodos() 메서드: To-Do 리스트가 변경될 때마다 SharedPreferences에 저장합니다. 이 메서드는 새 항목 추가나 완료 상태 변경 시 호출됩니다.
  • Todo 클래스: fromJsontoJson 메서드를 추가하여 JSON 형식으로 변환할 수 있게 했습니다. 이는 객체를 저장하고 불러오는 데 필요합니다.

JSON 직렬화의 필요성

SharedPreferences는 문자열, 정수, 부울 등의 기본 데이터 타입만을 저장할 수 있습니다. 그러나 우리의 To-Do 항목은 객체 형태입니다. 이를 해결하기 위해 우리는 To-Do 객체를 JSON 형식으로 변환하여 저장하고, 불러올 때 다시 객체로 변환하는 과정이 필요합니다. 이를 JSON 직렬화와 역직렬화라고 합니다.

비동기 프로그래밍의 중요성

로컬 저장소에 데이터를 저장하고 불러오는 작업은 시간이 걸리는 작업입니다. 이러한 작업을 메인 스레드에서 동기적으로 처리하면 앱의 성능이 저하되고 사용자 경험이 나빠질 수 있습니다. 따라서 우리는 이러한 작업을 비동기적으로 처리해야 합니다. Dart 언어는 ‘async’와 ‘await’ 키워드를 제공하여 비동기 프로그래밍을 쉽게 구현할 수 있게 해줍니다.

앱 실행하기

터미널에서 다음 명령어를 실행하여 앱을 시작합니다:

flutter run

이제 앱을 실행하면 To-Do 항목들이 로컬에 저장되어, 앱을 종료하고 다시 열어도 데이터가 유지됩니다.

마무리

이번 포스팅에서는 To-Do 리스트 앱에 로컬 저장소 기능을 추가하여 데이터 지속성을 구현해 보았습니다. 이를 통해 Flutter에서의 데이터 저장 방법과 비동기 프로그래밍의 기본을 배웠습니다. 이러한 방식으로 로컬 저장소를 구현함으로써, 우리의 To-Do 리스트 앱은 사용자 데이터의 지속성을 확보하게 되며, 더 나은 사용자 경험을 제공할 수 있게 됩니다.

다음 포스팅에서는 이 앱의 UI를 개선하고 할 일 삭제, 수정 등의 추가 기능을 구현해 보도록 하겠습니다. Flutter 개발에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!

관련 포스트

Flutter 개발-1 – CSAI

Flutter 개발-2: 프로젝트 생성 및 구조 파악 – CSAI

Flutter 개발-3: To-Do 리스트 앱 UI 구현하기 – CSAI

Flutter 개발-4: 앱 상태 관리 추가하기 – CSAI

관련 리소스

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다