Firebase ์ฐ๋ - Auth, Firestore, FCM
๋ก๊ทธ์ธ, ๋ฐ์ดํฐ ์ ์ฅ, ํธ์ ์๋ฆผ์ด ํ์ํ ์ฑ์ ๋ง๋ค๋ ค๋ฉด ๋ณดํต ์๋ฒ๋ฅผ ์ง์ ๋ง๋ค์ด์ผ ํ๋ค. Firebase๋ ์ด ์ธ ๊ฐ์ง๋ฅผ ์๋ฒ ์์ด ๋น๋ ค ์ฐ๊ฒ ํด์ค๋ค. ์ด ๊ธ์ Flutter์์ Firebase์ Authentication(๋ก๊ทธ์ธ), Cloud Firestore(๋ฐ์ดํฐ๋ฒ ์ด์ค), Cloud Messaging(ํธ์)์ ์ฐ๊ฒฐํ๋ ๋ฒ์ ๋ค๋ฃฌ๋ค. ๋จผ์ ์ค์ ์ ๊ฑด๋๋ฐ๊ณ ๋ก๊ทธ์ธ๋ถํฐ ์จ๋ณด๋ฉด ์ด๋ป๊ฒ ๋๋์ง ๋ณด์.
import 'package:firebase_auth/firebase_auth.dart';
Future<void> main() async {
runApp(const MyApp());
}
// ์ด๋๊ฐ์์
final user = FirebaseAuth.instance.currentUser; // ์ฌ๊ธฐ์ ์๋ฌ๋จ์ฑ์ด ์์๋์๋ง์ ์ด๋ฐ ์๋ฌ๊ฐ ๋๋ค.
[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()ํจํค์ง๋ฅผ ์ค์นํ๋๋ฐ๋ ์๋ฌ๊ฐ ๋๋ ๊ฑด, ๋ด ์ฑ์ด ์ด๋ Firebase ํ๋ก์ ํธ์ ์ฐ๊ฒฐ๋๋์ง ์์ง ์ฑ์ ์๋ ค์ฃผ์ง ์์๊ธฐ ๋๋ฌธ์ด๋ค.
์์ธ: ์ฑ๊ณผ Firebase ํ๋ก์ ํธ๋ฅผ ์ด์ด์ฃผ๋ ์ค์ ์ด ์๋ค
Firebase๋ฅผ ์ฐ๋ ค๋ฉด Firebase ์ฝ์์์ ๋ง๋ ํ๋ก์ ํธ ์ ๋ณด(์ฑ ID, API ํค ๋ฑ)๊ฐ ์ฑ์ ๋ค์ด ์์ด์ผ ํ๋ค. ์์ ์๋ Android์ฉ google-services.json, iOS์ฉ GoogleService-Info.plist๋ฅผ ์์ผ๋ก ๋ด๋ ค๋ฐ์ ๋ฃ์๋๋ฐ, ์ง๊ธ์ FlutterFire CLI๊ฐ ์ด ๊ณผ์ ์ ์๋์ผ๋ก ํด์ค๋ค.
1๋จ๊ณ: ํ๋ก์ ํธ ์ฐ๊ฒฐํ๊ธฐ
Firebase ์ฝ์์์ ํ๋ก์ ํธ๋ฅผ ํ๋ ๋ง๋ค๊ณ , ํฐ๋ฏธ๋์์ ์์๋๋ก ํ๋ค.
firebase login
dart pub global activate flutterfire_cli
flutterfire configurefirebase login: Google ๊ณ์ ์ผ๋ก Firebase์ ๋ก๊ทธ์ธํ๋คflutterfire_cli์ค์น: Firebase ์ค์ ์ ๋์ ํด์ฃผ๋ ๋๊ตฌ๋คflutterfire configure: ํ๋ก์ ํธ๋ฅผ ๊ณ ๋ฅด๋ฉด, ์ ํํ ํ๋ซํผ(Android, iOS ๋ฑ)๋ง๋ค Firebase ์ฑ์ ๋ง๋ค๊ณlib/firebase_options.dartํ์ผ์ ์์ฑํ๋ค. ํ๋ซํผ๋ณ ํค๊ฐ ์ด ํ์ผ์ ๋ชจ์ฌ ์๋ค
๊ทธ๋ค์ Firebase ์ฝ์ด์ ์ฐ๋ ค๋ ๊ธฐ๋ฅ์ ํ๋ฌ๊ทธ์ธ์ ์ค์นํ๋ค.
flutter pub add firebase_core firebase_auth cloud_firestore firebase_messaging๊ธฐ๋ฅ๋ง๋ค ํ๋ฌ๊ทธ์ธ์ด ํ๋์ฉ์ด๋ค. Authentication์ firebase_auth, Firestore๋ cloud_firestore, Cloud Messaging์ firebase_messaging์ด๋ค. ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํ ๋ค์๋ flutterfire configure๋ฅผ ๋ค์ ์คํํด์ ์ค์ ์ ์ต์ ์ผ๋ก ๋ง์ถฐ์ฃผ๋ ๊ฒ ์์ ํ๋ค.
2๋จ๊ณ: main์์ ์ด๊ธฐํ
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}๋ก์ปฌ ์ ์ฅ์ ๊ธ์์ ๋ณธ ํจํด ๊ทธ๋๋ก๋ค. main์ async๋ก ๋ฐ๊พธ๊ณ , ํ๋ฌ๊ทธ์ธ์ ์ฐ๊ธฐ ์ ์ WidgetsFlutterBinding.ensureInitialized()๋ฅผ ๋ถ๋ฅธ ๋ค, await๋ก ์ด๊ธฐํ๊ฐ ๋๋๊ธธ ๊ธฐ๋ค๋ฆฐ๋ค. DefaultFirebaseOptions.currentPlatform์ ์ง๊ธ ์คํ ์ค์ธ ํ๋ซํผ์ ๋ง๋ ํค๋ฅผ ๊ณจ๋ผ์ค๋ค. ์ด์ [core/no-app] ์๋ฌ๊ฐ ์ฌ๋ผ์ง๋ค.
Authentication: ์ด๋ฉ์ผ๋ก ๋ก๊ทธ์ธํ๊ธฐ
Firebase ์ฝ์์ Authentication ๋ฉ๋ด์์ ๋ก๊ทธ์ธ ๋ฐฉ๋ฒ(์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ)์ ์ผ์ผ ํ๋ค. ์ผ์ง ์์ผ๋ฉด operation-not-allowed ์๋ฌ๊ฐ ๋๋ค.
import 'package:firebase_auth/firebase_auth.dart';
final auth = FirebaseAuth.instance;
// ํ์๊ฐ์
await auth.createUserWithEmailAndPassword(email: email, password: password);
// ๋ก๊ทธ์ธ
await auth.signInWithEmailAndPassword(email: email, password: password);
// ๋ก๊ทธ์์
await auth.signOut();๋ก๊ทธ์ธ ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์๋ ค์ฃผ๋ Stream๋ ์๋ค.
auth.authStateChanges() // Stream<User?>, ๋ก๊ทธ์ธํ๋ฉด User, ๋ก๊ทธ์์ํ๋ฉด null๋น๋๊ธฐ UI ๊ธ์์ ๋ฐฐ์ด StreamBuilder์ ์ฐ๊ฒฐํ๋ฉด ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ผ ํ๋ฉด์ด ์์์ ๋ฐ๋๋ค.
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) return const HomePage(); // ๋ก๊ทธ์ธ๋จ
return const LoginPage(); // ๋ก๊ทธ์ธ ์ ๋จ
},
)ํ๋ฉด ์ด๋ ๊ธ์ go_router redirect์์ ๊ฐ์ ์ํ๋ฅผ ๋ณด๊ณ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ๋ณด๋ผ ์๋ ์๋ค.
์คํจํ๋ฉด FirebaseAuthException์ด ๋์ ธ์ง๊ณ e.code๋ก ์ด์ ๋ฅผ ๊ตฌ๋ถํ๋ค.
try {
await auth.signInWithEmailAndPassword(email: email, password: password);
} on FirebaseAuthException catch (e) {
final message = switch (e.code) {
'invalid-credential' || 'wrong-password' || 'user-not-found' =>
'์ด๋ฉ์ผ์ด๋ ๋น๋ฐ๋ฒํธ๊ฐ ๋ง์ง ์์์',
'email-already-in-use' => '์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ์ด์์',
'weak-password' => '๋น๋ฐ๋ฒํธ๊ฐ ๋๋ฌด ์งง์์',
_ => '๋ก๊ทธ์ธ์ ์คํจํ์ด์ (${e.code})',
};
// ์๋ฌ ์ฒ๋ฆฌ ๊ธ์ฒ๋ผ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค๋ค
}๋ก๊ทธ์ธ ์คํจ์ code๊ฐ wrong-password, user-not-found, invalid-credential ์ค ์ด๋ ๊ฒ์ผ๋ก ์ค๋์ง๋ ํ๋ก์ ํธ ์ค์ ๊ณผ ๋ฒ์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์์ด์, ์์ฒ๋ผ ๋ฌถ์ด์ ์ฒ๋ฆฌํ๋ ๊ฒ ์์ ํ๋ค.
Firestore: ๋ฐ์ดํฐ ์ ์ฅ๊ณผ ์ฝ๊ธฐ
Firestore๋ ์ปฌ๋ ์ (ํด๋) ์์ ๋ฌธ์(Map ํ๋)๊ฐ ์์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ค. ํ๊ฐ ์๋๋ผ JSON ๊ฐ์ ๋ฌธ์๋ฅผ ์ ์ฅํ๋ค.
import 'package:cloud_firestore/cloud_firestore.dart';
final db = FirebaseFirestore.instance;
// ์ถ๊ฐ (id๋ ์๋ ์์ฑ)
await db.collection('todos').add({'title': '์ฐ์ ์ฌ๊ธฐ', 'done': false});
// ํ ๋ฒ ์ฝ๊ธฐ
final snapshot = await db.collection('todos').get();
for (final doc in snapshot.docs) {
print('${doc.id}: ${doc.data()}'); // data()๋ Map<String, dynamic>
}
// ์์ , ์ญ์
await db.collection('todos').doc(id).update({'done': true});
await db.collection('todos').doc(id).delete();doc.data()๊ฐ Map<String, dynamic>์ด๋, JSON ์ง๋ ฌํ ๊ธ์์ ๋ง๋ fromJson์ผ๋ก ๋ชจ๋ธ๋ก ๋ฐ๊ฟ ์ด๋ค. ์กฐ๊ฑด๊ณผ ์ ๋ ฌ๋ ๋๋ค.
db.collection('todos')
.where('done', isEqualTo: false)
.orderBy('title')
.get();๋ก์ปฌ ์ ์ฅ์ ๊ธ์ drift watch()์ฒ๋ผ, Firestore๋ ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค ์ ๋ชฉ๋ก์ ํ๋ ค๋ณด๋ด๋ Stream์ด ์๋ค.
StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: db.collection('todos').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final docs = snapshot.data!.docs;
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, i) => ListTile(title: Text(docs[i]['title'])),
);
},
)๋ค๋ฅธ ๊ธฐ๊ธฐ์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํด๋ ์ด ํ๋ฉด์ ๋ชฉ๋ก์ด ์ค์๊ฐ์ผ๋ก ๊ฐฑ์ ๋๋ค. ์ด๊ฒ Firestore๋ฅผ ์ฐ๋ ํฐ ์ด์ ๋ค.
์ฝ์ง ํฌ์ธํธ: permission-denied
์ฝ๋๋ ๋ง๋๋ฐ ์ฝ๊ธฐ๋ ์ฐ๊ธฐ๊ฐ permission-denied๋ก ๊ฑฐ๋ถ๋๋ ๊ฒฝ์ฐ๊ฐ ๊ฐ์ฅ ํํ๋ค. Firestore๋ ์ฝ์์ ๋ณด์ ๊ท์น์ผ๋ก ๋๊ฐ ์ฝ๊ณ ์ธ ์ ์๋์ง๋ฅผ ์ ํ๋ค. ๊ฐ๋ฐ ์ด๊ธฐ์ "ํ
์คํธ ๋ชจ๋"๋ก ์์ํ๋ฉด ๋ชจ๋์๊ฒ ์ด๋ ค ์์ง๋ง, ์ด ๊ท์น์ ๊ธฐ๊ฐ์ด ์ง๋๋ฉด ๋ง๋ฃ๋์ด ๊ฐ์๊ธฐ ๋ชจ๋ ์์ฒญ์ด ๋งํ๋ค. ์ฑ์ ๋ฐฐํฌํ๊ธฐ ์ ์๋ request.auth != null์ฒ๋ผ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ํ์ฉํ๋ ๊ท์น์ผ๋ก ๋ฐ๊ฟ์ผ ํ๋ค. ๊ท์น ์์ด ์ด์ด๋ ์ฑ๋ก ๋๋ฉด ๋๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ ์ง์ธ ์ ์๋ค.
๋ Firestore๋ ๋ฌธ์๋ฅผ ์ฝ์ ํ์๋ก ๋น์ฉ์ด ๋งค๊ฒจ์ง๋ค. snapshots()๋ก ํฐ ์ปฌ๋ ์
์ ์ฒด๋ฅผ ๊ตฌ๋
ํ๋ฉด ์ฝ๊ธฐ ํ์๊ฐ ๋ถ์ด๋๋, ํ์ํ ๋งํผ๋ง where, limit์ผ๋ก ์ค์ฌ์ ์ฝ๋๋ค.
Cloud Messaging: ํธ์ ์๋ฆผ
import 'package:firebase_messaging/firebase_messaging.dart';
final messaging = FirebaseMessaging.instance;
// ์๋ฆผ ๊ถํ ์์ฒญ (iOS, ์๋๋ก์ด๋ 13 ์ด์)
await messaging.requestPermission();
// ์ด ๊ธฐ๊ธฐ๋ฅผ ๊ฐ๋ฆฌํค๋ ํ ํฐ. ์๋ฒ๊ฐ ์ด ํ ํฐ์ผ๋ก ํธ์๋ฅผ ๋ณด๋ธ๋ค
final token = await messaging.getToken();
print('FCM ํ ํฐ: $token');์ฑ์ด ์ผ์ ธ ์์ ๋ ๋ค์ด์จ ๋ฉ์์ง๋ Stream์ผ๋ก ๋ฐ๋๋ค.
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('๋ฐ์: ${message.notification?.title}');
});์ฑ์ด ๊บผ์ ธ ์์ ๋ ์๋ฆผ์ ๋๋ฌ์ ์ฑ์ด ์ด๋ฆฌ๋ฉด FirebaseMessaging.onMessageOpenedApp์ผ๋ก ๋ฐ๋๋ค. ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ค๋ฉด ํธ๋ค๋ฌ๋ฅผ ๋ฐ๋ก ๋ฑ๋กํ๋ค.
@pragma('vm:entry-point')
Future<void> _backgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
print('๋ฐฑ๊ทธ๋ผ์ด๋ ๋ฉ์์ง: ${message.messageId}');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
runApp(const MyApp());
}ํธ๋ค๋ฌ๋ ํด๋์ค ์์ด ์๋๋ผ ์ต์์ ํจ์์ฌ์ผ ํ๋ค. ์ฑ๊ณผ ๋ณ๊ฐ์ ํ๊ฒฝ์์ ์คํ๋๊ธฐ ๋๋ฌธ์, ๊ทธ ์์์ Firebase๋ฅผ ๋ค์ ์ด๊ธฐํํด์ผ ํ๋ค. @pragma('vm:entry-point')๋ ๋ฆด๋ฆฌ์ค ๋น๋์์ ์ด ํจ์๊ฐ "์ ์ฐ๋ ์ฝ๋"๋ก ์ง์์ง์ง ์๊ฒ ํ๋ ํ์๋ค.
์ฝ์ง ํฌ์ธํธ: ํธ์๊ฐ ์ ์จ๋ค
- iOS ์๋ฎฌ๋ ์ดํฐ์์๋ ํธ์๋ฅผ ๋ฐ์ ์ ์๋ค. ์ค์ ๊ธฐ๊ธฐ๊ฐ ํ์ํ๊ณ , Apple ๊ฐ๋ฐ์ ๊ณ์ ์์ ๋ง๋ APNs ์ธ์ฆ ํค๋ฅผ Firebase ์ฝ์์ ๋ฑ๋กํด์ผ ํ๋ค
- ์๋๋ก์ด๋์์ ์ฑ์ด ์ผ์ ธ ์์ ๋ ์๋ฆผ์ด ํ๋ฉด์ ์๋์ผ๋ก ๋จ์ง ์๋๋ค.
onMessage๋ก ๋ฐ์์ ์ง์ ๋ณด์ฌ์ค์ผ ํ๋ค(๋ณดํต ๋ก์ปฌ ์๋ฆผ ํจํค์ง๋ฅผ ๊ฐ์ด ์ด๋ค). ์ฑ์ด ๊บผ์ ธ ์๊ฑฐ๋ ๋ฐฑ๊ทธ๋ผ์ด๋์ผ ๋๋ ์์คํ ์ด ์๋ฆผ์ ๋์์ค๋ค - ํ ํฐ์ ์ฑ์ ์ง์ ๋ค ๋ค์ ์ค์นํ๋ ๋ฑ์ผ๋ก ๋ฐ๋ ์ ์์ด์, ์๋ฒ์ ์ ์ฅํ ํ ํฐ์ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํด์ผ ํ๋ค
๊ณ์ธต ๊ตฌ์กฐ์์์ ์์น
Firebase ํธ์ถ๋ ์์์ ๋ฐฐ์ด Service ๊ณ์ธต์ ๋๋ค. ํ๋ฉด์ด FirebaseFirestore.instance๋ฅผ ์ง์ ๋ถ๋ฅด๋ ๊ฑด ํ๋ฉด์ด ์๋ฒ๋ฅผ ์ง์ ๋ถ๋ฅด๋ ์ฝ๋์ ๊ฐ๋ค. TodoService๊ฐ Firestore๋ฅผ ๊ฐ์ธ๊ณ , TodoRepository๊ฐ ๋ชจ๋ธ๋ก ๋ฐ๊พธ๊ณ , ViewModel์ด ํ๋ฉด์ฉ ์ํ๋ก ๋ง๋ ๋ค. ๊ทธ๋ฌ๋ฉด ์ํ์์๋ Firestore ๋์ ๊ฐ์ง Repository๋ฅผ ๋ผ์ธ ์ ์๋ค.
Android, iOS์ ๋น๊ตํ๋ฉด
// Kotlin - Firebase Android SDK
val auth = Firebase.auth
auth.signInWithEmailAndPassword(email, password).await()
Firebase.firestore.collection("todos").snapshots() // Flow// Swift - Firebase iOS SDK
try await Auth.auth().signIn(withEmail: email, password: password)
let snapshot = try await Firestore.firestore().collection("todos").getDocuments()- ์ด๊ธฐ ์ค์ : Flutter
flutterfire configure์Firebase.initializeApp/ Androidgoogle-services.json๊ณผ Gradle ํ๋ฌ๊ทธ์ธ / iOSGoogleService-Info.plist - ๋ก๊ทธ์ธ ์ํ: Flutter
authStateChanges()Stream/ AndroidAuthStateListener๋Flow/ iOSaddStateDidChangeListener - ์ค์๊ฐ ๋ฐ์ดํฐ: Flutter
snapshots()Stream/ Androidsnapshots()Flow/ iOSaddSnapshotListener - ํธ์: ์ธ ๊ณณ ๋ชจ๋ ๊ฐ์ FCM์ด๊ณ , iOS๋ APNs ํค๊ฐ ํ์
์ธ ํ๋ซํผ์ด ๊ฐ์ Firebase ํ๋ก์ ํธ๋ฅผ ์ฐ๋ฏ๋ก, ์ด๋ฏธ ๋ง๋ Kotlin์ด๋ Swift ์ฑ๊ณผ ๋ฐ์ดํฐ์ ์ฌ์ฉ์๋ฅผ ๊ณต์ ํ ์๋ ์๋ค. Flutter๊ฐ ๋ค๋ฅธ ์ ์ Stream๊ณผ await ๊ฐ์ Dart์ ๋๊ตฌ๋ก ๊ฐ์ ์ผ์ ํํํ๋ค๋ ๊ฒ๋ฟ์ด๋ค.
์ ๋ฆฌ
- ์ค์ ์์ด Firebase๋ฅผ ๋ถ๋ฅด๋ฉด
[core/no-app]์๋ฌ๊ฐ ๋๋ฏ๋ก,flutterfire configure๋กfirebase_options.dart๋ฅผ ๋ง๋ค๊ณmain์์Firebase.initializeApp(options: ...)์awaitํ๋ค - Firebase๋ ๊ธฐ๋ฅ๋ง๋ค ํ๋ฌ๊ทธ์ธ์ด ํ๋์ฉ์ด๊ณ (
firebase_auth,cloud_firestore,firebase_messaging),main์์WidgetsFlutterBinding.ensureInitialized()๊ฐ ๋จผ์ ํ์ํ๋ค - ๋ก๊ทธ์ธ์ ์ฝ์์์ ๋ก๊ทธ์ธ ๋ฐฉ๋ฒ์ ์ผ๊ณ
signInWithEmailAndPassword๋ฅผ ์ฐ๋ฉฐ,authStateChanges()๋ฅผStreamBuilder์ ์ฐ๊ฒฐํ๋ฉด ๋ก๊ทธ์ธ ์ฌ๋ถ๋ก ํ๋ฉด์ด ๋ฐ๋๋ค. ์คํจ๋FirebaseAuthException์code๋ก ๊ตฌ๋ถํ๋ค - Firestore๋ ์ปฌ๋ ์
๊ณผ ๋ฌธ์์
Map์ ์ ์ฅํ๊ณsnapshots()๋ก ์ค์๊ฐStream์ ๋ฐ์ผ๋ฉฐ, ๋ฐ์ดํฐ๋fromJson์ผ๋ก ๋ชจ๋ธ๋ก ๋ฐ๊พผ๋ค permission-denied๋ ๋๊ฐ ๋ณด์ ๊ท์น ๋ฌธ์ ์ด๊ณ , ํ ์คํธ ๋ชจ๋ ๊ท์น์ ๋ง๋ฃ๋๋ฏ๋ก ๋ฐฐํฌ ์ ์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ํ์ฉํ๋ ๊ท์น์ผ๋ก ๋ฐ๊พผ๋ค- ํธ์๋
requestPermission,getToken,onMessage, ์ต์์ ํจ์onBackgroundMessage๋ก ๋ค๋ฃจ๊ณ , iOS๋ ์ค์ ๊ธฐ๊ธฐ์ APNs ํค๊ฐ ํ์ํ๋ค - Firebase ํธ์ถ์ Service ๊ณ์ธต์ ๋์ด์ ํ๋ฉด๊ณผ ๋ถ๋ฆฌํ๋ค
๋ค์ ๊ธ์์๋ ์ฑ์ด ๋๋ ค์ง ๋ ์ด๋๋ฅผ ๊ณ ์ณ์ผ ํ๋์ง, ์ฑ๋ฅ ์ต์ ํ(const, RepaintBoundary, DevTools)๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
