Flutter 개발

Flutter 메신저 앱 업그레이드: Firebase Storage로 프로필 이미지 저장하고 공유하기

withmilk 2025. 5. 4. 15:22

지난 글에서는 사용자가 선택한 이미지를 로컬에 저장하고,
SharedPreferences를 통해 경로를 기억하는 방식으로 프로필 이미지를 처리했어요. 
이 방식은 간단하고 빠르지만, 실제 서비스 환경에서는 치명적인 문제가 있었죠. 🥲
로컬 이미지 경로는 내 기기에서만 유효하고, 임시 저장소이기 때문에 앱 재실행 후 이미지가 사라질 수도 있었어요.

 

이제 이런 문제를 완전히 해결할 차례입니다.
이번 글에서는 Firebase Storage를 활용해 이미지를 클라우드에 저장하고,
모든 사용자 기기에서 안정적으로 이미지를 불러오는 방법을 알아보겠습니다.


✅ 우리가 만들 기능 요약

  • 사용자가 선택한 프로필 이미지를 Firebase Storage에 업로드
  • 업로드 후 생성되는 이미지 URL을 SharedPreferences와 Firebase Database에 저장
  • 채팅 화면에서 이 URL을 이용해 이미지 표시 (NetworkImage 사용)

🔧 Firebase Storage 설정하기

1. Firebase Console에서 Storage 활성화

  1. Firebase Console에 접속
  2. 프로젝트 선택 후, 왼쪽 메뉴에서 Storage 클릭
  3. Spark 요금제를 사용하고 있다면, Blaze로 업그레이드 (Blaze로도 무료 사용이 가능해요)
  4. 무료 위치에 테스트 규칙으로 Storage 설정
⚠️ 주의: 이 규칙은 개발 테스트용입니다.
실제 앱에서는 반드시 인증 기반 규칙(request.auth != null)을 사용해야 해요.

 

처음 Storage에 접속하면 다음 이미지 처럼 프로젝트 업그레이드가 필요하다는 메세지가 보여요.

프로젝트 요금제를 변경해야한다는 메세지가 보이는 모습

 

이미 결제 계정이 있는 경우에는 생성한 결제 계정으로 진행할 수도 있고, 새로 Cloud Billing 계정을 만들어서 진행해도 돼요.

결제 계정을 선택하는 화면

 

요금제를 Blaze로 변경해도 무료 할당량이 있어서, 충분히 우리가 만드는 메신저에서는 무료로 사용할 수 있어요.

요금제 변경이 완료된 모습

 

Firebase 공식 홈페이지에 나와있는 요금제 설명 페이지를 확인해봐도, 

Blaze 요금제로 업그레이드 하더라도 무료로 사용할 수 있는 한도는 똑같은 것을 알 수 있어요.

공식 홈페이지의 Pricing 설명

 

테스트 모드 규칙으로 우리의 메신저에 기능을 추가해볼게요.

테스트 모드 규칙으로 설정하면, 생성한 날짜로부터 한달 동안만 Storage를 사용할 수 있는 규칙이 설정되는 거에요.

Storage 생성 시 규칙 선택하는 화면


💡 여기서 잠깐: Blaze 요금제 관련 안내

Firebase Storage를 설정할 공개 읽기/쓰기 규칙을 사용하려고 하면,
(버킷 규칙을 allow read, write로 조건 없이 설정하려고 하면)
다음과 같은 경고를 보게 될 수 있어요. 

❗ “이 규칙은 Spark 요금제에서는 허용되지 않습니다. Blaze 요금제로 업그레이드해야 합니다.”

이는 Firebase의 보안 정책 때문인데요,
Spark(무료) 요금제에서는 인증되지 않은 사용자의 Storage 접근을 허용할 수 없기 때문입니다.
그래서 allow read, write;처럼 모든 사용자에게 열어두는 규칙은 무료 요금제에서 쓸 수 없어요.

