나는 이렇게 논다/Flutter 로 간단한 노트 앱을 만들어보자

Flutter로 멋진 노트 앱을 만들어보자 ⑦ - 노트 내보내기/가져오기 기능 구현하기

daco2020 2024. 12. 3. 21:51
반응형

이번 글에서는 작성한 노트를 백업할 수 있는 노트 내보내기/가져오기 기능을 구현해 보겠습니다.

 

우리가 만드는 노트 앱은 데이터를 따로 데이터베이스에 저장하거나 외부에서 데이터를 수집하지 않고 있습니다. 그렇기 때문에 노트 백업 또한 사용자 기기 저장소에 파일 형태로 저장하도록 구현해 보겠습니다.

 

노트 내보내기/가져오기의 주요 요구사항은 다음과 같습니다.

 

노트 내보내기 - 사용자는 저장된 모든 노트를 파일로 내보낼 수 있습니다.

노트 가져오기 - 사용자는 파일로부터 노트를 가져올 수 있습니다.

 

 

목차

  1. 노트 내보내기 구현
  2. 노트 가져오기 구현
  3. SnackBar 유틸 함수 분리
  4. 설정 화면에 메뉴 추가

 

1. 노트 내보내기 구현

NoteService 클래스에 노트를 CSV 파일로 내보내는 exportNotesToCsv 메서드를 구현하겠습니다.

