이전 글까지 Flutter로 만든 메신저 앱, 잘 작동하긴 하지만 뭔가 부족하지 않으셨나요?
오늘은 메신저 앱에 닉네임 설정과 프로필 이미지 표시 기능을 추가해 보면서,
좀 더 사용자다운 느낌, 그리고 예쁜 인터페이스를 만드는 법을 알아보겠습니다!
이 글은 초보자도 쉽게 따라 할 수 있도록 하나하나 순서대로,
그리고 코드 예제까지 포함해서 설명할게요. 😉
📌 오늘 목표
요약
- 사용자가 앱 처음 실행할 때 닉네임과 프로필 이미지를 설정할 수 있어요.
- 메시지를 보낼 때 닉네임과 이미지도 함께 저장돼요.
- 메시지 목록에서 닉네임과 프로필 이미지가 함께 보여요.
🧰 사용 기술 요약
- Firebase Realtime Database : 메시지 저장용
- SharedPreferences : 닉네임, 이미지 경로를 로컬에 저장함
- ImagePicker : 갤러리에서 사진 선택
닉네임 & 프로필 이미지 설정 화면 만들기
보통 카카오톡이나 다른 메신저를 사용해보셨다면 알겠지만,
메시지를 보내는 화면과 프로필, 닉네임을 설정할 수 있는 화면이 달라요.
닉네임과 프로필을 설정할 수 있는 화면을 하나 따로 만들어볼게요.
UserSetupScreen이라는 화면을 하나 만들어봐요.
먼저 image_picker와 shared_preferences 패키지를 사용하기 위해서 pubspec.yaml에 의존성을 추가해주세요.

