React Native๋ JavaScript์ React๋ฅผ ์ฌ์ฉํ์ฌ iOS์ Android ๋ชจ๋ฐ์ผ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ ์ ์๋ ์คํ ์์ค ํ๋ ์์ํฌ์ด๋ค.
# ์ฒ์ ํ ๋ฒ๋ง ์ค์น
# npm install -g expo-cli
npx create-expo-app --template
# ํ์
์คํฌ๋ฆฝํธ๋ก ํ๊ณ ์ถ์ ๊ฒฝ์ฐ
# npx create-expo-app -t expo-template-blank-typescript
Mac์์ CLI๋ก React-Native ์์ํ๊ธฐ
โ Android Studio a
โ Xcode i
xcode ์๋ฎฌ๋ ์ดํฐ
Core Components โ React Native์ ๊ธฐ๋ณธ์ ์ธ UI ์์๋ค
SafeAreaView
: ํ๋ฉด ์๋จ์ด๋ ํ๋จ์ ํ ๋ฐ ๋ฑ๊ณผ ๊ฐ์ ์์๋ก๋ถํฐ ์ปจํ
์ธ ๋ฅผ ์์ ํ๊ฒ ๋ฐฐ์นํ ์ ์๋๋ก ๋์์ค. (OS ๋ฒ์ 11 ์ด์์ด ์ค์น๋ iOS ๊ธฐ๊ธฐ์๋ง ์ ์ฉ๋จ)
StatusBar
: ๋ชจ๋ฐ์ผ ๋๋ฐ์ด์ค์ ์ํ ํ์์ค์ ๋ํ ์ค์ ์ ์ ์ด ๊ฐ๋ฅ.
<StatusBar backgroundColor="white" barStyle="dark-content" />
TextIput
: ์ฌ์ฉ์๋ก๋ถํฐ ํ
์คํธ๋ฅผ ์
๋ ฅ์ ๋ฐ์. (๋น๋ฐ๋ฒํธ์ผ ๊ฒฝ์ฐ secureTextEntry ์์ฑ ์ถ๊ฐํ๊ธฐ)
FlatList
: ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๋ ๋๋งํ๊ธฐ ์ํ ๋ฆฌ์คํธ ์ปดํฌ๋ํธ์.
/* FlatList ์ปดํฌ๋ํธ์ ์ต์
*/
// data => ๋ ๋๋งํ ๋ฐ์ดํฐ ๋ฐฐ์ด์ ์ค์
// renderItem => ๊ฐ ์์ดํ
์ ๋ ๋๋งํ๊ธฐ ์ํ ์ฝ๋ฐฑ ํจ์๋ฅผ ์ค์
// keyExtractor => ๊ณ ์ ํ ํค ๊ฐ
const MyComponent = () => {
// ๊ฐ์์ ๋ฐ์ดํฐ ๋ฐฐ์ด
const data = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
// ์ถ๊ฐ์ ์ธ ์์ดํ
๋ค...
];
return (
<FlatList
data={data} // ํ์ํ ๋ฐ์ดํฐ ๋ฐฐ์ด
renderItem={({ item }) => <ItemComponent {...item} />} // ์ปดํฌ๋ํธ๋ฅผ ํตํด ์์ดํ
์ ๋ ๋๋ง
keyExtractor={(item) => item.id} // ๊ฐ ์์ดํ
์ ๊ณ ์ ํค๋ฅผ ์ง์
/>
);
};
KeyboardAvoidingView
: ํค๋ณด๋๊ฐ ํ๋ฉด์ ๊ฐ๋ฆฌ๋ ์ํฉ์์ ์๋์ผ๋ก ํ๋ฉด์ ์กฐ์ ํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฆฌ์ง ์๋๋ก ๋์์ฃผ๋ ์ญํ ์ ํจ.
import { KeyboardAvoidingView } from 'react-native';
...
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
</KeyboardAvoidingView>
Pressable
: ํฐ์น ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ์ฌ ์ฌ์ฉ์์ ํฐ์น ์
๋ ฅ์ ๋ฐ์ํ๋ ์ญํ ์ ํจ.
<Pressable onPress={onPressFunction}>
<Text>I'm pressable!</Text>
</Pressable>
-----
<Pressable
hitSlop={20} // hitSlop => ์ฌ์ฉ์๊ฐ ํฐ์น ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๋ ์์ญ์ ํ์ฅํ๋ ์ญํ
pressRetentionOffset={30} // pressRetentionOffset => ์ฌ์ฉ์๊ฐ ํฐ์น๋ฅผ ์์ํ ํ์๋ ํด๋น ์์ญ์ ๋๋ฅด๊ณ ์๋ ๋์์๋ง ํฐ์น ์ด๋ฒคํธ๋ฅผ ์ ์งํ๋ ์ญํ
>
<Text>I'm pressable!</Text>
</Pressable>
TouchableOpacity
: ํฐ์น ์ด๋ฒคํธ ๋ฐ์ ์ ํฌ๋ช
๋๊ฐ ๋ณํจ. (ํฐ์น ํจ๊ณผ๋ฅผ ์ฃผ๋ ๋ฐ ์ฃผ๋ก ์ฌ์ฉ)
TextInput ์ปดํฌ๋ํธ
onChangeText
: TextInput์ ํ
์คํธ๋ฅผ ์
๋ ฅํ ๋๋ง๋ค ํธ์ถ๋๋ ์ด๋ฒคํธ
onSubmitEditing
: TextInput์์ ํ
์คํธ๋ฅผ ์
๋ ฅํ๊ณ ์ ์ถํ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
Pressable ์ปดํฌ๋ํธ
onPress
: Pressable๋ฅผ ํญํ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
TouchableOpacity ์ปดํฌ๋ํธ
onPress
: TouchableOpacity๋ฅผ ํญํ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
ScrollView ์ปดํฌ๋ํธ
onScroll
: ScrollView๊ฐ ์คํฌ๋กค๋๋ ๋์ ํธ์ถ๋๋ ์ด๋ฒคํธ
onContentSizeChange
: ScrollView์ ๋ด์ฉ ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
FlatList ์ปดํฌ๋ํธ
onEndReached
: ์คํฌ๋กคํ์ฌ ๋ฆฌ์คํธ์ ๋์ ๋๋ฌํ์ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
onRefresh
: ๋ฆฌ์คํธ๋ฅผ ์๋ก๊ณ ์นจ ํ๋๋ก ์์ฒญํ์ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
Image ์ปดํฌ๋ํธ
onLoad
: ์ด๋ฏธ์ง ๋ก๋๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
onError
: ์ด๋ฏธ์ง ๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋๋ ์ด๋ฒคํธ
// StyleSheet ๊ฐ์ ธ์ค๊ธฐ
import { StyleSheet, Text, View } from 'react-native';
const LotsOfStyles = () => {
return (
<View style={styles.container}>
<Text style={styles.red}>just red</Text>
<Text style={styles.bigBlue}>just bigBlue</Text>
<Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
<Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingTop: Platform.OS === "android" ? 20 : 0,
},
bigBlue: {
color: 'blue',
fontWeight: 'bold', // ์นด๋ฉ์ผ์ด์ค ์ฌ์ฉ
fontSize: 30,
},
red: {
color: 'red',
},
});
export default LotsOfStyles;
โ ์ฐธ๊ณ
styled-components๋ ์ฌ์ฉํ ์ ์๋ค.
# ์ค์น
npm install --save styled-components
# ํ์
์คํฌํฌ๋ฆฝํธ ์ฌ์ฉ ์ ์ถ๊ฐ ์ค์น
# npm install -D @types/styled-components @types/styled-components-react-native
/* styled-components ์ฌ์ฉ ์์ */
import { styled } from 'styled-components';
const StyledTextInput = styled.TextInput`
background-color: pink;
`;
...
<StyledTextInput placeholder="์ด๋ฉ์ผ" value={email} onChangeText={setEmail}/>
# ์ค์น
npm install react-native-svg
# Expo์ผ ๊ฒฝ์ฐ โ
# expo install react-native-svg
npm install -D react-native-svg-transformer
metro.config.js ํ์ผ ์์ฑ
/* metro.config.js */
const { getDefaultConfig } = require("expo/metro-config");
module.exports = (() => {
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config;
config.transformer = {
...transformer,
babelTransformerPath: require.resolve("react-native-svg-transformer"),
};
config.resolver = {
...resolver,
assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...resolver.sourceExts, "svg"],
};
return config;
})();
React Navigation โ ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ ์ ์๊ฒ ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
npm install @react-navigation/native @react-navigation/native-stack
@react-navigation/bottom-tabs @react-navigation/material-top-tabs --save
npm install react-native-screens react-native-safe-area-context
# expo์ผ ๊ฒฝ์ฐ
# npx expo install react-native-screens react-native-safe-area-context
/* App.js */
// NavigationContainer, createNativeStackNavigator ๊ฐ์ ธ์ค๊ธฐ
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
// ํ์
์คํฌ๋ฆฝํธ
// const navigation = useNavigation<NativeStackNavigationProp<any>>();
const MyStack = () => {
return (
<NavigationContainer> // ๊ฐ์ธ๊ธฐ
<Stack.Navigator> // ๊ฐ์ธ๊ธฐ2
<Stack.Screen // ์ปดํฌ๋ํธ ๊ฒฝ๋ก ์ง์
name="Home"
component={HomeScreen}
options={{ headerShown: false }} // ์๋จ์ ํ์ดํ ์ ๋ณด์ด๊ฒ ํด์ค
// options={{ title: 'Welcome' }} => ์๋จ์ ํ์ดํ ๋ณ๊ฒฝ
/>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator> // ๊ฐ์ธ๊ธฐ2
</NavigationContainer> // ๊ฐ์ธ๊ธฐ
);
};
function Home({ navigation }) {
const goToProfile = () => {
navigation.navigate('Profile'); // Profile๋ก ๊ฒฝ๋ก ์ด๋.
// navigation.goBack() => ๋ค๋ก๊ฐ๊ธฐ
// navigation.push('Profile') => ์ ํ๋ฉด์ ์คํ์ ๋ฃ์
// navigation.replace('Profile') => ํ์ฌ ํ๋ฉด์ ์ ํ๋ฉด์ผ๋ก ๊ต์ฒด
// navigation.reset({ index: 0, routes: [{ name: 'Profile' }] }) => routes ๋ฐฐ์ด๋ก ํ์คํ ๋ฆฌ๋ฅผ ์ด๊ธฐํํ๋ฉฐ, index๊ฐ 0์ธ Profile๋ก ์ด๋
// navigation.setOptions({title: 'Notice'}) => ์๋จ์ ํ์ดํ์ Notice๋ก ๋ณ๊ฒฝ
};
return (
<Pressable onPress={goToProfile}>
<Text>Go to Profile</Text>
</Pressable>
);
}
Params ๋๊ธฐ๊ธฐ
/* Home.jsx */
function Home({ navigation }) {
const goToProfile = () => {
navigation.navigate('Profile', { // ๋๋ฒ์งธ ์ธ์๋ก ์ ๋ฌํจ
userId: 1,
nickname: 'tata',
});
};
return (
<Pressable onPress={goToProfile}>
<Text>Go to Profile</Text>
</Pressable>
);
}
Params ์ ๋ฌ๋ฐ๊ธฐ
/* Profile.jsx */
function Profile({ navigation, route }) {
const { userId, nickname } = route.params;
return (
<View>
<Text>Profile</Text>
<Text>{userId}</Text>
<Text>{nickname}</Text>
</View>
);
}
... + map ์ฌ์ฉ
function Home({ navigation }) {
const profiles = [
{ userId: 1, nickname: 'tata' },
{ userId: 2, nickname: 'coco' },
{ userId: 3, nickname: 'momo' },
];
const goToProfile = (userId) => {
navigation.navigate('Profile', {
userId,
});
};
return (
<View>
{profiles.map((profile) => (
<Pressable key={profile.userId} onPress={() => goToProfile(profile.userId)}>
<Text>{profile.nickname}</Text>
</Pressable>
))}
</View>
);
}
import { useNavigation } from '@react-navigation/native';
function Home() {
const navigation = useNavigation();
const goToProfile = () => {
navigation.navigate('Profile'); // Profile๋ก ๊ฒฝ๋ก ์ด๋.
// navigation.goBack() => ๋ค๋ก๊ฐ๊ธฐ
// navigation.push('Profile') => ์ ํ๋ฉด์ ์คํ์ ๋ฃ์
// navigation.replace('Profile') => ํ์ฌ ํ๋ฉด์ ์ ํ๋ฉด์ผ๋ก ๊ต์ฒด
// navigation.reset({ index: 0, routes: [{ name: 'Profile' }] }) => routes ๋ฐฐ์ด๋ก ํ์คํ ๋ฆฌ๋ฅผ ์ด๊ธฐํํ๋ฉฐ, index๊ฐ 0์ธ Profile๋ก ์ด๋
// navigation.setOptions({title: 'Notice'}) => ์๋จ์ ํ์ดํ์ Notice๋ก ๋ณ๊ฒฝ
};
return (
<Pressable onPress={goToProfile}>
<Text>Go to Profile</Text>
</Pressable>
);
try {
...
} catch (error) {
Alert.alert(
'์ด๋ฏธ ์กด์ฌํ๋ ํ์์
๋๋ค.',
error.message,
[{ text: '๋ซ๊ธฐ', onPress: () => console.log('๋ซ๊ธฐ') }],
{ cancelable: true } // alert์ฐฝ์ ๋ฐ์ ๋๋ฌ๋ alert์ฐฝ์ ๋ซ๊ฒ ํด์ค
);
}
# ์ค์น
npm install --save react-native-toast-message
App.js
/* App.js */
import Toast from 'react-native-toast-message';
export function App(props) {
return (
<>
{/* ... */}
<Toast />
</>
);
components/SignupButton.jsx
/* SignupButton.jsx */
import Toast from 'react-native-toast-message';
...
Toast.show({
type: 'success',
text1: 'ํ์๊ฐ์
์๋ฃ',
text2: `${email}์ผ๋ก ๊ฐ์
๋์์ต๋๋ค.`,
});