해결 방법?

  • 1) Blaze 요금제로 업그레이드
    → 유연하게 보안 규칙을 설정할 수 있지만, 신용카드 등록이 필요합니다.
    → 이번 글에서는 Blaze 요금제로 업그레이드해서 기능을 구현해볼게요.
  • 2) 익명 로그인 기능 사용 (추천)
    → Firebase Authentication의 익명 로그인을 사용하면, 무료 요금제에서도
    allow read, write: if request.auth != null; 조건을 만족할 수 있어요.
    → 실제 서비스에서도 안전하고 확장성 높은 방식입니다.

👉 이 익명 로그인 연결 방법은 다음 글에서 자세히 소개할게요!


🧱 프로필 이미지 업로드 구현

먼저 이번 구현에서 필요한 패키지를 pubspec.yaml에 추가해줄게요.

다음과 같이 firebase_storage와, path를 추가해주세요.

새로운 의존성을 추가한 모습

 

user_setup_screen.dart

사용자가 이미지를 선택하고, Firebase Storage에 업로드한 뒤 다운로드 URL을 받아 저장하는 코드입니다. 다음과 같이 수정해볼까요?

import 'package:easy_messenger/chat_screen.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io';
import 'package:path/path.dart' as path;

class UserSetupScreen extends StatefulWidget {
  @override
  _UserSetupScreenState createState() => _UserSetupScreenState();
}

class _UserSetupScreenState extends State<UserSetupScreen> {
  final TextEditingController _nameController = TextEditingController();
  File? _selectedImage;
  bool _uploading = false;

  void _pickImage() async {
    final picked = await ImagePicker().pickImage(source: ImageSource.gallery);
    if (picked != null) {
      setState(() {
        _selectedImage = File(picked.path);
      });
    }
  }

  Future<void> _uploadAndSaveUserInfo() async {
    if (_nameController.text.isEmpty || _selectedImage == null) return;

    setState(() => _uploading = true);

    final fileName = path.basename(_selectedImage!.path);
    final ref = FirebaseStorage.instance.ref().child('profiles/$fileName');

    await ref.putFile(_selectedImage!);
    final downloadUrl = await ref.getDownloadURL();

    final prefs = await SharedPreferences.getInstance();
    prefs.setString('nickname', _nameController.text);
    prefs.setString('profileImageUrl', downloadUrl);

    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: _uploadAndSaveUserInfo,
              child:
                  _uploading
                      ? CircularProgressIndicator(color: Colors.white)
                      : Text('시작하기'),
            ),
          ],
        ),
      ),
    );
  }
}

코드 변경점 설명

_saveUserInfo 함수가 _uploadAndSaveUserInfo로 변경되었어요. 이전에는 단순히 shared preferences에 닉네임과 프로필 이미지 파일 이름을 쓰고 있었다면, 이제는 FirebaseStorage.instance.ref().child('profiles/$fileName') 로 파일을 저장할 위치를 가져온 뒤에, putFile로 파일을 업로드합니다. 

그리고는 getDownloadUrl 함수를 통해서 접근할 수 있는 url을 받아온 뒤에, 그 url을 이제 preferences에 저장해요. 이제는 Firebase에 이미지를 저장하고, 그 이미지를 읽어올 수 있는 url을 가지게 됐죠. 이 url을 사용하면 모든 유저가 이제 내가 설정해서 업로드해놓은 프로필 사진을 볼 수 있어요.

 

⚠️ flutter run을 통해서 앱을 실행하기 전에, 핸드폰에 설치되어 있는 앱을 먼저 지우셔야 해요.

이전 버전에서 이미 shared preferences에 내 닉네임과 프로필 이미지의 url이 설정되어 있기 때문에, 앱을 다시 설치하지 않으면 UserSetupScreen에 접근할 수 없어요.

 

앱을 지우고 다시 설치하면 다시 닉네임 설정 화면을 볼 수 있어요.이제 다시 닉네임과 프로필 사진을 설정해볼게요.

