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" />
Text
: ๊ธ์
<Text numberOfLines={1} ellipsizeMode="tail">
์ ๋ชฉ
</Text>
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} // ๊ฐ ์์ดํ
์ ๊ณ ์ ํค๋ฅผ ์ง์
showsHorizontalScrollIndicator={false} // ๊ฐ๋ก ์คํฌ๋กค ์ธ๋์ผ์ดํฐ ์จ๊น
horizontal // ๊ฐ๋ก ์คํฌ๋กค ํ์ฉ
ItemSeparatorComponent={() => <View style={{ width: 16 }} />} // ์์ดํ
์ฌ์ด์ ๊ฐ๊ฒฉ
contentContainerStyle={{flexGrow: 1, paddingHorizontal: 16}}
numColumns={2} // grid์ฒ๋ผ ์ฌ์ฉ
/>
);
};
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
: ํฐ์น ์ด๋ฒคํธ ๋ฐ์ ์ ํฌ๋ช
๋๊ฐ ๋ณํจ. (ํฐ์น ํจ๊ณผ๋ฅผ ์ฃผ๋ ๋ฐ ์ฃผ๋ก ์ฌ์ฉ)
ScrollView
: ์คํฌ๋กค ๊ฐ๋ฅ. horizontal ๋ถ์ด๋ฉด ๊ฐ๋ก ์คํฌ๋กค ๊ฐ๋ฅ
<ScrollView horizontal>
...
</ScrollView>
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}/>
# ์ค์น
yarn add nativewind
yarn add --dev tailwindcss@3.3.2
# tailwind.config.js ํ์ผ ์์ฑ
npx tailwindcss init
tailwind.config.js
module.exports = {
content: ['./App.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
+ plugins: ["nativewind/babel"],
};
global.d.ts
/// <reference types="nativewind/types" />
# ์ค์น
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>
);
}
/* App.tsx */
function App() {
const Stack = createNativeStackNavigator();
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" options={{headerShown: false}}>
{props => <HomeScreen {...props} paramName="home" />} // ์ด๋ฐ์์ผ๋ก ์ฌ์ฉํจ
</Stack.Screen>
<Stack.Screen name="Profile" options={{headerShown: false}}>
{props => <ProfileScreen {...props} paramName="profile" />}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
);
}
Params ๋๊ธฐ๊ธฐ
/* Home.jsx */
import {RouteProp, ParamListBase, useNavigation} from '@react-navigation/native';
function Home() {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const goToProfile = () => {
navigation.navigate('Profile', { // ๋๋ฒ์งธ ์ธ์๋ก ์ ๋ฌํจ
id: 1,
name: 'tata',
});
};
return (
<Pressable onPress={goToProfile}>
<Text>Go to Profile</Text>
</Pressable>
);
}
Params ์ ๋ฌ๋ฐ๊ธฐ
/* Profile.jsx */
import {useRoute} from '@react-navigation/native';
interface IProfile {
id: number;
name: string;
}
interface Props {
paramName: string;
}
function Profile({}: Props) {
const route = useRoute<RouteProp<{Profile: IProfile}, 'Profile'>>();
const {params} = route;
return (
<View>
<Text>Profile</Text>
<Text>{params.id}</Text>
<Text>{params.name}</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}์ผ๋ก ๊ฐ์
๋์์ต๋๋ค.`,
});
useColorScheme
import React, { ReactNode } from 'react';
import { SafeAreaView, StatusBar, useColorScheme } from 'react-native';
interface Props {
children: ReactNode;
}
const DefaultLayout = ({children}: Props) => {
const theme = useColorScheme();
const isDarkMode = theme === 'dark';
return (
<SafeAreaView>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={isDarkMode ? '#2B2B30' : '#F4F4F4'}
/>
{children}
</SafeAreaView>
);
};
export default DefaultLayout;