ios๋ ์ค๋ฌผ ๊ธฐ๊ธฐ๋ก, android๋ ์๋ฎฌ๋ ์ดํฐ๋ก ๋ นํํ์์ต๋๋ค.
-
background, quit, foreground์ํ์ ๋ฐ๋ฅธ push ์๋ฆผ
-
์ด๋ฏธ์ง์ ์์ฑ๋ฉ์์ง ๋ณด๋ด๊ณ ํ์ธ. ํ์ธ์ 1 ์ฌ๋ผ์ง
๋ชฉ์ฐจ
- ๊ตฌํ ๊ธฐ๋ฅ
- Firebase ์ค์
- react-navigation ํจํค์ง ์ค์น
- ํ์๊ฐ์ ํ์ด์ง ๊ตฌํ
- ์ฑํ ๋ฐฉ ํ์ด์ง ๊ตฌํ
- ๋ฉ์์ง ๋ณด๋ด๊ธฐ ๋ฐ ๋ถ๋ฌ์ค๊ธฐ ๊ธฐ๋ฅ
- ์ฑํ ๋ฉ์์ง ๊ตฌํ
- ์๊ฐ ์ถ๋ ฅ
- ์ค์๊ฐ ์๋ก์ด ๋ฉ์์ง ๋ฐ๊ธฐ ๊ตฌํ
- ํ๋กํ ์ด๋ฏธ์ง ๋ฑ๋ก
- ๋ฉ์์ง ์ฝ์ ํ์ ๊ตฌํ
- ์์ฑ๋ฉ์์ง ์ ์ก
- react-native-firebase-messaging
- Push Notification
-
ํ ์คํธ,์ด๋ฏธ์ง ๋ฐ ์์ฑ ๋ฉ์์ง๋ฅผ ์ ์กํ ์ ์๋ 1:1 ์ฑํ ์ฑ
-
๊ธฐ๋ฅ
- ํ์๊ฐ์ ,๋ก๊ทธ์ธ
- ํ๋กํ ์ด๋ฏธ์ง ๋ฑ๋ก
- ์ฌ์ฉ์ ๋ฆฌ์คํธ
- ์ฑํ ๋ฐฉ
- ํ ์คํธ, ์ด๋ฏธ์ง, ์ค๋์ค ๋ฉ์์ง ์ ์ก
- ๋ฉ์์ง ์ฝ์ ํ์
- ํธ์ ์๋ฆผ
-
๋ชฉํ
- Typescript ๊ธฐ๋ฐ React Naitve ํ๋ก์ ํธ๋ฅผ CLI๋ฅผ ์ด์ฉํด์ ์ด๊ธฐํ
- ๋ฉํฐ๋ฏธ๋์ด ๋ค๋ฃจ๊ธฐ (์ด๋ฏธ์ง, ์ค๋์ค ๋ น์ ๋ฐ ์ฌ์)
- Firebase๋ฅผ ์ด์ฉํ์ฌ serverless ํ๊ฒฝ์์ ์ฑ ๊ฐ๋ฐ ( Authentication, Firestore, Storage, Cloud Functions)
- Firebase Cloud Messaging์ ์ด์ฉํ ํธ์ ๋ ธํฐํผ์ผ์ด์ ์ ์ก
- react native firebase ์ค์
- rnfirebase
npm install --save @react-native-firebase/app @react-native-firebase/auth @react-native-firebase/firestore
npm install --save @react-navigation/native-stack @react-navigation/native react-native-screens react-native-safe-area-context
-
ํ์ ํ์ธํ๊ธฐ . validator ์ฌ์ฉ
- npm install validator
- npm install @types/validator --save
if (!validator.isEmail(email)) { return " ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ์ด ์๋๋๋ค."; }
-
SignupScreen
- name,email,password ํ์ธ ๋ฐ ๊ฒฝ๊ณ ๋ฌธ
<TextInput value={password} style={styles.input} secureTextEntry //๋น๋ฐ๋ฒํธ ์ ๋ ฅ ์ ๊ฐ๋ ค์ฃผ๋์ญํ onChangeText={onChangePasswordText} />
-
firestore ์ค์
- authentication, firestore database ์ค์
-
npm i lodash
-
npm i @types/lodash --save
-
await firestore().collection(Collections.CHATS)
.add
๋ ๋ฌธ์ ์๊ด์์ด ์ถ๊ฐ.doc
๋ ๋ฌธ์๋ช ๋ฐ๋ก ์ค์ ๊ฐ๋ฅ
-
useChat hook ์์ฑ
import { useCallback, useEffect, useState } from 'react'; import firestore from '@react-native-firebase/firestore'; import _ from 'lodash'; import { Chat, Collections, User } from '../types'; // userIds๋ฅผ ๋ฐ์์ ๊ท์น์ ๋ฐ๋ผ lodash๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ ฌํด์ฃผ๋ ํจ์ const getChatKey = (userIds: string[]) => { // userId๊ฐ์ ์ค๋ฆ์ฐจ์์ผ๋ก ์ ๋ ฌํ๋ ๊ฒ return _.orderBy(userIds, userId => userId, 'asc'); }; // ์ฌ์ฉ์๊ฐ ํฌํจ๋ ์ฑํ ๋ฐฉ์ด ์๋ค๋ฉด ๋ถ๋ฌ์ค๊ณ , ์๋ค๋ฉด ์๋ก์์ฑ const useChat = (userIds: string[]) => { const [chat, setChat] = useState<Chat | null>(null); const [loadingChat, setLoadingChat] = useState(false); const loadChat = useCallback(async () => { try { setLoadingChat(true); // userIds๋ ์ฐ๋ฆฌ๊ฐ ์ค userIds๊ฐ ๊ฐ์ ์ฑํ ๋ฐฉ์ด ์์ฑ๋์ด์๋์ง ์ฒดํฌ const chatSnapshot = await firestore() .collection(Collections.CHATS) .where('userIds', '==', getChatKey(userIds)) .get(); if (chatSnapshot.docs.length > 0) { const doc = chatSnapshot.docs[0]; setChat({ id: doc.id, userIds: doc.data().userIds as string[], users: doc.data().users as User[], }); return; } // userId์ userIds๊ฐ ํฌํจ๋ ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๊ฒ๋จ. const usersSnapshot = await firestore() .collection(Collections.USERS) .where('userId', 'in', userIds) .get(); const users = usersSnapshot.docs.map(doc => doc.data() as User); const data = { userIds: getChatKey(userIds), users, }; const doc = await firestore().collection(Collections.CHATS).add(data); setChat({ id: doc.id, ...data, }); } finally { setLoadingChat(false); } }, [userIds]); useEffect(() => { loadChat(); }, [loadChat]); return { chat, loadingChat, }; }; export default useChat;
-
firestore์ subcollection์ ์ด์ฉํด์ ๋ฉ์์ง ์ ์ก ๊ธฐ๋ฅ ๊ตฌํ
-
subcollection
- ๋ฉ์์ง chats ์ปฌ๋ ์ ์ ์ ์ ๋ชฉ๋ก๊ณผ ๋ถ๋ฆฌํ์ฌ์ผํจ
- chat๋คํ๋จผํธ collection์์ subcollection์ ๋ง๋ค์ด์ ์ฌ์ฉ ๊ฐ๋ฅ
// ์๋ธ์ปฌ๋ ์ ์ด์ด์ฃผ์ด ์ ์ฅํ๊ธฐ const doc = await firestore() .collection(Collections.CHATS) .doc(chat.id) .collection(Collections.MESSAGES) .add(data); // ์ด์ ๋ฉ์์ง + ์๋ก์ด ๋ฉ์์ง ์ ๋ฐ์ดํธ setMessages((prevMessages) => [ { id: doc.id, ...data, }, ].concat(prevMessages) );
-
๋ฉ์์ง ๋ถ๋ฌ์ค๊ธฐ
const loadMessages = useCallback(async (chatId: string) => { try { setLoadingMessages(true); const messagesSnapshot = await firestore() .collection(Collections.CHATS) .doc(chatId) .collection(Collections.MESSAGES) .orderBy("createdAt", "desc") .get(); const ms = messagesSnapshot.docs.map<Message>((doc) => { const data = doc.data(); return { id: doc.id, user: data.user, text: data.text, createdAt: data.createdAt.toDate(), //db์ date๋ฅผ dateํ์ ์ผ๋ก ๋ฐ๊ฟ์ฃผ๊ธฐ ์ํด toDate()์ฌ์ฉ }; }); setMessages(ms); } finally { setLoadingMessages(false); } }, []); useEffect(() => { if (chat?.id != null) { loadMessages(chat.id); } }, [chat?.id, loadMessages]);
- inverted FlatList
- inverted ์ต์
์ ์ฃผ๋ฉด ์คํฌ๋กค์ ์๋ฐฉํฅ์ผ๋ก ํ ์ ์์
<FlatList inverted style={styles.messageList} data={messages} renderItem={({ item: message }) => { return ( <View> <Text>{message.user.name}</Text> <Text>{message.text}</Text> <Text>{message.createdAt.toISOString()}</Text> </View> ); }} />
- inverted ์ต์
์ ์ฃผ๋ฉด ์คํฌ๋กค์ ์๋ฐฉํฅ์ผ๋ก ํ ์ ์์
npm i moment
- ํฌ๋งท ํ์ ์์ฑํด์ฃผ๋ฉด๋จ
<Text style={styles.timeText}>{moment(createdAt).format("HH:mm")}</Text>
-
firestore ์ฌ์ดํธ ์ฐธ๊ณ
-
onSnapshot()
๋ฉ์๋ ์ฌ์ฉํ๊ธฐfirestore() .collection(Collections.CHATS) .doc(chat.id) .collection(Collections.MESSAGES) .orderBy("createdAt", "desc") .onSnapshot((snapshot) => { // ๋ฉ์์ง๊ฐ ์ถ๊ฐ๋ ๋๋ง ๋ด์ฉ๋ณ๊ฒฝ const newMessages = snapshot .docChanges() .filter(({ type }) => type === "added") .map((docChange) => { const { doc } = docChange; const docData = doc.data(); const newMessage: Message = { id: doc.id, text: docData.text, user: docData.user, createdAt: docData.createdAt.toDate(), }; return newMessage; }); addNewMessage(newMessages); });
-
lodash โ uniqBy
-
์๋ก์ด ๋ฉ์์ง๊ฐ ์ค๋ณต๋๋๋ผ๋ ๊ฐ์๋ฉ์์ง๊ฐ ์์ผ๋ฉด lodash uniqBy๋ก ์ ๊ฑฐ
const addNewMessage = useCallback((newMessages: Message[]) => { setMessages((prevMessages) => { return _.uniqBy(newMessages.concat(prevMessages), (m) => m.id); }); }, []);
-
- ์ด๋ฏธ์ง ํฌ๋กญ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด์ฉ
- react-native-image-crop-picker
- ์ ์ฌ์ดํธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ios์ android ์ ํ ํ๊ธฐ
const onPressProfile = useCallback(async () => { const image = await ImageCropPicker.openPicker({ cropping: true, cropperCircleOverlay: true, //์๋ชจ์์ผ๋ก ์ฌ์ง์ด ์๋ฆผ }); }, []);
- firebase storage์ ์ด๋ฏธ์ง ์
๋ก๋
- firebase storage ์์ ๋ฐ ํจํค์ง ์ค์น
npm i @react-native-firebase/storage
- firebase ๊ด๋ จ ํจํค์ง ๋ฒ์ ์ ๋๊ฐ์ด ๋ง์ถฐ์ฃผ์ด์ผํจ
"@react-native-firebase/app": "^17.4.2", "@react-native-firebase/auth": "^17.4.2", "@react-native-firebase/firestore": "^17.4.2", "@react-native-firebase/storage": "^17.4.2",
- ํ๋กํ ๋ณด์ฌ์ฃผ๊ธฐ ๋ฐ ํ๋๊ธฐ๋ฅ
- react-native-image-viewing
- firestore server timestamp์ด์ฉ
- ๋ง์ง๋ง์ผ๋ก ์ฑํ ๋ฐฉ์ ๋ค์ด์จ ์๊ฐ chat db์ ๊ธฐ๋ก
- ์๊ฐ์ ๋น๊ต
- ๋ฉ์์ง ์ ์ก์๊ฐ > ์ฑํ ๋ฐฉ์ ๋ค์ด์จ ์๊ฐ : ์์ฝ์
- ๋ฉ์์ง ์ ์ก์๊ฐ โค ์ฑํ ๋ฐฉ์ ๋ค์ด์จ ์๊ฐ : ์ฝ์
- firestore ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ด์ฉํด์ ๋ฉ์์ง ์ฝ์ ์ฌ์ฉ์ ์ ๋ณด ์ค์๊ฐ์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ
- ๋ฉ์์ง๋ฅผ ์ฝ์ง ์์ ์ฌ๋ ์ ํ์
react-native-audio-recorder-player
import React, { useCallback, useRef, useState } from "react";
import { PermissionsAndroid, Platform, StyleSheet } from "react-native";
import AudioRecorderPlayer, {
AVEncodingOption,
AudioEncoderAndroidType,
} from "react-native-audio-recorder-player";
const MicButton = () => {
const [recording, setRecording] = useState(false);
const audioRecorderPlayerRef = useRef(new AudioRecorderPlayer());
const startRecord = useCallback(async () => {
// android permission
if (Platform.OS === "android") {
const grants = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
]);
const granted =
grants[PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE] ===
PermissionsAndroid.RESULTS.GRANTED &&
grants[PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE] ===
PermissionsAndroid.RESULTS.GRANTED &&
grants[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] ===
PermissionsAndroid.RESULTS.GRANTED;
if (!granted) {
return;
}
}
//๋
น์์คํ
await audioRecorderPlayerRef.current.startRecorder(undefined, {
// android์ ios ์ค๋์คํ์
ํต์ผ
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AVFormatIDKeyIOS: AVEncodingOption.aac,
});
audioRecorderPlayerRef.current.addRecordBackListener(() => {});
setRecording(true);
}, []);
const stopRecord = useCallback(async () => {
const uri = await audioRecorderPlayerRef.current.stopRecorder();
audioRecorderPlayerRef.current.removeRecordBackListener();
setRecording(false);
}, []);
};
export default MicButton;
- ๋ง์ดํฌ ๊ถํ ์์ฒญ
- ์์ฑ ๋
น์ ๊ธฐ๋ฅ ๊ตฌํ
- android๋ npm run android๋ก ์คํ์ ๋ น์์ด ์๋๋ฏ๋ก android studio์์ ์ด์ด์ผํจ.
- open์ผ๋ก ํ๋ก์ ํธ์ androidํด๋๋ฅผ open โ (tools)device manager โ ์ฌ์
- emulator์ ์ ์ธ๊ฐ(extended controls) ํด๋ฆญ โ microphone โ virtual microphone uses host audio input ํ์ฑํ
- firebase storage์ ์์ฑ ์ ๋ก๋ํ๊ธฐ
- ์์ฑ ๋ฉ์์ง ์ ์กํ๊ธฐ
- ์์ฑ ๋ฉ์์ง ์ฌ์ ๋ฐ ๋ฉ์ถค ๊ธฐ๋ฅ ๊ตฌํ
- ๋จ์ ์ฌ์์๊ฐ ํ์
-
ios
- ์ค์ ๋๋ฐ์ด์ค, apple ๊ฐ๋ฐ์ ๋ฑ๋ก์ด ํ์ํจ
-
android
- android ์๋ฎฌ๋ ์ดํฐ๋ ์ค์ ๋๋ฐ์ด
-
FCM(Firebase Cloud Messaging)
- ๋น์ฉ์์ด Server-to-Device, Device-to-Device๋ก ์๋ฆผ์ ์ ์กํ ์ ์๋ ํ๋ซํผ
-
์๋ฆผ ์์ ์ ์ฑ ์ํ
- Foreground : ์ฑ์ด ์คํ๋๊ณ ํ์ฌ ๋ณด์ฌ์ง๋ ์ํ
- Background : ์ฑ์ ์คํ๋์์ผ๋ ์ต์ํ ๋์ด์๋ ์ํ
- Quit : ์ฑ์ด ์์ ํ ์ข ๋ฃ๋ ์ํ
-
๋ฉ์์ง ํ์ ์ ๋ฐ๋ฅธ ์๋ฆผํ์
- Foreground : ์๋ฆผํ์ ๋ถ๊ฐ๋ฅ
- Background: Notification, Notification + Data ์๋ฆผํ์๊ฐ๋ฅ
- Quit : Notification, Notification + Data ์๋ฆผํ์๊ฐ๋ฅ
-
ํธ๋ํฐ์์ ๋ณ๋์ฌํญ์ด firebase๋ก ๋ค์ด๊ฐ โ cloud functions์์ ๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ์ ์ก
-
https://rnfirebase.io/messaging/usage
yarn add @react-native-firebase/messaging
- ๋ค๋ฅธ firestore ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค๊ณผ ๋ฒ์ ๋๊ฐ์ด ๋ง์ถ์ด์ฃผ๊ธฐ โ yarn
- ios > rm -rf Pods ๋ก ์ง์์ค ํ, pod install
-
์๋ฆผ๊ถํ ์์ฒญ (ios, android 13+ : 13๋ฒ์ ์ด์ ์ ์ด๋ฏธ๊ถํ์ด ๋์ด์์)
- android 13๋ฒ์ ์ด์์ ์ํ ํจํค์ง
- react-native-permissions
yarn add react-native-permissions
- https://github.com/zoontek/react-native-permissions/releases/tag/3.5.0
- android 13๋ฒ์ ์ด์์ ์ํ ํจํค์ง
-
Push Notification ์ ์ก์ ์ํด fcmํ ํฐ ๋ฑ๋กํ๊ธฐ
-
๋ฐฑ์๋
-
firebase cloud functions ํ๋ก์ ํธ ์ด๊ธฐํ https://firebase.google.com/docs/functions?hl=ko
-
https://firebase.google.com/docs/functions/firestore-events?hl=ko
cloud functions์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ firebase๋ฌด๋ฃ(spark)์๊ธ์ ๋ก๋ ์ฌ์ฉ ๋ถ๊ฐ๋ฅ โ ์ฌ์ฉํ ์๋ก ์๊ธ์ด ๋ถ๊ณผ๋๋ Blaze์๊ธ์ ๋ฅผ ์ฌ์ฉํด์ผ ํจ
- firestore trigger์ด์ฉ์ ๋ฌธ์๊ฐ ์ ๋ฐ์ดํธ,์์ฑ,์ญ์ ๋ ๋ ์๋ฒ์์ ์คํ๋ ์ ์๋ ์ฝ๋ ๊ตฌํ๊ฐ๋ฅ
-
์ฌ์ฉ
npm install -g firebase-tools
https://firebase.google.com/docs/functions/get-started?hl=ko ๋งํฌ ์ฐธ๊ณ ํ์ฌ ํ๋ก์ ํธ setting- login โ functions๊ธฐ๋ฅ์ ํ โ ๊ธฐ์กด ํ๋ก์ ํธ ์ ํ โ ์ฌ์ฉ์ธ์ด์ ํ
- ํจํค์ง ์ค์น๋ ์คํ์ ChatAppServer > functions ํด๋ ๋ด์์ ์์
-
npm i eslint-config-prettier -s
- eslint์ prettier๊ฐ ์ถฉ๋ํ ๋ (4์นธ๋์ธ์ง 2์นธ๋์ธ์ง ๋ฑ) ํ๊ฐ์ง๋ฅผ ์ ํํ๋ ํจํค์ง
- .eslint.rcํ์ผ์์ ๋ค์๋ด์ฉ์ถ๊ฐ
extends: [ "prettier", ],
-
npm i firebase-admin
- function์์ ๋ฌธ์๋ฅผ ์ฝ๊ธฐ์ํด ์ฌ์ฉ
-
์๋ฒ์์ firestore ์ค์๊ฐ ์ ๋ฐ์ดํธ ์์ ํ๊ธฐ
-
์๋ก์ด ๋ฉ์์ง ์์ฑ ์ Push Notification ์ ์ก
- npm run deploy
- ์๋ฒ์ ๋ฐฐํฌ๊ฐ ๋จ. ์ฝ 3๋ถ์์
- firebase โ ํ๋ก์ ํธ โ functions โ ํจ์์ ์ 3๊ฐ โ ๋ก๊ทธ๋ณด๊ธฐ ์์ ์๋ณด๋ด์ง๋์ง ํ์ธ๊ฐ๋ฅ
- npm run deploy
-
-
์ฑ
- ๋ฉ์์ง ์์ ์ Notification ๋ณด์ฌ์ฃผ๊ธฐ
- Notification ํฐ์นํ๋ฉด ์ฑํ
์คํฌ๋ฆฐ์ผ๋ก ์ด๋
- background์ ์๋๊ฒฝ์ฐ์ quit์ํ์ ์๋๊ฒฝ์ฐ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํด์ฃผ์ด์ผํจ.
- foreground์์ ์๋ฆผ๋์ฐ๊ธฐ
- messaging์ onMessage์ฌ์ฉ
- react-native-toast-message ํจํค์ง ์ฌ์ฉํ์ฌ ์ฑ ๋ด์์ ์๋ฆผ๋์ฐ๊ธฐ
- react-native-toast-message
- app.tsx์ ์ปดํฌ๋ํธ ์ถ๊ฐ
- ios APNs(Apple Push Notifications service)์ค์
- ios Messaging setup
- https://rnfirebase.io/messaging/usage/ios-setup
- capabilities ์ถ๊ฐ
- APNs ํค ๋ฑ๋ก
- developer.apple.com
- ํ๋ก๊ทธ๋จ ๋ฆฌ์์ค โ ํค ๋ฑ๋ก โ ๋ค์ด๋ก๋(์ฌ๋ค์ด๋ก๋๋ถ๊ฐ๋ฅ)
- firebase ํ๋ก์ ํธ ์ค์ โ ํด๋ผ์ฐ๋ ๋ฉ์์ง โ APN์ธ์ฆ ํค ๋ฑ๋ก (ํคid, ํid-์ด๋ฆ์ ์ฝ๋), ํค ํ์ผ ๋ฑ๋ก