wan의 개발일기
[Flutter] 4. 프로필(닉네임/사진) 만들고 채팅 이미지까지 전송하기 본문
들어가며
이번 글은 시간을 살짝 되돌린다. 실제 커밋 순서로는 프로필 기능 → 채팅 UI 개선 → 이미지 전송이 3편(그룹 채팅)보다 먼저 만들어졌다. 시리즈 구성상 그룹 채팅을 3편에 먼저 배치했을 뿐, 코드 상으로는 이번 글에서 다루는 기능들이 더 이전 단계다. 3편에서 "이건 4편 소재"라고 미뤄뒀던 _otherUserProfileUrl, _sendImage()가 바로 여기서 나온다.
이메일/UID만 들고 있던 FirebaseAuth 유저에, "보여줄 수 있는" 정보(닉네임, 프로필 사진)를 어떻게 붙였는지, 그리고 텍스트만 주고받던 채팅에 이미지를 어떻게 끼워 넣었는지를 다룬다.
프로필 화면 만들기
새 화면 ProfileScreen을 만들고, 홈 화면 AppBar에 프로필 아이콘을 추가해서 진입할 수 있게 했다. 화면을 열면 Firestore에 저장된 닉네임/프로필 사진을 불러와 보여준다.
Future<void> _loadProfile() async {
final uid = FirebaseAuth.instance.currentUser!.uid;
final doc = await FirebaseFirestore.instance.collection('users').doc(uid).get();
if (doc.exists) {
final data = doc.data()!;
_nicknameController.text = data['nickname'] ?? '';
setState(() {
_profileImageUrl = data['profileImageUrl'];
});
}
}
닉네임 저장은 단순히 Firestore 문서를 update()하는 것으로 끝난다. 인증 정보(FirebaseAuth)와 분리해서 Firestore에 둔 덕분에, "닉네임 변경"이 곧 일반적인 DB 업데이트 한 줄이 됐다 — 2편에서 미리 그렇게 구조를 짜둔 게 여기서 빛을 본 셈이다.
Future<void> _saveNickname() async {
final nickname = _nicknameController.text.trim();
if (nickname.isEmpty) return;
final uid = FirebaseAuth.instance.currentUser!.uid;
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'nickname': nickname,
});
}
프로필 사진 업로드
이미지 업로드는 두 단계다. ① image_picker로 갤러리에서 사진을 고르고, ② firebase_storage에 올린 뒤 받은 URL을 Firestore 문서에 저장한다.
Future<void> _pickAndUploadImage() async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery, maxWidth: 512);
if (picked == null) return;
final uid = FirebaseAuth.instance.currentUser!.uid;
final ref = FirebaseStorage.instance.ref().child('profiles/$uid.jpg');
// 웹과 네이티브(Android/iOS)는 파일 다루는 방식이 다르다
if (kIsWeb) {
final bytes = await picked.readAsBytes();
await ref.putData(bytes, SettableMetadata(contentType: 'image/jpg'));
} else {
await ref.putFile(File(picked.path));
}
final url = await ref.getDownloadURL();
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'profileImageUrl': url,
});
}
웹 브라우저는 파일시스템 경로(
File)에 접근할 수 없어서readAsBytes()로 바이트째 읽어putData()로 올려야 하고, 모바일은picked.path로 실제 파일을 가리켜putFile()을 쓸 수 있다.kIsWeb분기를 안 해주면 웹에서File생성 자체가 막혀서 컴파일이 안 된다. Node.js에서 브라우저용 코드와 서버용 코드를 분기하는 것과 비슷한 느낌이다.
저장 경로를 profiles/{uid}.jpg로 고정해뒀기 때문에, 다시 업로드하면 같은 경로의 파일이 덮어써진다 — 별도로 이전 이미지를 지우는 코드가 필요 없다.
채팅에 닉네임/프로필 사진 반영하기
프로필을 만들었으니, 이제 채팅 화면에서 이메일 대신 닉네임과 프로필 사진을 보여줘야 한다. ChatScreen이 열릴 때 상대방의 이메일로 Firestore를 조회해서 닉네임/사진 URL을 가져온다.
Future<void> _loadOtherUserInfo() async {
final query = await FirebaseFirestore.instance
.collection('users')
.where('email', isEqualTo: widget.otherUserEmail)
.limit(1)
.get();
if (query.docs.isNotEmpty) {
final data = query.docs.first.data();
setState(() {
_otherUserNickname = data['nickname'] ?? widget.otherUserEmail;
_otherUserProfileUrl = data['profileImageUrl'];
});
}
}
AppBar 제목을 Text(otherUserEmail) 한 줄에서, 프로필 사진 CircleAvatar + 닉네임으로 바꿨다. 메시지 버블도 상대방 메시지마다 작은 프로필 아이콘을 붙이고, 시간(HH:mm)을 옆에 표시하도록 디자인을 다시 짰다. 채팅방 목록(home_screen.dart)도 똑같은 방식으로 FutureBuilder를 추가해서 닉네임/프로필/시간을 보여주게 했다.
메시지를 보내면
_scrollController.animateTo(maxScrollExtent)로 맨 아래로 스크롤하는 것도 이때 추가했다. 그 전까지는 메시지를 보내도 화면이 그대로 있어서, 새 메시지를 보려면 직접 스크롤을 내려야 했다.
채팅 이미지 전송
프로필 사진 업로드와 거의 같은 패턴이다. 다만 저장 경로가 chats/{chatId}/{timestamp}.jpg로, 메시지마다 새 파일이 쌓이도록 타임스탬프를 파일명에 넣었다.
Future<void> _sendImage() async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery, maxWidth: 1024);
if (picked == null) return;
setState(() => _isUploading = true);
try {
final currentUser = FirebaseAuth.instance.currentUser!;
final timestamp = DateTime.now().microsecondsSinceEpoch;
final ref = FirebaseStorage.instance
.ref()
.child('chats/${widget.chatId}/$timestamp.jpg');
if (kIsWeb) {
final bytes = await picked.readAsBytes();
await ref.putData(bytes, SettableMetadata(contentType: 'image/jpeg'));
} else {
await ref.putFile(File(picked.path));
}
final imageUrl = await ref.getDownloadURL();
// 메시지 타입을 'image'로 구분해서 저장
await FirebaseFirestore.instance
.collection('chats').doc(widget.chatId).collection('messages')
.add({
'text': '',
'imageUrl': imageUrl,
'senderId': currentUser.uid,
'senderEmail': currentUser.email,
'type': 'image',
'timestamp': FieldValue.serverTimestamp(),
});
await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).update({
'lastMessage': '사진',
'lastMessageTime': FieldValue.serverTimestamp(),
});
} finally {
setState(() => _isUploading = false);
}
}
메시지 문서에 type: 'image' 필드를 추가해서, 채팅 화면에서 버블을 그릴 때 텍스트인지 이미지인지 분기한다.
child: type == 'image'
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
msg['imageUrl'] ?? '',
width: 200,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
// 로딩 중엔 같은 크기의 스피너로 자리 차지
return const SizedBox(
width: 200, height: 150,
child: Center(child: CircularProgressIndicator()),
);
},
),
)
: Text(msg['text'] ?? ''),
업로드하는 동안엔 _isUploading 플래그로 입력창 위에 작은 로딩 인디케이터를 띄우고, 이미지 버튼도 비활성화해서 중복 전송을 막았다.
Firebase Storage CORS 설정
Flutter Web에서 테스트할 때 이미지가 안 뜨는 문제를 겪었는데, 원인은 Storage의 CORS 정책이었다. 모바일 앱은 HTTP 클라이언트가 직접 받아오니 문제가 없는데, 웹은 브라우저가 다른 origin(firebasestorage.googleapis.com)으로의 요청을 CORS 헤더로 검증한다. gsutil로 아래 설정을 Storage 버킷에 적용해서 해결했다.
[{"origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600}]
gsutil cors set cors.json gs://<your-project>.appspot.com
실행 화면
아래는 프로필 화면이다. 다크 모드 토글은 7편에서 추가된 기능이라, 이번 편 코드에는 아직 없다.
정리
| 기능 | 핵심 API / 구조 |
|---|---|
| 닉네임 저장 | Firestore users/{uid} 문서 update() |
| 프로필 사진 업로드 | FirebaseStorage + profiles/{uid}.jpg 고정 경로 |
| 채팅에 닉네임/사진 반영 | 이메일로 users 컬렉션 조회 (where + limit(1)) |
| 이미지 메시지 구분 | 메시지 문서의 type: 'image' 필드 |
| 웹/네이티브 분기 업로드 | kIsWeb + putData() / putFile() |
| 웹 이미지 로딩 문제 | Storage 버킷 CORS 설정 (gsutil cors set) |
다음 글에서는 시간 순서를 다시 따라가서, 그룹 채팅(3편) 다음에 추가된 읽음 표시와 타이핑 인디케이터를 다룬다.
'Flutter' 카테고리의 다른 글
| [Flutter] 6. 닉네임/이메일 prefix 검색 + 온라인 상태 표시 (0) | 2026.06.24 |
|---|---|
| [Flutter] 5. 읽음 표시 + 타이핑 인디케이터로 채팅 디테일 살리기 (0) | 2026.06.24 |
| [Flutter] 3. 그룹 채팅 추가하기 + authStateChanges로 로그인 유지 (0) | 2026.06.24 |
| [Flutter] 2. Firestore StreamBuilder로 1:1 실시간 채팅 구현하기 (0) | 2026.06.24 |
| [Flutter] 1. Firebase Auth로 이메일 로그인/회원가입 만들기 (0) | 2026.06.24 |