wan의 개발일기
[Flutter] 7. 메시지 삭제, 그룹 나가기, 다크모드, 비밀번호 찾기 — 마무리 본문
들어가며
시리즈의 마지막 편이다. 큰 기능 하나씩이 아니라, 그동안 미뤄뒀던 자잘하지만 실제 채팅 앱이라면 당연히 있어야 할 것들을 모아서 정리한다 — 메시지 삭제, 그룹 채팅방 나가기, 다크 모드, 그리고 1편 때부터 미뤄뒀던 비밀번호 재설정.
내 메시지 길게 눌러 삭제하기
메시지 버블을 GestureDetector로 감싸고 onLongPress를 달았다. 내가 보낸 메시지(isMe)에만 삭제를 허용해서, 남의 메시지를 못 지우게 막았다.
GestureDetector(
onLongPress: isMe ? () => _deleteMessage(messages[index].reference) : null,
child: Container(
// 기존 말풍선 디자인 그대로
),
),
삭제 자체는 확인 다이얼로그를 띄운 뒤 DocumentReference.delete() 한 줄이다. Firestore 문서는 RDB의 행(row)과 비슷해서, 참조만 있으면 삭제도 단순하다.
void _deleteMessage(DocumentReference ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('메시지 삭제'),
content: const Text('이 메세지를 삭제하시겠습니까?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('취소')),
TextButton(
onPressed: () {
ref.delete();
Navigator.pop(context);
},
child: const Text('삭제', style: TextStyle(color: Colors.red)),
),
],
),
);
}
메시지 목록이 StreamBuilder로 구독 중이기 때문에, delete()가 끝나는 순간 화면에서도 바로 사라진다. 별도로 로컬 상태(setState)를 건드릴 필요가 없다 — 이게 Firestore 실시간 구독의 장점이다.
그룹 채팅방 나가기
그룹 채팅의 AppBar에 PopupMenuButton을 추가해서 "채팅방 나가기" 메뉴를 달았다. 나가기를 누르면 그 채팅방의 users/userEmails 배열에서 내 정보만 빼는 식으로 처리한다.
final currentUser = FirebaseAuth.instance.currentUser!;
await FirebaseFirestore.instance
.collection('chats')
.doc(widget.chatId)
.update({
'users': FieldValue.arrayRemove([currentUser.uid]),
'userEmails': FieldValue.arrayRemove([currentUser.email]),
});
채팅방 문서 자체를 지우는 게 아니라 배열에서 내 uid만 빼는 방식이라, 다른 멤버들에게는 채팅방이 그대로 남는다. 그리고 홈 화면의 채팅 목록은
where('users', arrayContains: uid)쿼리로 가져오기 때문에, 내 uid가 배열에서 빠지는 순간 그 쿼리에 더 이상 걸리지 않아 목록에서 자동으로 사라진다. 별도로 "목록에서 제거"하는 코드를 추가한 게 아니라, 쿼리 조건의 부수효과로 해결된 셈이다.
다크 모드
Flutter는 MaterialApp에 theme(라이트)와 darkTheme을 동시에 등록해두고, themeMode 값으로 전환한다. 전역에서 테마를 바꾸려면 어디서든 접근 가능한 상태가 필요해서, main.dart에 ValueNotifier를 전역 변수로 뺐다.
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.light);
MyApp의 build()를 ValueListenableBuilder로 감싸서, themeNotifier 값이 바뀔 때마다 MaterialApp을 다시 그리게 했다.
return ValueListenableBuilder<ThemeMode>(
valueListenable: themeNotifier,
builder: (context, themeMode, child) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
useMaterial3: true,
),
themeMode: themeMode,
home: /* authStateChanges StreamBuilder */,
);
},
);
Redux나 Provider 같은 상태관리 라이브러리를 쓰지 않고, 전역
ValueNotifier하나로 앱 전체 테마를 바꾸는 가장 가벼운 방법이다. React로 치면 Context API 없이 모듈 스코프 변수 + 구독자 패턴을 쓰는 느낌인데, 앱 규모가 작을 땐 이 정도로도 충분했다.
토글 스위치는 프로필 화면에 SwitchListTile로 추가했고, 값이 바뀌면 themeNotifier.value를 직접 갱신한다.
ValueListenableBuilder<ThemeMode>(
valueListenable: themeNotifier,
builder: (context, themeMode, child) {
return SwitchListTile(
title: const Text('다크 모드'),
secondary: Icon(themeMode == ThemeMode.dark ? Icons.dark_mode : Icons.light_mode),
value: themeMode == ThemeMode.dark,
onChanged: (value) {
themeNotifier.value = value ? ThemeMode.dark : ThemeMode.light;
},
);
},
)
비밀번호 재설정 — 1편부터 미뤄둔 기능
1편에서 "비밀번호를 잊으셨나요?" 링크가 스크린샷에 보였지만 코드는 다루지 않았던 부분이 바로 이거다. Firebase가 제공하는 이메일 발송 API 한 줄로 끝난다.
Future<void> _resetPassword() async {
final email = _emailController.text.trim();
if (email.isEmpty) {
setState(() => _errorMessage = '이메일을 입력해주세요.');
return;
}
try {
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('비밀번호 재설정 이메일을 발송했습니다.')),
);
}
} on FirebaseAuthException catch (e) {
setState(() => _errorMessage = e.message ?? '오류가 발생했습니다.');
}
}
토큰 발급, 만료 시간 관리, 메일 발송 서버 — 이런 걸 하나도 직접 만들지 않고 Firebase Auth에 위임했다. 로그인 화면의 "비밀번호를 잊으셨나요?" 버튼은 로그인 모드(_isLogin)일 때만 보이게 했다.
정리
| 기능 | 핵심 API / 구조 |
|---|---|
| 메시지 삭제 | GestureDetector.onLongPress + DocumentReference.delete() |
| 그룹 나가기 | FieldValue.arrayRemove() (쿼리 조건 부수효과로 목록에서 자동 제거) |
| 다크 모드 | 전역 ValueNotifier<ThemeMode> + ValueListenableBuilder |
| 비밀번호 재설정 | sendPasswordResetEmail() |
시리즈를 마치며
1편 Firebase 인증부터 시작해서 7편까지, 이메일 로그인 한 줄짜리 앱이 1:1/그룹 채팅, 이미지 전송, 읽음 표시, 타이핑 인디케이터, 온라인 상태, 다크 모드까지 갖춘 채팅 앱이 됐다. 돌이켜보면 Firestore의 StreamBuilder 하나의 패턴(쿼리 구독 → 화면 자동 갱신)을 거의 모든 기능에 반복해서 써먹은 셈이다. 백엔드를 따로 안 만들어도 이 정도 실시간 기능을 붙일 수 있다는 게 이번 프로젝트에서 가장 크게 느낀 점이었다.
'Flutter' 카테고리의 다른 글
| [Flutter] 6. 닉네임/이메일 prefix 검색 + 온라인 상태 표시 (0) | 2026.06.24 |
|---|---|
| [Flutter] 5. 읽음 표시 + 타이핑 인디케이터로 채팅 디테일 살리기 (0) | 2026.06.24 |
| [Flutter] 4. 프로필(닉네임/사진) 만들고 채팅 이미지까지 전송하기 (0) | 2026.06.24 |
| [Flutter] 3. 그룹 채팅 추가하기 + authStateChanges로 로그인 유지 (0) | 2026.06.24 |
| [Flutter] 2. Firestore StreamBuilder로 1:1 실시간 채팅 구현하기 (0) | 2026.06.24 |