다시 앱을 재설치한 모습

 

업로드 되는 것을 기다리고 Firebase Storage에 접속해보면, 다음과 같이 새로운 파일이 profile/ 디렉토리에 저장된 것을 확인할 수 있어요. 이제 다른 모든 유저도 똑같이 이 프로필 사진을 볼 수 있게 됐어요. 이제는 ChatScreen을 수정해서 프로필 이미지를 볼 수 있게 해보자구요.

프로필 이미지가 드디어 Storage에 업로드된 모습


🖼 프로필 이미지 채팅화면에 표시하기

이제 Firebase에 저장된 이미지 URL을 SharedPreferences에서 불러와
채팅 메시지에 포함시키고, 채팅 화면에서 표시할 수 있어요.

chat_screen.dart 파일에서 다음 함수들을 변경해주세요.

 

사용자 정보 불러오기

myProfilePath 대신에 myProfileImageUrl 이라는변수로 변경했어요.

읽어오는 부분과, _sendMessage 부분을 변경해주세요.

String? myNickname;
String? myProfileImageUrl;

void _loadUserInfo() async {
  final prefs = await SharedPreferences.getInstance();
  setState(() {
    myNickname = prefs.getString('nickname') ?? '익명';
    myProfileImageUrl = prefs.getString('profileImageUrl');
  });
}

 

 

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      _messagesRef.push().set({
        'text': _controller.text,
        'sender': myNickname,
        'profileUrl': myProfileImageUrl,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
      });
      _controller.clear();
    }
  }

 

메시지 표시할 때 NetworkImage 사용

CircleAvatar를 만드는 부분이 변경됐어요. 이전에는 FileImage로 로컬에 있는 파일을 불러왔다면,

이제는 NetworkImage를 통해서 기기에 있는 이미지가 아니라 url을 통해서 Storage나 다른 서버에 있는 이미지를 불러올거에요.

  Widget _buildMessageBubble({required Map message, required bool isMe}) {
    return Row(
      mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      children: [
        if (message['profileUrl'] != null)
          CircleAvatar(backgroundImage: NetworkImage(message['profileUrl']))
        else
          CircleAvatar(child: Icon(Icons.person)),
        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),
              ),
            ],
          ),
        ),
      ],
    );
  }

 

모두 구현하고 새로운 메세지를 하나 보내보면 다음과 같이 Storage에 업로드된 프로필 이미지가 잘 보이는 것을 확인할 수 있어요.

Storage에 있는 이미지가 잘 보이는 모습

 

좀 더 그럴듯하게 메세지를 보내보면 아래처럼 확실히 더 실제 메신저처럼 보여요!

대화하는 것처럼 메세지 구성을 한 모습


✅ 마무리 정리

이제 우리는 Firebase Storage를 통해
로컬 경로 대신 안전한 이미지 URL을 기반으로 프로필 사진을 저장하고 공유하는 기능을 만들었어요.

  • 이미지는 클라우드에 안전하게 저장되고
  • 누구나 동일한 URL로 접근할 수 있으며
  • 채팅 메시지와 함께 사용자 프로필도 공유 가능해졌습니다!

Storage를 안전하게 사용하려면 인증된 사용자만 접근 가능하도록 해야 하는데,
그걸 무료로 해결할 수 있는 방법이 있습니다. 다음 글에서는

“Firebase 익명 로그인으로 Storage를 안전하게 쓰는 방법”
을 주제로, 인증 기반으로 Storage를 사용하는 실전 방법을 알려드릴게요.

 

이제 정말 서비스 같은 메신저 앱이 완성되어 가고 있어요!
다음 글에서 인증까지 완성해서, 안전하고 확장 가능한 구조로 발전시켜봐요. 😊

👉 “Firebase 익명 로그인으로 Storage 안전하게 쓰기” 편에서 계속됩니다!