Firebase 연동 — Auth, Firestore, Cloud Messaging

Firebase는 Google이 제공하는 BaaS(Backend as a Service)로, Flutter와의 통합이 매우 잘 되어 있습니다. 인증, 데이터베이스, 푸시 알림을 빠르게 구현할 수 있습니다.


Firebase 초기 설정

FlutterFire CLI 설치

BASH
# FlutterFire CLI 설치
dart pub global activate flutterfire_cli

# Firebase 프로젝트 연결 (자동 설정)
flutterfire configure

이 명령어가 firebase_options.dart 파일을 자동 생성합니다.

패키지 설치

YAML
dependencies:
  firebase_core: ^3.0.0
  firebase_auth: ^5.0.0
  cloud_firestore: ^5.0.0
  firebase_messaging: ^15.0.0

초기화

DART
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

Firebase Auth — 인증

이메일/비밀번호 로그인

DART
import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // 현재 유저
  User? get currentUser => _auth.currentUser;

  // 인증 상태 스트림
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  // 회원가입
  Future<UserCredential> signUp(String email, String password) async {
    try {
      return await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _handleAuthError(e);
    }
  }

  // 로그인
  Future<UserCredential> signIn(String email, String password) async {
    try {
      return await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _handleAuthError(e);
    }
  }

  // 로그아웃
  Future<void> signOut() async {
    await _auth.signOut();
  }

  // 에러 처리
  String _handleAuthError(FirebaseAuthException e) {
    switch (e.code) {
      case 'email-already-in-use':
        return '이미 등록된 이메일입니다';
      case 'wrong-password':
        return '비밀번호가 틀렸습니다';
      case 'user-not-found':
        return '등록되지 않은 이메일입니다';
      case 'weak-password':
        return '비밀번호가 너무 약합니다';
      default:
        return '인증 에러: ${e.message}';
    }
  }
}

인증 상태 감시

DART
class AuthWrapper extends StatelessWidget {
  const AuthWrapper({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }
        if (snapshot.hasData) {
          return const HomeScreen();  // 로그인 상태
        }
        return const LoginScreen();   // 비로그인 상태
      },
    );
  }
}

Firestore — NoSQL 데이터베이스

CRUD 작업

DART
import 'package:cloud_firestore/cloud_firestore.dart';

class PostRepository {
  final _collection = FirebaseFirestore.instance.collection('posts');

  // 생성
  Future<void> create(Post post) async {
    await _collection.add({
      'title': post.title,
      'body': post.body,
      'authorId': post.authorId,
      'createdAt': FieldValue.serverTimestamp(),
    });
  }

  // 조회 (1회)
  Future<List<Post>> getAll() async {
    final snapshot = await _collection
        .orderBy('createdAt', descending: true)
        .limit(20)
        .get();

    return snapshot.docs.map((doc) {
      return Post.fromFirestore(doc.id, doc.data());
    }).toList();
  }

  // 실시간 조회 (스트림)
  Stream<List<Post>> watchAll() {
    return _collection
        .orderBy('createdAt', descending: true)
        .snapshots()
        .map((snapshot) {
      return snapshot.docs.map((doc) {
        return Post.fromFirestore(doc.id, doc.data());
      }).toList();
    });
  }

  // 수정
  Future<void> update(String id, Map<String, dynamic> data) async {
    await _collection.doc(id).update({
      ...data,
      'updatedAt': FieldValue.serverTimestamp(),
    });
  }

  // 삭제
  Future<void> delete(String id) async {
    await _collection.doc(id).delete();
  }

  // 조건 조회
  Future<List<Post>> getByAuthor(String authorId) async {
    final snapshot = await _collection
        .where('authorId', isEqualTo: authorId)
        .orderBy('createdAt', descending: true)
        .get();

    return snapshot.docs.map((doc) {
      return Post.fromFirestore(doc.id, doc.data());
    }).toList();
  }
}

실시간 데이터로 UI 업데이트

DART
StreamBuilder<List<Post>>(
  stream: PostRepository().watchAll(),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('에러: ${snapshot.error}');
    }
    if (!snapshot.hasData) {
      return const CircularProgressIndicator();
    }
    final posts = snapshot.data!;
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(posts[index].title));
      },
    );
  },
)

Firebase Cloud Messaging — 푸시 알림

DART
import 'package:firebase_messaging/firebase_messaging.dart';

class PushNotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;

  Future<void> initialize() async {
    // 권한 요청 (iOS)
    final settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    print('알림 권한: ${settings.authorizationStatus}');

    // FCM 토큰 가져오기
    final token = await _messaging.getToken();
    print('FCM 토큰: $token');

    // 토큰 갱신 감시
    _messaging.onTokenRefresh.listen((newToken) {
      // 서버에 새 토큰 전송
      print('토큰 갱신: $newToken');
    });

    // 포그라운드 메시지 처리
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('포그라운드 메시지: ${message.notification?.title}');
      // 로컬 알림 표시
    });

    // 알림 클릭으로 앱 열었을 때
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('알림 클릭: ${message.data}');
      // 해당 화면으로 이동
    });
  }
}

// 백그라운드 메시지 핸들러 (최상위 함수)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(
  RemoteMessage message,
) async {
  await Firebase.initializeApp();
  print('백그라운드 메시지: ${message.messageId}');
}

// main에서 등록
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseMessaging.onBackgroundMessage(
    _firebaseMessagingBackgroundHandler,
  );
  runApp(const MyApp());
}

Firestore 보안 규칙

JAVASCRIPT
// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 인증된 사용자만 읽기/쓰기
    match /posts/{postId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null
        && request.auth.uid == resource.data.authorId;
    }
  }
}

면접 포인트: Firestore 보안 규칙을 클라이언트 측에서 검증하지 않고 서버 측(Firebase Rules)에서 검증해야 합니다. 클라이언트 코드는 우회 가능하기 때문입니다.


정리

  • flutterfire configure로 Firebase 연동을 자동 설정합니다
  • Firebase Auth의 authStateChanges() 스트림으로 인증 상태를 감시합니다
  • Firestore의 snapshots()로 실시간 데이터 동기화가 가능합니다
  • FCM 토큰을 서버에 전송해서 푸시 알림을 보낼 수 있습니다
  • 보안 규칙을 반드시 설정하여 데이터 접근을 제어하세요
댓글 로딩 중...