- React-Native
- Styled-Components: Styling
- Google Material Design: Icon
- React-Navigation(Stack, Tab)
- Context API: μν κ΄λ¦¬
- Firebase: μλΉμ€μ νμν μλ²μ λ°μ΄ν°λ² μ΄μ€λ₯Ό μ§μ ꡬμΆνμ§ μκ³ κ°λ°μ΄ κ°λ₯ν κ°λ° νλ«νΌ
- expo-image-picker: κΈ°κΈ°μ μ¬μ§μ΄λ μμμ κ°μ Έμ¬ μ μλλ‘ μμ€ν UIμ μ κ·Όν μ μλ κΈ°λ₯μ μ 곡
- moment: μκ°μ λ€μν ννλ‘ λ³κ²½νλ λ± μκ°κ³Ό κ΄λ ¨λ λ§μ κΈ°λ₯μ μ 곡
- react-native-keyboard-aware-scroll-view: ν€λ³΄λκ° νλ©΄μ κ°λ¦¬λ©°μ μκΈ°λ λΆνΈν μ μ ν΄κ²°ν μ μλ κΈ°λ₯ μ 곡
- react-native-gifted-chat: λ©μμ§λ₯Ό μ£Όκ³ λ°λ μ±ν νλ©΄μ μ½κ² ꡬνν μ μλλ‘ λλ λΌμ΄λΈλ¬λ¦¬
- components: μ»΄ν¬λνΈ νμΌ κ΄λ¦¬
- contexts: Context API νμΌ κ΄λ¦¬
- navigations: λ΄λΉκ²μ΄μ νμΌ κ΄λ¦¬
- screens: νλ©΄ νμΌ κ΄λ¦¬
- utils: νλ‘μ νΈμμ μ΄μ©ν κΈ°ν κΈ°λ₯ κ΄λ¦¬
π https://console.firebase.google.com/
- Firebaseλ μΈμ¦(Authentication), λ°μ΄ν°λ² μ΄μ€(Database) λ±μ λ€μν κΈ°λ₯μ μ 곡νλ κ°λ° νλ«νΌμ΄λ€.
- Firebaseκ° μ 곡νλ κΈ°λ₯μ μ΄μ©νλ©΄ λλΆλΆμ μλΉμ€μμ νμν μλ²μ λ°μ΄ν°λ² μ΄μ€λ₯Ό μ§μ ꡬμΆνμ§ μμλ κ°λ°μ΄ κ°λ₯νλ€.
//Firebase Setting
1. νλ‘μ νΈ μ€μ > μΌλ° > λ΄ μ±μμ 'μΉ'μ μ ννκ³ μ±μ μΆκ°
2. νλ‘μ νΈ μ€μ > μΌλ° > λ΄ μ±μμ 'Firebase SDK snippet'μμ Firebase μ€μ κ°μ νμΈνλ€.
3. νλ‘μ νΈ λ£¨νΈ λλ ν°λ¦¬μ firebase.json νμΌμ μμ± ν 2λ²μμ νμΈν μ½λλ₯Ό λ£λλ€.
- firebase.jsonμ μ€μν νμΌμ΄κΈ° λλ¬Έμ .gitignoreμ μΆκ°νλ€.
//firebase.json
{
"apiKey": "...",
"authDomain": "...",
"projectId": "...",
"storageBucket": "...",
"messagingSenderId": "...",
"appId": "...",
"measurementId": "..."
}
4. μΈμ¦, λ°μ΄ν°λ² μ΄μ€, μ€ν λ¦¬μ§ μ€μ νλ€.
5. expo install firebase λ₯Ό ν΅ν΄ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ€μΉνλ€.
6. firebase.js νμΌμ μμ±νλ€.
//src/utils/firebase.js
import * as firebase from "firebase";
import config from "../../firebase.json";
const app = firebase.initializeApp(config);
- νλ‘μ νΈμμ μ¬μ©ν μ΄λ―Έμ§μ ν°νΈλ₯Ό 미리 λΆλ¬μμ μ¬μ©ν μ μλλ‘ cacheImages, cacheFonts ν¨μλ₯Ό μμ±νκ³ μ΄λ₯Ό _loadAssets ν¨μλ₯Ό ꡬμ±νλ€.
- μ΄λ―Έμ§λ ν°νΈλ₯Ό 미리 λΆλ¬μ€λ©΄ μ ν리μΌμ΄μ μ μ¬μ©νλ νκ²½μ λ°λΌ μ΄λ―Έμ§λ ν°νΈκ° λλ¦¬κ² μ μ©λλ λ¬Έμ λ₯Ό κ°μ ν μ μλ€.
- μ ν리μΌμ μ 미리 λΆλ¬μμΌ νλ νλͺ©λ€μ λͺ¨λ λΆλ¬μ€κ³ νλ©΄μ΄ λ λλ§ λλλ‘ AppLoading μ»΄ν¬λνΈλ₯Ό μ¬μ©νλ€.
const cacheImages = (images) => {
return images.map((image) => {
if (typeof image === "string") {
return Image.prefetch(image);
} else {
return Asset.fromModule(image).downloadAsync();
}
});
};
const cacheFonts = (fonts) => {
return fonts.map((font) => Font.loadAsync(font));
};
const App = () => {
(...)
const _loadAssets = async () => {
const imageAssets = cacheImages([
require("../assets/splash.png"),
]);
const fontAssets = cacheFonts([]);
await Promise.all([...imageAssets, ...fontAssets]);
};
return isReady ? (
(...)
) : (
<AppLoading
startAsync={_loadAssets}
onFinish={() => setIsReady(true)}
onError={console.error}
/>
);
};
- μ΄λ² μ ν리μΌμ΄μ μ λ‘κ³ λ₯Ό Firebase μ€ν 리μ§μ μ λ‘λνκ³ λ‘κ·ΈμΈ νλ©΄μμ μ¬μ©νλλ‘ λ§λ€μμ΅λλ€.
- μ€ν 리μ§μ νμΌμ μ λ‘λνκ³ νμΌ μ 보μμ μ΄λ¦μ ν΄λ¦νλ©΄ ν΄λΉ νμΌμ urlμ μ»μ μ μμ΅λλ€.
//1. src/utils/images.js μμ±
const prefix =
"https://firebasestorage.googleapis.com/v0/b/react-native-chat-65246.appspot.com/o";
export const images = {
logo: `${prefix}/logo.png?alt=media`,
};
//2. src/App.js (_loadAssets λ©μλ μμ )
const _loadAssets = async () => {
const imageAssets = cacheImages([
require("../assets/splash.png"),
...Object.values(images),
]);
const fontAssets = cacheFonts([]);
await Promise.all([...imageAssets, ...fontAssets]);
};
//3. Firebase μ€ν λ¦¬μ§ Rules μμ
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /logo.png {
allow read;
}
}
}
- useRefλ₯Ό μ΄μ©νμ¬ passwordRefλ₯Ό λ§λ€κ³ λΉλ°λ²νΈλ₯Ό μ λ ₯νλ Input μ»΄ν¬λνΈμ refλ‘ μ§μ νμ΅λλ€.
- μ΄λ©μΌμ μ λ ₯νλ Input μ»΄ν¬λνΈμ onSubmitEditing ν¨μλ₯Ό passwordRef λ₯Ό μ΄μ©ν΄μ λΉλ°λ²νΈλ₯Ό μ λ ₯νλ Input μ»΄ν¬λνΈλ‘ ν¬μ»€μ€κ° μ΄λλλλ‘ μμ±ν©λλ€.
- refλ keyμ²λΌ 리μ‘νΈμμ μμ μ»΄ν¬λνΈμ propsλ‘ μ λ¬λμ§ μμ΅λλ€. μ΄λ, forwardRef ν¨μλ₯Ό μ΄μ©νλ©΄ refλ₯Ό μ λ¬λ°μ μ μμ΅λλ€.
const Input = forwardRef(
(
{
(...)
},
ref
) => {
return (
<Container>
(...)
<StyledTextInput
ref={ref}
isFocused={isFocused}
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setIsFocused(false);
onBlur();
}} //inputμ ν¬μ»€μ€κ° ν릴λ νΈμΆλλ μ½λ°±
placeholder={placeholder}
secureTextEntry={isPassword} //λ¬Έμλ₯Ό κ°μΆλ κΈ°λ₯
returnKeyType={returnKeyType} //λ¦¬ν΄ ν€λ₯Ό λ μ΄λΈλ‘ μ€μ
maxLength={maxLength} //μ
λ ₯ ν μμλ μ΅λ λ¬Έμ μλ₯Ό μ ν
autoCapitalize="none" //μλ λλ¬Έμ λ³ν
autoCorrect={false} //μλ μμ
textContentType="none" //iOS
underlineColorAndroid="transparent" //Android TextInput λ°μ€ μ μμ
/>
</Container>
);
}
);
- TextInputμ μ λ ₯ λμ€ λ€λ₯Έ κ³³μ ν°μΉνλ©΄ ν€λ³΄λκ° μ¬λΌμ§λλ°, μ΄λ μ¬μ©μ νΈμλ₯Ό μν μΌλ°μ μΈ μ ν리μΌμ΄μ μ λμ₯ λ°©μμ λλ€.
- 리μ‘νΈ λ€μ΄ν°λΈμμ TouchableWithoutFeedback μ»΄ν¬λνΈμ Keyboard APIλ₯Ό μ¬μ©ν΄μ μ λμ₯ λ°©μμ ꡬνν μ μμ΅λλ€.
- μμ λ μ»΄ν¬λνΈλ λμ , μμΉμ λ°λΌ ν€λ³΄λκ° Input μ»΄ν¬λνΈλ₯Ό κ°λ¦¬λ λ¬Έμ λ₯Ό ν΄κ²°νμ§λ λͺ»ν©λλ€.
- react-native-keyboard-aware-scroll-view λΌμ΄λΈλ¬λ¦¬λ₯Ό μ΄μ©νλ©΄ μ λ¬Έμ λ₯Ό ν΄κ²°ν μ μμ΅λλ€. λΏλ§ μλλΌ focusκ° μλ TextInput μ»΄ν¬λνΈμ μμΉλ‘ μλ μ€ν¬λ‘€λλ κΈ°λ₯ λ± Input μ»΄ν¬λνΈμ νμν κΈ°λ₯λ€μ μ 곡ν©λλ€.
//import
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
<KeyboardAwareScrollView
contentContainerStyle={{ flex: 1 }}
extraScrollHeight={20} //μ€ν¬λ‘€λλ μμΉλ₯Ό μ‘°μ ν λ μ¬μ©
>
(...)
</KeyboardAwareScrollView>
//μ¬λ°λ₯Έ μ΄λ©μΌ νμ κ²μ¬
export const validateEmail = (email) => {
const regex = /^[0-9?A-z0-9?]+(\.)?[0-9?A-z0-9?]+@[0-9?A-z]+\.[A-z]{2}.?[A-z]{0,3}$/;
return regex.test(email);
};
//곡백 μ κ±°
export const removeWhitespace = (text) => {
const regex = /\s/g;
return text.replace(regex, "");
};
- TouchableOpacityλ ν°μΉ μ΄λ²€νΈ(onPress)λ₯Ό μ¬μ©ν μ μλ View
const Container = styled.TouchableOpacity`
(...)
`;
- react-native-safe-area-context λΌμ΄λΈλ¬λ¦¬κ° μ 곡νλ useSafeAreaInsets Hook ν¨μλ₯Ό μ΄μ©νλ©΄ λ ΈμΉλμμΈμ ν΄κ²°ν μ μλ€.
- useSafeAreaInsetsμ μ₯μ μ iOSλΏλ§μλλΌ μλλ‘μ΄λμμλ μ μ© κ°λ₯ν padding κ°μ μ λ¬νλ€.
//import
import { useSafeAreaInsets } from "react-native-safe-area-context";
//padding topκ³Ό bottomμ κ°μ useSafeAreaInsets ν¨μκ° μλ €μ£Όλ κ°λ§νΌ μ€μ νλ€.
const Container = styled.View`
(...)
padding: 0 20px;
padding-top: ${({ insets: { top } }) => top}px;
padding-bottom: ${({ insets: { bottom } }) => bottom}px;
`;
- expo-image-picker λΌμ΄λΈλ¬λ¦¬λ₯Ό ν΅ν΄μ κΈ°κΈ°μ μ¬μ§μ²©μ μ κ·Όν΄μ μ νλ μ¬μ§μ μ 보λ₯Ό κ°μ Έμ¬ μ μλ€.
- iOSμμλ μ¬μ§μ²©μ μ κ·ΌνκΈ° μν΄ μ¬μ©μμκ² κΆνμ μμ²νλ κ³Όμ μ΄ νμνλ―λ‘, κΆνμ μμ²νλ λΆλΆμ μΆκ°ν΄μΌ νλ€. μλλ‘λμμλ νΉλ³ν μ€μ μμ΄ μ¬μ§μ μ κ·Όν μ μλ€.
//install
expo install expo-image-picker
//import
import * as ImagePicker from "expo-image-picker";
import * as Permissions from "expo-permissions";
//iOS κΆν μμ²
useEffect(() => {
async () => {
try {
if (Platform.OS === "ios") {
const { status } = await Permissions.askAsync(
Permissions.CAMERA_ROLL
);
if (status !== "granted") {
Alert.alert(
"Photo Permission",
"Please turn on the camera roll permissions"
);
}
}
} catch (e) {
Alert.alert("Photo Permission Error", e.message);
}
};
}, []);
- μ¬μ§ λ³κ²½ λ²νΌμ ν΄λ¦νλ©΄ νΈμΆλλ ν¨μμμ κΈ°κΈ°μ μ¬μ§μ μ κ·ΌνκΈ° μν΄ νΈμΆλλ λΌμ΄λΈλ¬λ¦¬ ν¨μλ λ€μκ³Ό κ°μ κ°λ€μ ν¬ν¨ν κ°μ²΄λ₯Ό νλΌλ―Έν°λ‘ μ λ¬λ°λλ€.
- mediaTypes: μ‘°ννλ μλ£μ νμ
- allowsEditing: μ΄λ―Έμ§ μ ν ν νΈμ§ λ¨κ³ μ§ν μ¬λΆ
- aspect: μλλ‘μ΄λ μ μ© μ΅μ μΌλ‘ μ΄λ―Έμ§ νΈμ§μ μ¬κ°νμ λΉμ¨([x, y])
- quality: 0 ~ 1 μ¬μ΄μ κ°μ λ°μΌλ©° μμΆ νμ§μ μλ―Έ (1: μ΅λ νμ§)
const _handelEditButton = async () => {
try {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.cancelled) {
onChangeImage(result.uri);
}
} catch (e) {
Alert.alert("Photo Error", e.message);
}
};
- κΈ°κΈ°μ μ¬μ§μ μ κ·Όνλ ν¨μλ κ²°κ³Όλ₯Ό λ°ννλλ°, cancelled κ°μ ν΅ν΄ μ ν μ¬λΆλ₯Ό νμΈν μ μλ€. λ§μ½ μ¬μ©μκ° μ¬μ§μ μ ννλ€λ©΄ λ°νλ κ²°κ³Όμ uriλ₯Ό ν΅ν΄ μ νλ μ¬μ§μ μ£Όμλ₯Ό μ μ μλ€.
//μλ¨μ resultμ λ°ν κ°
{
"cancelled": true,
}
{
"cancelled": false,
"height": 000,
"type": "image",
"uri": "file:../...jpg",
"width": 000,
}
- νμ΄μ΄λ² μ΄μ€λ₯Ό μ΄μ©ν μ΄λ©μΌκ³Ό λΉλ°λ²νΈλ₯Ό μ΄μ©ν΄ μΈμ¦λ°λ ν¨μλ signInWithEmailAndPassword μ λλ€.
//utils/firebase.js
export const login = async ({ email, password }) => {
const { user } = await Auth.signInWithEmailAndPassword(email, password);
return user;
};
//screens/Login.js
import { login } from "../utils/firebase";
const _handleLoginButtonPress = async () => {
try {
const user = await login({ email, password });
Alert.alert("Login Success", user.email);
} catch (e) {
Alert.alert("Login Error", e.message);
}
};
- νμ΄μ΄λ² μ΄μ€λ₯Ό μ΄μ©ν μ΄λ©μΌκ³Ό λΉλ°λ²νΈλ₯Ό μ΄μ©ν΄ μ¬μ©μλ₯Ό μμ±νλ ν¨μλ createUserWithEmailAndPassword μ λλ€.
//utils/firebase.js
export const signup = async ({ email, password, name, photoUrl }) => {
const { user } = await Auth.createUserWithEmailAndPassword(email, password);
return user;
};
//screens/Signup.js
import { signup } from "../utils/firebase";
const _handleSignupButtonPress = async () => {
try {
const user = await signup({ email, password, name, photoUrl });
console.log(user);
Alert.alert("Signup Success", user.email);
} catch (e) {
Alert.alert("Signup Error", e.message);
}
};
- Spinner μ»΄ν¬λνΈλ λ‘κ·ΈμΈ νΉμ νμκ°μ μ΄ μ§νλλ λμ λ°μ΄ν°λ₯Ό μμ νκ±°λ λ²νΌμ μΆκ°λ‘ ν΄λ¦νλ μΌμ΄ λ°μνμ§ μλλ‘ λ°©μ§νλ κΈ°λ₯μ νλ€.
- Spinner μ»΄ν¬λνΈλ 리μ‘νΈ λ€μ΄ν°λΈμμ μ 곡νλ AcitivityIndicator μ»΄ν¬λνΈλ₯Ό μ΄μ©ν΄μ μ½κ² λ§λ€ μ μλ€.
- Spinner μ»΄ν¬λνΈλ₯Ό AuthStack λ΄λΉκ²μ΄μ μ νμ μ»΄ν¬λνΈλ‘ μ¬μ©νλ©΄ λ΄λΉκ²μ΄μ μ ν¬ν¨ν νλ©΄ μ 체λ₯Ό μ°¨μ§ν μ μμ΅λλ€. λ΄λΉκ²μ΄μ μ ν¬ν¨ν νλ©΄ μ 체λ₯Ό κ°μΈκΈ° μν΄μλ navigations ν΄λμ index.jsμμ AuthStack λ΄λΉκ²μ΄μ κ³Ό κ°μ μμΉμ Spinner μ»΄ν¬λνΈλ₯Ό μ¬μ©ν΄μΌ λ©λλ€.
import React, { useContext } from 'react';
import { ActivityIndicator } from 'react-native';
import styled, { ThemeContext } from 'styled-components/native';
const Container = styled.View`
(...)
`;
const Spinner = () => {
const theme = useContext(ThemeContext);
return (
<Container>
<ActivityIndicator size={'large'} color={theme.spinnerIndicator} />
</Container>
)
};
export default Spinner;
- Spinner μ»΄ν¬λνΈλ₯Ό AuthStack λ΄λΉκ²μ΄μ μ νμ μ»΄ν¬λνΈλ‘ μ¬μ©νλ©΄ λ΄λΉκ²μ΄μ μ ν¬ν¨ν νλ©΄ μ 체λ₯Ό μ°¨μ§ν μ μμ΅λλ€. λ΄λΉκ²μ΄μ μ ν¬ν¨ν νλ©΄ μ 체λ₯Ό κ°μΈκΈ° μν΄μλ navigations ν΄λμ index.jsμμ AuthStack λ΄λΉκ²μ΄μ κ³Ό κ°μ μμΉμ Spinner μ»΄ν¬λνΈλ₯Ό μ¬μ©ν΄μΌ λ©λλ€.
(...)
const Navigation = () => {
const { inProgress } = useContext(ProgressContext);
return (
<NavigationContainer>
<AuthStack />
{inProgress && <Spinner />}
</NavigationContainer>
);
};
(...)
- createContext ν¨μλ₯Ό μ΄μ©ν΄ Contextλ₯Ό μμ±νκ³ , Provider μ»΄ν¬λνΈμ valueμ Spinner μ»΄ν¬λνΈμ λ λλ§ μνλ₯Ό κ΄λ¦¬ν inPrgress μν λ³μμ μνλ₯Ό λ³κ²½ν μ μλ ν¨μλ₯Ό μ λ¬ν©λλ€.
//contexts/Progress.js
import React, { useState, createContext } from 'react';
//Context μμ±
const ProgressContext = createContext({
inProgress: false,
spinner: () => {},
});
//ProgressProvider
const ProgressProvider = ({ children }) => {
const [inProgress, setInProgress] = useState(false);
const spinner = {
start: () => setInProgress(true),
stop: () => setInProgress(false),
};
const value = { inProgress, spinner };
return (
<ProgressContext.Provider value={value}>
{children}
</ProgressContext.Provider>
);
};
export { ProgressContext, ProgressProvider };
//contexts/index.js
import {ProgressContext, ProgressProvider } from './Progress';
export { ProgressContext, ProgressProvider };
//src/App.js
<ThemeProvider theme={theme}>
<ProgressProvider>
<StatusBar barStyle="light-content" />
<Navigation />
</ProgressProvider>
</ThemeProvider>
- MainStack λ΄λΉκ²μ΄μ μμ MainTab λ΄λΉκ²μ΄μ μ΄ νλ©΄μΌλ‘ μ¬μ©λλ Screen μ»΄ν¬λνΈμ nameμ "Main"μΌλ‘ μ€μ λμ΄ μλ€. ν€λμ νμ΄νκ³Ό κ΄λ ¨ν΄ νΉλ³ν μ€μ νμ§ μμΌλ©΄ Screen μ»΄ν¬λνΈμ nameμ μ€μ λ κ°μ΄ ν€λμ νμ΄νλ‘ λκΈ° λλ¬Έμ, νλ‘ν νλ©΄κ³Ό μ±λ λͺ©λ‘ λͺ¨λ 'Main'μΌλ‘ νμ΄νμ΄ λνλλ€.
<Stack.Navigator
initialRouteName="Main"
(...)
>
<Stack.Screen name="Main" component={MainTab} />
(...)
</Stack.Navigator>
- MainTab λ΄λΉκ²μ΄μ μ MainStack λ΄λΉκ²μ΄μ μ νλ©΄μΌλ‘ μ¬μ©λμκΈ° λλ¬Έμ λ€λ₯Έ νλ©΄λ€κ³Ό λ§μ°¬κ°μ§λ‘ propsλ₯Ό ν΅ν΄ navigationκ³Ό routeλ₯Ό μ λ¬ λ°λλ€.
- routeμ ν¬ν¨λ stateμ κ°μ λ€μκ³Ό κ°λ€
- index: νμ¬ λ λλ§ λλ νλ©΄μ μΈλ±μ€
- routeNames: νλ©΄μΌλ‘ μ¬μ©λλ Navigator μ»΄ν¬λνΈμμ Screen μ»΄ν¬λνΈλ€μ name μμ±μ λ°°μ΄λ‘ κ°λλ€.
- type: νμ¬ νλ©΄μΌλ‘ μ¬μ©λλ Navigator μ»΄ν¬λνΈμ νμ μ΄λ©°, MainTab λ΄λΉκ²μ΄μ μ ν λ΄λΉκ²μ΄μ μ΄κΈ° λλ¬Έμ 'tab' κ°μ κ°λλ€.
//routeμ state
{
"index": 0,
"routeNames": [
"Channel List",
"Profile",
],
"type": "tab",
...
}
//MainTab
useEffect(() => {
const titles = route.state?.routeNames || ['Channels'];
const index = route.state?.index || 0;
navigation.setOptions({ headerTitle: titles[index ]});
}, [route]);
- νμ§λ§ μμ λ°©μλλ‘ νλ©΄ routeμ stateμ μ§μ μ κ·Όν΄μ λ°μνλ κ²½κ³ λ©μμ§κ° λ¬λ€. μ΄κ±Έ ν΄κ²°νλ €λ©΄ getFocusedRouteNameFromRoute λ©μλλ₯Ό μ¬μ©νλ©΄ λλ€.
- π κ΄λ ¨ μ΄μ: Alchemist85K/my-first-react-native#26
useEffect(() => {
const screenName = getFocusedRouteNameFromRoute(route) || 'Channels';
navigation.setOptions({
headerTitle: screenName,
});
}, [route]);
- π κ΄λ ¨ μ΄μ: Alchemist85K/my-first-react-native#28
- /node_modules/react-native/Libraries/Core/Timers/JSTimers.js
- MAX_TIMER_DURATION_MS λΌλ λ³μ κ°μ 60 * 1000 μμ 10000 * 1000μΌλ‘ λ³κ²½
- νμ΄μ΄λ² μ΄μ€μμ μ 곡νλ νμ΄μ΄μ€ν μ΄λ NoSQL λ¬Έμ μ€μ¬μ λ°μ΄ν°λ² μ΄μ€μ΄λ€.
- SQL λ°μ΄ν°λ² μ΄μ€μ λ¬λ¦¬ ν
μ΄λΈμ΄λ νμ΄ μκ³ μ»¬λ μ
, λ¬Έμ, νλλ‘ κ΅¬μ±λλ€.
- 컬λ μ μ λ¬Έμμ 컨ν μ΄λ μν μ νλ©°, λͺ¨λ λ¬Έμλ νμ 컬λ μ μ μ μ₯λλ€.
- λ¬Έμλ νμ΄μ΄μ€ν μ΄μ μ μ₯ λ¨μλ‘ κ°μ΄ μλ νλλ₯Ό κ°λλ€. λ¬Έμμ κ°μ₯ ν° νΉμ§μ 컬λ μ μ νλλ‘ κ°μ§ μ μλ€.
- νμ΄μ΄μ€ν μ΄λ μΌλ°μ μΈ λ°μ΄ν°λ² μ΄μ€μ λ¬λ¦¬ λ°μ΄ν°λ² μ΄μ€μ λ΄μ©μ΄ μμ λλ©΄ μ€μκ°μΌλ‘ λ³κ²½λ λ΄μ©μ μ μ μλ€.
- 컬λ μ κ³Ό λ¬Έμλ νμ μ μΌν IDλ₯Ό κ°κ³ μμ΄μΌ νλ€λ κ·μΉμ΄ μλ€.
//νμ΄μ΄μ€ν μ΄ λ³΄μ κ·μΉ μμ
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /channels/{channel} {
allow read, write: if request.auth.uid != null;
}
}
}
- μ§κΈκΉμ§ λ§μ μμ λ°μ΄ν°λ₯Ό λ λλ§ν λ ScrollView μ»΄ν¬λνΈλ₯Ό μ΄μ©ν΄ νλ©΄μ΄ λμ΄κ°λλ‘ μ€ν¬λ‘€μ΄ μμ±λμ΄ νμΈν μ μλλ‘ λ§λ€μμ΅λλ€.
- FlatListμ»΄ν¬λνΈλ ScrollView μ»΄ν¬λνΈμ κ°μ μν μ νλλ°, ScrollView μ»΄ν¬λνΈλ λ λλ§ν΄μΌ νλ λͺ¨λ λ°μ΄ν°λ₯Ό νλ²μ λ λλ§ν©λλ€. μ¦, λ°μ΄ν°μ μμ μκ³ μμ λ μ¬μ©νλ κ²μ΄ μ’κ³ , λ°μ΄ν°κ° λ§€μ° λ§μΌλ©΄ λ λλ§ μλκ° λλ €μ§κ³ λ©λͺ¨λ¦¬ μ¬μ©λμ΄ μ¦κ°νλ λ± μ±λ₯μ΄ μ νλ©λλ€.
- κ·Έμλ°ν΄, FlatListλ νλ©΄μ μ μ ν μμ λ°μ΄ν°λ§ λ λλ§νκ³ μ€ν¬λ‘€μ μ΄λμ λ§μΆ° νμν λΆλΆμ μΆκ°μ μΌλ‘ λ λλ§νλ νΉμ§μ΄ μμ΅λλ€.
- FlatListμλ κΈ°λ³Έμ μΌλ‘ 3κ°μ§ μμ±μ΄ μμ΅λλ€.
- data: μ²μ λ λλ§ν νλͺ©μ λ°μ΄ν°λ₯Ό λ°°μ΄λ‘ μ λ¬νλ€.
- renderItem: μ λ¬λ λ°°μ΄μ νλͺ©μ μ΄μ©ν΄ λ λλ§νλ ν¨μλ₯Ό μμ±ν΄μΌ νλ€.
- keyExtractor: keyλ₯Ό μΆκ°νκΈ° μν΄ κ³ μ ν κ°μ λ°ννλ ν¨μλ₯Ό μ λ¬ν΄μΌ νλ€.
<FlatList
keyExtractor={item => item['id'].toString()}
data={channels}
renderItem={({ item }) => {
return (
<Item item={item} onPress={_handleItemPress} />
)
}}
windowSize={3}
/>
- FlatListμμ λ λλ§ λλ λ°μ΄ν°μ μμ μ‘°μ νκ³ μΆλ€λ©΄ windowSize μμ±μ μΆκ°ν΄μ κ°μ μνλ κ°μΌλ‘ μ€μ νλ©΄ λλ€.
- windowSizeμ κ°μ μμ κ°μΌλ‘ λ³κ²½νλ©΄ λ λλ§λλ λ°μ΄ν°κ° μ€μ΄λ€μ΄ λ©λͺ¨λ¦¬μ μλΉλ₯Ό μ€μ΄κ³ μ±λ₯μ ν₯μ μν¬ μ μμ§λ§, λΉ λ₯΄κ² μ€ν¬λ‘€νλ μν©μμ 미리 λ λλ§λμ§ μμ λΆλΆμ μκ°μ μΌλ‘ λΉ λ΄μ©μ΄ λνλ μ μλ€λ λ¨μ μ΄ μλ€.
<FlatList
(...)
windowSize={3}
/>
- React.Memoλ useMemo Hook ν¨μμ λΉμ·νμ§λ§, λΆνμν ν¨μμ μ¬μ°μ°μ λ°©μ§νλ useMemoμ λ¬λ¦¬ μ»΄ν¬λνΈμ 리λ λλ§μ λ°©μ§νλ€λ μ°¨μ΄κ° μλ€.
- React.Memoλ μ»΄ν¬λνΈλ₯Ό κ°μΈλ κ²μΌλ‘ κ°λ¨ν μ μ©ν μ μλ€.
- React.Memoλ₯Ό μ¬μ©νλ©΄ Item μ»΄ν¬λνΈλ propsκ° λ³κ²½λ λκΉμ§ 리λ λλ§λμ§ μλλ€.
const Item = React.memo(({ item: { id, title, description, createdAt }, onPress }) => {
const theme = useContext(ThemeContext);
console.log(`Item ${id}`);
return (
<ItemContainer onPress={() => onPress({ id, title })}>
<ItemTextContainer>
<ItemTitle>{title}</ItemTitle>
<ItemDescription>{description}</ItemDescription>
</ItemTextContainer>
<ItemTime>{createdAt}</ItemTime>
<MaterialIcons
name="keyboard-arrow-right"
size={24}
color={theme.listIcon}
/>
</ItemContainer>
);
- firebaseμ Cloud Firestoreμμ μ€μκ° λ°μ΄ν°λ₯Ό λ°μμ€κΈ° μν΄μλ onSnapshop λ©μλλ₯Ό μ΄μ©νμ¬ λ°μ΄ν°λ₯Ό μμ ν μ μμ΅λλ€.
- onSnapshop λ©μλλ μμ λκΈ° μνλ‘ μλ€κ° λ°μ΄ν°λ² μ΄μ€μ λ¬Έμκ° μΆκ°λκ±°λ μμ λ λλ§λ€ μ§μ λ ν¨μκ° νΈμΆλ©λλ€.
- μ΄λ, μ€λ¦μ°¨μ, λ΄λ¦Όμ°¨μμ orderBy λ©μλλ₯Ό μ΄μ©ν΄μ ν μ μμ΅λλ€.
useEffect(() => {
const unsubscribe = DB.collection('channels')
.orderBy('createdAt', 'desc')
.onSnapshot(snapshot => {
const list = [];
snapshot.forEach(doc => {
list.push(doc.data());
});
setChannels(list);
});
return () => unsubscribe();
}, []);
- μ±ν νλ©΄μμ μ¬μ©ν μ μλ κΈ°λ₯μ λ€μνκ² μ 곡νλ react-native-gifted-chat λΌμ΄λΈλ¬λ¦¬
- react-native-gifted-chat λΌμ΄λΈλ¬λ¦¬μ GiftedChat μ»΄ν¬λνΈλ λ€μν μ€μ μ΄ κ°λ₯νλλ‘ λ§μ μμ±μ μ 곡νλ€.
- μ λ ₯λ λ΄μ©μ μ€μ λ μ¬μ©μμ μ 보 λ° μλμΌλ‘ μμ±λ IDμ ν¨κ» μ λ¬ νλ κΈ°λ₯
- μ μ‘ λ²νΌμ μμ νλ κΈ°λ₯
- μ€ν¬λ‘€μ μμΉμ λ°λΌ μ€ν¬λ‘€ μμΉλ₯Ό λ³κ²½νλ λ²νΌ λ λλ§
<Container>
<GiftedChat
listViewProps={{
style: { backgroundColor: theme.background },
}}
placeholder="Enter a Message"
messages={messages}
user={{ _id: uid, name, avatar: photoUrl }}
onSend={_handleMessageSend}
alwaysShowSend={true}
textInputProps={{
autoCapitalize: "none",
autoCorrect: false,
textContentType: "none",
underlineColorAndroid: "transparent",
}}
multiline={false}
renderUsernameOnMessage={true}
scrollToBottom={true}
renderSend={(props) => <SendButton {...props} />}
/>
</Container>
- userμ λ€μκ³Ό κ°μ ννλ‘ μ¬μ©μμ μ 보λ₯Ό μ λ ₯ν΄λλ©΄ onSendμ μ μν ν¨μκ° νΈμΆλ λ μ λ ₯λ λ©μμ§μ μ¬μ©μμ μ 보λ₯Ό ν¬ν¨ν κ°μ²΄λ₯Ό μ λ¬νλ€.
User {
_id: string | number;
name: string;
avatar: string | renderFunction;
}
Message {
_id: string | number;
text: string;
createdAt: Date | number;
user: User;
...
}