user_setup_screen.dart
새로운 유저 설정 화면을 구현해볼까요? 아래와 같이 코드를 작성해주세요.
import 'package:easy_messenger/chat_screen.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io';
class UserSetupScreen extends StatefulWidget {
@override
_UserSetupScreenState createState() => _UserSetupScreenState();
}
class _UserSetupScreenState extends State<UserSetupScreen> {
final TextEditingController _nameController = TextEditingController();
File? _selectedImage;
void _pickImage() async {
final picked = await ImagePicker().pickImage(source: ImageSource.gallery);
if (picked != null) {
setState(() {
_selectedImage = File(picked.path);
});
}
}
void _saveUserInfo() async {
if (_nameController.text.isEmpty || _selectedImage == null) return;
final prefs = await SharedPreferences.getInstance();
prefs.setString('nickname', _nameController.text);
prefs.setString('profileImagePath', _selectedImage!.path);
if (context.mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => ChatScreen()),
(route) => false,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('닉네임 설정')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
GestureDetector(
onTap: _pickImage,
child: CircleAvatar(
radius: 50,
backgroundImage:
_selectedImage != null ? FileImage(_selectedImage!) : null,
child:
_selectedImage == null
? Icon(Icons.camera_alt, size: 40)
: null,
),
),
SizedBox(height: 16),
TextField(
controller: _nameController,
decoration: InputDecoration(labelText: '닉네임 입력'),
),
SizedBox(height: 16),
ElevatedButton(onPressed: _saveUserInfo, child: Text('시작하기')),
],
),
),
);
}
}
코드 구현 설명
- _pickImage 함수에서는 image_picker 라이브러리를 사용해서 갤러리의 이미지를 불러와서 저장하는 기능을 구현했어요.
선택된 이미지는 CircleAvatar 위젯을 통해서 프로필 사진처럼 보여줄거에요. - _saveUserInfo 함수에서는 닉네임을 입력하고, 사진을 선택했을 때에 그 값들을 shared preferences에 저장하는 기능을 구현했어요. 닉네임과 프로필 이미지의 파일 위치가 저장이 완료되면 Navigator를 통해서 ChatScreen으로 자동으로 이동할거에요.
그리고 이제 앱을 처음 열었을 때 유저가 먼저 프로필 설정을 할 수 있도록 설정 화면이 보이면 좋겠죠?
main.dart 파일도 아래와 같이 바꿔볼게요.
main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'chat_screen.dart';
import 'user_setup_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// 사용자 설정 여부 확인
final prefs = await SharedPreferences.getInstance();
final hasNickname = prefs.containsKey('nickname');
final hasProfileImage = prefs.containsKey('profileImagePath');
runApp(MyApp(showSetupScreen: !(hasNickname && hasProfileImage)));
}
class MyApp extends StatelessWidget {
final bool showSetupScreen;
const MyApp({required this.showSetupScreen});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 메신저',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: showSetupScreen ? UserSetupScreen() : ChatScreen(),
);
}
}
코드 구현 설명
main함수에서 MyApp을 생성하기 전에, shared preferences 를 먼저 체크하는 거에요. 닉네임과 프로필이 설정되어 있지 않다면 UserSetupScreen 화면을 먼저 보여주고, 이미 닉네임과 프로필 이미지 설정을 완료했으면 바로 ChatScreen이 켜지는 거에요.
user_setup_screen.dart를 구현하고, main.dart 함수를 변경하고 앱을 실행해보면 다음 이미지처럼 앱을 켰을 때 바로 설정 화면으로 이동하는 것을 볼 수 있어요. 하지만 닉네임과 프로필 이미지를 설정하고 접속해도 아직 채팅 화면에는 아무런 변화가 없어요. 이제 채팅 화면에서 닉네임과 프로필 이미지가 보이도록 수정을 해볼까요?
채팅 화면에서 사용자 정보 불러오기
chat_screen.dart
채팅 화면 코드를 다음과 같이 변경해볼게요.
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart'; // 시간 포맷을 위해 필요해요.
import 'dart:io';
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _controller = TextEditingController();
final DatabaseReference _messagesRef = FirebaseDatabase.instance.ref(
'messages',
);
String? myNickname;
String? myProfilePath;
void _loadUserInfo() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
myNickname = prefs.getString('nickname') ?? '익명';
myProfilePath = prefs.getString('profileImagePath');
});
}
@override
void initState() {
super.initState();
_loadUserInfo();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter 메신저')),
body: Column(
children: [
Expanded(
child: StreamBuilder(
stream: _messagesRef.orderByChild('timestamp').onValue,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.snapshot.value != null) {
Map data = snapshot.data!.snapshot.value as Map;
List items = data.entries.map((e) => e.value).toList();
items.sort(
(a, b) => a['timestamp'].compareTo(b['timestamp']),
); // 시간순
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
print(
'[메세지 $index] ${DateFormat('HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(items[index]['timestamp']))}',
);
bool isMe = items[index]['sender'] == myNickname;
return _buildMessageBubble(
message: items[index],
isMe: isMe,
);
},
);
} else {
return Center(child: Text('아직 메시지가 없어요.'));
}
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(labelText: '메시지 입력'),
),
),
IconButton(icon: Icon(Icons.send), onPressed: _sendMessage),
],
),
),
],
),
);
}
void _sendMessage() {
if (_controller.text.isNotEmpty) {
_messagesRef.push().set({
'text': _controller.text,
'sender': myNickname,
'profile': myProfilePath,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
_controller.clear();
}
}
Widget _buildMessageBubble({required Map message, required bool isMe}) {
final profilePath = message['profile'];
return Row(
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (isMe && profilePath != null)
CircleAvatar(backgroundImage: FileImage(File(profilePath))),
Container(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: isMe ? Colors.blueAccent : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message['sender'] ?? '익명',
style: TextStyle(fontSize: 12, color: Colors.black54),
),
Text(
message['text'] ?? '',
style: TextStyle(color: isMe ? Colors.white : Colors.black),
),
],
),
),
],
);
}
}
코드 변경점 설명
- myNickname을 이전에는 "나" 라는 텍스트를 설정해두었지만, 이제는 _loadUserInfo 함수를 통해서 shared preferences에 저장해둔 내 닉네임 값을 읽게 되었어요. 이제는 저장된 닉네임을 사용해서 채팅을 할거에요.
- _sendMessage 함수에서는 프로필 이미지의 저장 위치까지 한 번에 전송하도록 변경했어요.
- _buildMessageBundle 함수의 레이아웃이 변경되었어요.
- 이전 버전에서는 메세지가 가로로 너무 길게 보였는데, 메세지의 크기에 따라서 버블의 크기가 조절되도록 했어요.
- 메시지 버블에 내 닉네임과 프로필 사진이 보여요.
chat_screen.dart 함수까지 변경하고 앱을 실행해보면, 아래 사진처럼 새로운 메세지를 보냈을 때 설정 화면에서 설정한 내 닉네임과 프로필 이미지가 잘 보이는 것을 확인할 수 있어요.
❗ 로컬 이미지 경로의 한계
이번 예제에서는 사용자가 선택한 프로필 이미지를 ImagePicker()로 가져온 뒤,
그 경로를 SharedPreferences에 저장하고, 메시지를 보낼 때도 해당 경로를 함께 저장했습니다.
이 방식은 간단하고 빠르게 구현할 수 있다는 장점이 있지만,
실제 서비스 환경에서는 몇 가지 중요한 문제가 발생합니다.
Flutter를 한 번 종료했다가 flutter run 명령어로 다시 앱을 실행해보면 바로 문제를 확인할 수 있을거에요.
껐다가 키기만 했는데도 분명히 보이던 내 프로필 이미지가 안보이게 돼요.
1. 📁 임시 경로는 사라질 수 있어요
ImagePicker()로 불러온 이미지는 보통 기기의 임시 저장소(cache 디렉토리) 에 저장됩니다.
이 디렉토리는 앱을 껐다 켜거나, 일정 시간이 지나면 시스템에 의해 자동으로 비워질 수 있어요. 🥲
그 결과, 사용자가 앱을 재실행했을 때 이전에 설정한 프로필 이미지가
더 이상 존재하지 않아서 표시되지 않는 문제가 생깁니다.
즉, 내 앱 안에서도 이미지가 깨지는 상황이 발생할 수 있는 거죠.
2. 🔒 내 이미지, 내 기기에서만 유효해요
또 하나의 중요한 문제는 저장된 이미지 경로가 내 스마트폰에서만 유효하다는 점입니다.
예를 들어, 내가 메시지를 보낼 때 내 기기 내부의 이미지 경로를 메시지와 함께 저장했다면,
그 메시지를 받은 상대방은 해당 경로에 접근할 수 없습니다. 🥲
상대방의 기기에는 그 이미지 파일이 존재하지 않기 때문에
상대방은 내 프로필 이미지를 전혀 볼 수 없게 됩니다.
이처럼 로컬 경로 방식은 이미지 공유가 불가능한 구조입니다.
지금 방식은 혼자 테스트하거나 단기적으로 기능을 구성할 때는
빠르고 편하게 사용할 수 있지만,
멀티 유저 기반의 서비스로 발전시키기에는 구조적으로 부족합니다.
마무리하며
이 문제들을 해결하기 위해 다음 글에서는
Firebase Storage를 활용해 프로필 이미지를 안정적으로 저장하고 공유하는 방법을 다룰 예정입니다. 😆
Firebase Storage에 이미지를 업로드하고, 해당 이미지의 다운로드 URL을 얻어 메시지에 포함시키면, 모든 사용자가 어떤 기기에서든 동일한 이미지에 접근할 수 있게 됩니다.
이 방식은 실제 메신저 서비스에서 사용하는 표준적인 방법이에요.
다음 글에서는 다음과 같은 내용을 다룰 예정이에요:
- firebase_storage 패키지로 이미지 업로드하기
- 업로드된 이미지의 다운로드 URL 받아오기
- URL을 Firebase Realtime Database에 함께 저장하기
- NetworkImage로 프로필 이미지 안정적으로 표시하기
이제부터는 진짜 서비스 수준의 사용자 경험을 만들 수 있어요.
우리 메신저, 다음 단계로 함께 나아가 봅시다! 💬🚀
👉 “Firebase Storage로 프로필 이미지 저장하고 공유하기” 편에서 계속됩니다!
'Flutter 개발' 카테고리의 다른 글
Flutter 메신저 앱 업그레이드: Firebase Storage로 프로필 이미지 저장하고 공유하기 (0) | 2025.05.04 |
---|---|
Flutter 메신저 업그레이드: 메시지 버블, 시간, 닉네임 추가 방법 (코드 변경점 자세히 설명) (0) | 2025.04.27 |
Flutter로 Firebase Realtime Database를 활용한 간단한 메신저 앱 만들기 (예제 코드 제공) (0) | 2025.04.27 |
VSCode에서 콘솔라스(Consolas) 폰트 적용하는 쉬운 방법! (0) | 2025.03.06 |
Flutter 개발자를 위한 필수 VSCode 확장 프로그램 추천! (0) | 2025.03.06 |