wan의 개발일기

[Flutter] 7. 메시지 삭제, 그룹 나가기, 다크모드, 비밀번호 찾기 — 마무리 본문

Flutter

[Flutter] 7. 메시지 삭제, 그룹 나가기, 다크모드, 비밀번호 찾기 — 마무리

wan_ 2026. 6. 24. 15:06

소스코드 → github.com/Jywan/Flutter_Learning

들어가며

시리즈의 마지막 편이다. 큰 기능 하나씩이 아니라, 그동안 미뤄뒀던 자잘하지만 실제 채팅 앱이라면 당연히 있어야 할 것들을 모아서 정리한다 — 메시지 삭제, 그룹 채팅방 나가기, 다크 모드, 그리고 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는 MaterialApptheme(라이트)와 darkTheme을 동시에 등록해두고, themeMode 값으로 전환한다. 전역에서 테마를 바꾸려면 어디서든 접근 가능한 상태가 필요해서, main.dartValueNotifier를 전역 변수로 뺐다.

final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.light);

MyAppbuild()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 하나의 패턴(쿼리 구독 → 화면 자동 갱신)을 거의 모든 기능에 반복해서 써먹은 셈이다. 백엔드를 따로 안 만들어도 이 정도 실시간 기능을 붙일 수 있다는 게 이번 프로젝트에서 가장 크게 느낀 점이었다.