Flutter로 간단한 노트 앱을 만들어보자 ⑥ - 노트에 중요 표시 라벨 붙이기
이번 글에서는 우리가 작성한 노트에 라벨 속성을 추가하여 ⭐중요 표시⭐를 할 수 있도록 구현해보겠습니다.
중요 표시의 결과는 다음과 같습니다.
노트 목록 화면에 보이는 노트 카드에 별 아이콘을 추가하고 사용자가 클릭을 하면 토글이 되도록 하였습니다.
어떤가요? 굉장히 중요해 보이지 않나요? 🤭
주요 개발 내용
1. 기존 노트 모델에 label 속성을 추가합니다.
2. 기존 노트 목록 화면에서 노트 카드를 컴포넌트로 분리합니다.
3. 노트 카드 컴포넌트에 label 수정 기능을 추가합니다.
1. 노트 label 속성 추가하기
NoteLabel Enum 추가
NoteLabel이라는 Enum을 추가합니다. 우선 none, important 두 가지로 정의합니다. 중요 표시를 불린 값으로 정의하지 않고 라벨Enum 으로 정의한 이유는 추후 이 라벨을 이용하여 중요 표시 뿐만 아니라 다양하게 활용해보고 싶기 때문입니다.
enum NoteLabel {
none, // 일반 노트 (중요도 없음)
important, // 중요한 노트
}
NoteModel 에 label 속성 추가
NoteModel 클래스에 label 속성을 추가하여, 각 노트가 중요도 정보를 가질 수 있도록 합니다. label은 기본적으로 NoteLabel.none으로 설정합니다.
class NoteModel {
final String id;
final String content;
final DateTime createdAt;
final DateTime updatedAt;
final NoteLabel label; // 노트의 중요도를 나타내는 필드 추가
NoteModel({
required this.id,
required this.content,
required this.createdAt,
required this.updatedAt,
this.label = NoteLabel.none, // 기본값은 none
});
속성을 추가했다면, NoteModel을 JSON 형식으로 변환할 때 label 필드를 문자열로 저장할 수 있도록 toJson과 fromJson 메소드도 업데이트 합시다.
// JSON을 NoteModel 객체로 변환
factory NoteModel.fromJson(Map<String, dynamic> json) {
return NoteModel(
id: json['id'],
content: json['content'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
label: NoteLabel.values.firstWhere(
(e) => e.name == json['label'], // 문자열을 Enum으로 변환
),
);
}
// NoteModel 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'label': label.name, // Enum을 문자열로 변환
};
}
2. 노트 카드 컴포넌트 분리
기존 노트 목록 화면에서는 NoteListView가 직접 노트를 그려 반환하고 있었습니다. 하지만 NoteListView에 필터나 정렬 등의 기능들을 추가하면서 코드가 많이 길어졌는데요. 가독성과 효율적인 유지보수를 위해 개별 노트 항목을 노트 카드 컴포넌트로 분리합니다.
이러한 리팩터링 과정은 새로운 기능(중요 표시 라벨)을 추가하기 위한 사전 작업입니다.
NoteCard 컴포넌트 분리
NoteCard는 개별 노트 항목을 구성하는 컴포넌트로, 노트 내용을 표시하고 수정 및 삭제 기능을 제공하는 버튼을 포함합니다.
NoteCard 컴포넌트는 note 파라미터를 통해 노트의 정보를 전달받아 표시합니다. 그리고 onEdit과 onDelete 처럼 파라미터를 통해 각각 삭제 및 수정 기능을 노트 항목에 연결시킵니다.
// lib/components/note_card.dart
import 'package:flutter/material.dart';
import 'package:ttingnote/models/note_model.dart';
import 'package:ttingnote/utils/color_palette.dart';
class NoteCard extends StatelessWidget {
final NoteModel note;
final Future<void> Function(String) onDelete;
final Future<void> Function(String) onEdit;
const NoteCard({
super.key,
required this.note,
required this.onDelete,
required this.onEdit,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
note.content,
style: TextStyle(color: ColorPalette.textWhite),
),
subtitle: Text(
note.createdAt.toString(),
style: TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit_outlined, color: Colors.blue),
onPressed: () async {
await onEdit(note.id);
},
),
IconButton(
icon: Icon(Icons.delete_outline, color: Colors.red),
onPressed: () async {
await onDelete(note.id);
},
),
],
),
);
}
}
onEdit과 onDelete 처럼 콜백 함수를 이용하는 이유는 NoteCard 컴포넌트와 view 간에 관심사를 분리하기 위함입니다. 이렇게 분리하면 부모 컴포넌트(여기서는 view)가 해당 동작들을 자유롭게 정의할 수 있고, NoteCard 컴포넌트는 순수하게 UI로직만 담당할 수 있어 유지보수에 유리합니다.
NoteListView에서 NoteCard 컴포넌트 적용
기존에는 NoteListView에서 ListTile에 직접 노트 정보를 표시하고 버튼을 추가했었습니다. 그 역할과 책임을 NoteCard 컴포넌트로 넘겼으니 이를 반영해 NoteListView 내의 코드 길이를 줄이고 가독성을 높여보겠습니다.
// lib/views/note_list_view.dart
import 'package:provider/provider.dart';
import 'package:ttingnote/services/note_service.dart';
import 'package:ttingnote/utils/color_palette.dart';
import 'package:ttingnote/utils/sort.dart';
import 'package:ttingnote/utils/string_similarity.dart';
import 'package:ttingnote/components/note_card.dart';
...
return ListView.builder(
itemCount: sortedNotes.length,
itemBuilder: (context, index) {
final note = sortedNotes[index];
return NoteCard(
note: note,
onDelete: (id) async {
await noteService.deleteNoteById(id);
},
onEdit: (id) async {
await context.push('/noteDetail?id=$id');
},
);
},
);
노트를 NoteCard로 표현하고 onDelete와 onEdit 콜백을 설정해 버튼 기능을 주입합니다.
3. label 수정 기능 추가
사전 준비가 끝났으니 노트 카드 컴포넌트에 라벨 수정 기능을 추가하겠습니다. 그리고 기존의 노트 업데이트 로직을 리팩터링하여 내용(content)뿐만 아니라 라벨(label) 속성도 수정할 수 있도록 변경하겠습니다.
라벨 수정 기능은 별 아이콘 버튼을 추가하여 토글을 하면 ⭐(중요 표시), 다시 토글을 하면 ☆(중요 표시 해제)가 되도록 구현해보겠습니다.
NoteCard 컴포넌트에서 라벨 변경 버튼 추가
NoteCard에 라벨 변경을 위한 아이콘 버튼을 추가합니다. 이 버튼을 통해 사용자는 노트의 중요도를 중요와 없음으로 전환할 수 있습니다.
// lib/components/note_card.dart
class NoteCard extends StatelessWidget {
final NoteModel note;
final Future<void> Function(String) onDelete;
final Future<void> Function(String) onEdit;
final Future<void> Function(String, NoteLabel) onLabelUpdate; // 콜백 파라미터 추가
const NoteCard({
super.key,
required this.note,
required this.onDelete,
required this.onEdit,
required this.onLabelUpdate,
});
...
IconButton(
icon: Icon(
note.label == NoteLabel.none ? Icons.star_border : Icons.star,
color: note.label == NoteLabel.important
? Colors.yellow
: Colors.grey[700],
),
onPressed: () async {
NoteLabel nextLabel;
switch (note.label) {
case NoteLabel.none:
nextLabel = NoteLabel.important;
break;
case NoteLabel.important:
nextLabel = NoteLabel.none;
break;
}
await onLabelUpdate(note.id, nextLabel);
},
),
note.label이 important일 때 노란색 별⭐을 표시하여 중요도를 시각적으로 나타냅니다.
사용자가 클릭(onPressed)할 때, NoteLabel.none이라면 important 를, NoteLabel.important라면 none 으로 속성 값을 업데이트 합니다.
NoteModel에 copyWith 메소드 추가
copyWith 메소드를 추가하여 NoteModel의 인스턴스의 일부 필드를 변경한 채로 새로운 인스턴스를 만들 수 있도록 합니다. copyWith을 만들어두면 이후 특정 필드만 업데이트할 때 편리하게 사용할 수 있습니다.
// lib/models/note_model.dart
NoteModel copyWith({
String? id,
String? content,
NoteLabel? label,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return NoteModel(
id: id ?? this.id,
content: content ?? this.content,
label: label ?? this.label,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
이 메소드는 새로운 값을 전달받은 필드만 변경하고, 나머지는 기존 값을 유지한 상태로 새로운 NoteModel 객체를 반환합니다. 예를 들어, 라벨만 변경할 때 기존 필드 값을 유지하고 라벨 필드만 새 값으로 변경할 수 있습니다.
NoteRepository와 NoteService 수정
NoteRepository의 updateNote 메소드는 기존에 content 값을 받아서 업데이트 했었습니다. 이제는 NoteModel 전체를 받아 업데이트하도록 변경합니다.
// lib/repositories/note_repository.dart
Future<void> updateNote(NoteModel note) async {
final prefs = await SharedPreferences.getInstance();
List<NoteModel> notes = await fetchNotes();
final index = notes.indexWhere((n) => n.id == note.id);
if (index != -1) {
notes[index] = note;
final notesJson = jsonEncode(notes.map((n) => n.toJson()).toList());
await prefs.setString(_key, notesJson);
}
}
NoteService의 updateNote 메소드는 라벨이나 콘텐츠만 선택적으로 받아 위에서 정의한 copyWith 메서드를 통해 새로운 note 객체를 생성합니다. 복사한 객체의 속성 값을 수정하여 이를 NoteRepository updateNote 메소드에 전달합니다.
// lib/services/note_service.dart
Future<void> updateNote(String id, {String? content, NoteLabel? label}) async {
final note = notes.firstWhere((note) => note.id == id);
final updatedNote = note.copyWith(
content: content ?? note.content,
label: label ?? note.label,
updatedAt: DateTime.now(),
);
await _noteRepository.updateNote(updatedNote);
final index = notes.indexWhere((note) => note.id == id);
notes[index] = updatedNote;
notifyListeners();
}
NoteListView에서 라벨 변경 로직 적용
위에서 NoteCard 컴포넌트에 라벨 변경 버튼을 추가했었습니다. 이 버튼에 사용할 콜백 함수 onLabelUpdate 를 NoteCard 컴포넌트에 주입하여 라벨을 변경할 수 있도록 합니다.
// lib/views/note_list_view.dart
NoteCard(
note: note,
onDelete: (id) async {
await noteService.deleteNoteById(id);
},
onEdit: (id) async {
await context.push('/noteDetail?id=$id');
},
onLabelUpdate: (id, label) async { // 콜백함수 파라미터 추가
await noteService.updateNote(id, label: label);
},
);
중요 표시 기능의 최종 결과를 영상으로 보겠습니다.
마무리
이번 글에서는 노트에 중요 표시 라벨 기능을 추가하여, 중요도가 시각적으로 표시되고 클릭 한 번으로 간편하게 토글될 수 있도록 구현해보았습니다. 이 기능을 통해 사용자는 노트의 중요도를 빠르게 확인하고 정리할 수 있게 되었습니다.
또한, NoteCard 컴포넌트 분리와 노트 업데이트 로직 리팩터링을 통해 코드의 가독성을 높이고, 유연하게 노트 속성을 업데이트할 수 있도록 개선했습니다.
다음 글에서는 이번에 만든 라벨, 생성, 수정 일시를 기반으로 노트 정렬 기능을 구현해보겠습니다.
[Flutter로 간단한 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.