Future<String> exportNotesToCsv() async {
  try {
    final List<List<dynamic>> rows = [
      ['ID', 'Content', 'Created At', 'Updated At', 'Label'],
      ..._originalNotes.map((note) => [
            note.id,
            note.content,
            note.createdAt.toIso8601String(),
            note.updatedAt.toIso8601String(),
            note.label.name,
          ]),
    ];

 

 

CSV 파일로 내보내기 위해서 노트 객체를 row 라는 배열 형태로 바꿔줍니다.

 

ListToCsvConverter 를 사용하기 위해 csv 패키지를 추가합니다.

flutter pub add csv

 

배열 형태의 rows 를 csv 형태로 바꾸기 위해 ListToCsvConverter 의 convert 메서드를 호출합니다.

final csvData = const ListToCsvConverter().convert(rows);

 

변환된 csvData 를 안드로이드와 iOS 기기에 저장해 보겠습니다.

 

먼저 안드로이드 저장 방식입니다.

final fileName = '${DateTime.now().millisecondsSinceEpoch}.csv';

final result = await const MethodChannel('ttingnote/file_saver')
    .invokeMethod<String>('saveFile', {
  'fileName': fileName,
  'data': csvData,
});

return result;

 

파일 이름을 동적으로 생성하고 MethodChannel 을 통해 파일을 저장합니다.

 

다음으로 iOS 저장 방식입니다.

final directory = await getApplicationDocumentsDirectory();
final file = File(
    '${directory.path}/${DateTime.now().millisecondsSinceEpoch}.csv');
await file.writeAsString(csvData);
return file.path;

 

애플리케이션 디렉토리 경로에 파일을 저장합니다.

 

 

 

2. 노트 가져오기 구현

NoteService 클래스에 CSV 파일에서 데이터를 가져오는 importNotesFromCsv 메서드를 구현하겠습니다.

Future<void> importNotesFromCsv(String filePath) async {
  final file = File(filePath);
  final csvData = await file.readAsString();
  final List<List<dynamic>> rows =
      const CsvToListConverter().convert(csvData);

 

인자로 들어온 파일 경로를 바탕으로 CSV 파일 데이터를 rows 배열로 변환합니다.

 

rows 배열을 순회하며 row 를 노트 객체로 바꿔줍니다. 이때, 첫 번째 행은 헤더이므로 제외하고 다음 행부터 순회합니다.

for (var i = 1; i < rows.length; i++) {
  final row = rows[i];
  final note = NoteModel(
    id: row[0].toString(),
    content: row[1].toString(),
    createdAt: DateTime.parse(row[2].toString()),
    updatedAt: DateTime.parse(row[3].toString()),
    label: NoteLabel.values.firstWhere(
      (e) => e.name == row[4].toString(),
      orElse: () => NoteLabel.none,
    ),
  );

 

여기서 주의할 점이 있습니다.

 

만약 기존 노트가 이미 있고 같은 아이디를 가진 백업 노트가 들어오게 된다면 동일한 노트가 두 개가 존재하는 셈입니다.

 

동일한 노트라면 하나는 덮어쓰기 형태가 되도록 로직을 추가하겠습니다.

  final existingNote = await _noteRepository.fetchNoteById(note.id);
  if (existingNote != null) {
    // 기존 노트가 있다면 업데이트
    await _noteRepository.updateNote(note);
  } else {
    // 새로운 노트라면 저장
    await _noteRepository.saveNote(note);
  }

 

기존 노트가 있다면 노트를 업데이트하고, 없으면 새로 저장합니다.

 

마지막으로 노트 목록을 불러와 새로고침하겠습니다.

await getNotes();

 

 

 

 

 

3. SnackBar 유틸 함수 분리

기존에는 SnackBar 를 사용처에서 각각 따로 정의했었습니다. 그러다 보니 코드가 반복되고 일부 UI가 상이하여 일관성을 유지하기가 어려웠습니다.

 

SnackBar 의 일관성을 유지하고 코드를 쉽게 재사용하기 위해 SnackBar 로직을 유틸 함수로 분리하겠습니다.

 

snackbar_utils.dart 파일을 생성하고 아래 코드를 입력합니다.

void showCustomSnackBar({
  required BuildContext context,
  required String message,
  required bool isSuccess,
}) {
  if (!context.mounted) return;

  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
      behavior: SnackBarBehavior.floating,
      backgroundColor: isSuccess
          ? Theme.of(context).colorScheme.success
          : Theme.of(context).colorScheme.error,
    ),
  );
}

 

이렇게 유틸 함수로 분리하면 기본적인 설정들, 예를 들어 behavior 나 backgroundColor 값들을 미리 지정해 둘 수 있습니다. 

 

SnackBar 사용처에서는 보여주고자 하는 메시지와 성공 여부만 전달하면 되는 것이죠. 중복되는 코드를 줄이고 재사용성을 높일 수 있습니다.

 

 

4. 설정 화면에 메뉴 추가

마지막으로 설정 탭 화면(SettingsView)으로 이동하여 노트 내보내기와 가져오기 메뉴를 추가합니다.

class SettingsView extends StatelessWidget {
  // ...

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        // ...

        ListTile(
          leading: const Icon(Icons.file_upload),
          title: const Text('노트 내보내기'),
          onTap: () async {
            try {
              final noteService = context.read<NoteService>();
              final result = await noteService.exportNotesToCsv();

              if (result.isNotEmpty) {
                showCustomSnackBar(
                  context: context,
                  message: '노트를 성공적으로 내보냈습니다.',
                  isSuccess: true,
                );
              }
            } catch (e) {
              showCustomSnackBar(
                context: context,
                message: '노트 내보내기에 실패했습니다.',
                isSuccess: false,
              );
            }
          },
        ),
        ListTile(
          leading: const Icon(Icons.file_download),
          title: const Text('노트 가져오기'),
          onTap: () async {
            try {
              final result = await FilePicker.platform.pickFiles(
                type: FileType.custom,
                allowedExtensions: ['csv'],
              );

              if (result != null && result.files.single.path != null) {
                await context
                    .read<NoteService>()
                    .importNotesFromCsv(result.files.single.path!);

                showCustomSnackBar(
                  context: context,
                  message: '노트를 성공적으로 가져왔습니다.',
                  isSuccess: true,
                );
              }
            } catch (e) {
              showCustomSnackBar(
                context: context,
                message: '노트 가져오기에 실패했습니다.',
                isSuccess: false,
              );
            }
          },
        ),

        // ...
      ],
    );
  }
}

 

내보내기 버튼을 눌렀을 때에는 앞서 구현한 exportNotesToCsv 메서드를, 가져오기 버튼을 눌렀을 때에는 importNotesFromCsv 메서드를 이용해 파일을 내보내고 가져옵니다.

 

또한 사용자에게 결과에 대한 정보를 명확히 전달하기 위해 성공과 실패 케이스를 구분하여 메시지가 포함된 SnackBar 를 보여줍니다.

 

 

설정 탭의 최종 모습은 다음과 같습니다.

 

왼쪽은 기존의 UI, 오른쪽은 변경 후 UI 입니다. (일부 자잘한 수정사항은 다루지 않았습니다)

 

 

마무리

이번 글에서는 노트 내보내기와 가져오기 기능을 구현했습니다. 사용자는 이 기능을 통해 자신의 노트를 파일로 백업하거나 다른 기기로 옮겨 복원할 수 있습니다.

 

여기까지 앱의 핵심 기능과 UI를 모두 구현했습니다. 현재는 앱스토어와 플레이스토어에 출시 및 심사 중에 있습니다.

 

다음 글에서는 실제 사용자의 피드백을 받아 앱의 전반적인 사용성을 개선해 보도록 하겠습니다.

 

 

 

[Flutter로 멋진 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.
반응형