Delivery icons created by dreamicons - Flaticon
์ฒซ ์์(setting)
- ์ด๊ธฐ ์ธํ (Expo๋ง๊ณ React Native CLI Quickstart): ๋ฐ๋์ ๋ฐ๋ผํ๊ธฐ
- java 17 ๋ฒ์ ์ค์นํ๋ฉด ์ ๋จ(11๋ฒ์ ์ค์นํ ๊ฒ), JAVA_HOME ํ๊ฒฝ ๋ณ์ ์ค์ ๋ ์ ํด ๋์ ๊ฒ(macOS JAVA_HOME ์ธํ ๋ฒ)
- Android 13(ํฐ๋ผ๋ฏธ์)์ด ์์ด์ผ ํจ. ๊ฐ์๊ธฐ๊ธฐ๋ Nexus 5๋ก ๋ฐ์ ๊ฒ
- ํฐ๋ฏธ๋์ adb ์ ๋ ฅํด์ ์ ๋จ๋ฉด adb ์ค์น ํ์, ANDROID_HOME ํ๊ฒฝ๋ณ์๋
- ์ฝ์ด๋ณด๋ฉด ์ข์ ๋ฒจ๋กํผํธ๋์ ๊ธ
# ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ณ ์ ํ๋ ํด๋๋ก ์ด๋
npx react-native init FoodDeliveryApp
# ๋ค์ ๋ง์ด ๋จ๋ฉด y ์
๋ ฅ
Need to install the following packages:
[email protected]
Ok to proceed? (y)
# mac์ด๋ฉด ๋ค์ ์ฝ๋๋ ์
๋ ฅ(๋งฅ ๋น๋ฐ๋ฒํธ ์
๋ ฅ ํ์)
sudo gem install cocoapods
# cocoapods ์ค์น์ ruby ์ค๋ฅ๊ฐ ๋๋ฉด ํฐ๋ฏธ๋์ ๋์ค๋ ๋ช
๋ น์ด ์
๋ ฅ ํ cocoapods ์ฌ์ค์น(์๋ ๋ฒ์ ์ ๋ฌ๋ผ์ง ์ ์์ผ๋ ์ฃผ์)
sudo gem install activesupport -v 6.1.7.6
# ๋ง์ง๋ง์ผ๋ก pod ์ค์น
cd ./FoodDeliveryApp/ios && pod install
์ ๊น!! ์ด ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํ๋ฉด ํญ์ ์ต์ ๋ฒ์ ์ react๋ฅผ ๋ฐ์์ค๋ฏ๋ก ๊ฐ์ข์ ๋ฒ์ (0.66)๊ณผ ์ผ์นํ์ง ์๊ฒ ๋จ. ํ์ฌ ์ต์ ๋ฒ์ ์ 0.72์ด๋ผ์ ์๋นํ ์ฐจ์ด๊ฐ ๋จ. ๊ฐ์ข๋ ๋์ผํ ๋ฒ์ ์ผ๋ก ํ์ง ์์ผ๋ฉด ๋ง์ ์คํธ๋ ์ค๋ฅผ ๋ฐ์ ์ ์์. ๊ฐ์ข๋ ๋์ผํ ๋ฒ์ ์ผ๋ก ํ๋ ค๋ฉด ์ด๋ฏธ ์ด๋ฐ ์ธํ ์ด ๋ค ๋์ด ์๋ setting ํด๋๋ฅผ git clone๋ฐ์ ์์ํ๋ ๊ฒ์ด ์ข์(ํด๋ก ํ npm i && npx pod-install ์ํ ํ์). 0.71์ด๋ 0.72 ๋ฒ์ ์์ค์ฝ๋๋ ๊นํ์ ์กด์ฌํ๋ ๊ฐ๊ฐ์ ํด๋ ์ฐธ๊ณ . 0.72๋ฒ์ ์ผ๋ก ํด๋ ์ ๋์๊ฐ๊ธด ํจ.
๋ณดํต์ ๊ฐ์์ฉ์ผ๋ก ์๋์์ฑ ์ ์ข์ํ๋๋ฐ RN์ ์๋์์ฑํ์ง ์์ผ๋ฉด ๋ค์ดํฐ๋ธ๋จ๊น์ง ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ค์
ํ๋ก์ ํธ ํด๋์์ ๋ค์ ๋ช ๋ น์ด๋ก ์ฑ ์คํ ๊ฐ๋ฅ
npm run android # ์๋๋ก์ด๋ ์คํ ๋ช
๋ น์ด
npm run ios # ์์ดํฐ ์คํ ๋ช
๋ น์ด
์๋ฒ๊ฐ ํ๋ ๋ฐ ๊ฒ์. Metro ์๋ฒ. ์ฌ๊ธฐ์ ์๋ฒ๊ฐ ์ ๋จ๊ณ No device ๋ฑ์ ์๋ฌ ๋ฉ์์ง๊ฐ ๋ฌ๋ค๋ฉด ์๋๋ก์ด๋ ์๋ฎฌ๋ ์ดํฐ ์คํํ ์ฑ๋ก ๋ค์ ๋ช ๋ น์ด ์ ๋ ฅํ ๊ฒ. Metro ์๋ฒ์์ ์์ค ์ฝ๋๋ฅผ ์ปดํ์ผํ๊ณ ์ฑ์ผ๋ก ์ ์กํด์ค. ๊ธฐ๋ณธ 8081ํฌํธ. ๋ฉํธ๋ก ์๋ฒ๊ฐ ๊บผ์ ธ์๋ค๋ฉด ํฐ๋ฏธ๋์ ํ๋ ๋ ์ด์ด
npm start
๊ฐ๋ฐ์ iOS ๊ธฐ์ค์ผ๋ก ํ๋ ๊ฒ ์ข๋ค(๊ฐ์ธ ๊ฒฝํ). ๊ทธ๋ฌ๋ ๊ฐ์ข๋ ์ด์ฉ ์ ์์ด Windows๋ก ํ๋ค.
[email protected] ๋ฒ์ , ํ ๋ฌ์ 0.1์ฉ ์ฌ๋ผ๊ฐ๋๋ฐ ์์ฆ ๊ฐ๋ฐ ์๋๊ฐ ๋๋ ค์ ธ์ ๊ท์น์ด ๊นจ์ง. ๊ฑฐ์ ์์ฑ ๋จ๊ณ๋ผ ์ ๊ท ๊ธฐ๋ฅ์ npm์์ @react-native-community๋ก๋ถํฐ ๋ฐ์์ผ ํจ. ๋ฒ์ ์ ๊ทธ๋ ์ด๋ ํจ๋ถ๋ก ํ์ง ๋ง ๊ฒ!
[๋งฅ ์ ์ฉ]npx pod-install๋ ๋ฏธ๋ฆฌ ํ ๋ฒ, iOS ๋ผ์ด๋ธ๋ฌ๋ฆฌ(pod) ๋ฐ๋ ์ฉ๋
ํด๋ ๊ตฌ์กฐ
- android: ์๋๋ก์ด๋ ๋ค์ดํฐ๋ธ ํด๋
- ios: ios ๋ค์ดํฐ๋ธ ํด๋
- node_modules: ๋ ธ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- app.json: name์ ์ฑ ์ปดํฌ๋ํธ ์ด๋ฆ์ด๋ ํจ๋ถ๋ก ๋ฐ๊พธ๋ฉด ์ ๋จ, ์ด๊ฑฐ ๋ฐ๊พธ๋ฉด ๋ค์ดํฐ๋ธ ์ปดํฌ๋ํธ ์ด๋ฆ๋ ๋ค ๋ฐ๊ฟ์ผํจ, displayName์ ์ฑ ์ด๋ฆ ๋ณ๊ฒฝ์ฉ
- ios/FoodDeliveryApp/AppDelegate.m ์ moduleName
- android/app/src/main/java/com/fooddeliveryapp/MainActivity.java ์ getMainComponentName
- babel.config.js: ๋ฐ๋ฒจ ์ค์
- index.js: ๋ฉ์ธ ํ์ผ
- App.tsx: ๊ธฐ๋ณธ App ์ปดํฌ๋ํธ
- metro.config.js: ๋ฉํธ๋ก ์ค์ ํ์ผ(์นํฉ ๋์ ์ฌ์ฉ)
- tsconfig.json: ํ์ ์คํฌ๋ฆฝํธ ์ค์
- android/app/src/main/java/com/fooddeliveryapp/MainActivity.java: ์๋๋ก์ด๋ ์กํฐ๋นํฐ์์ js์์ง ํตํด ๋ฆฌ์กํธ ์ฝ๋ ์คํ + bridge๋ก ์ํต
์ฑ ์คํ ํ
- cmd + R๋ก ๋ฆฌ๋ก๋ฉ
- cmd + D๋ก ๋๋ฒ๊ทธ ๋ฉ๋ด
- Debugging with Chrome์ผ๋ก ๊ฐ๋ฐ์ ๋๊ตฌ ์ฌ์ฉ ๊ฐ๋ฅ
- Configure Bundler๋ก ๋ฉํธ๋ก ์๋ฒ ํฌํธ ๋ณ๊ฒฝ ๊ฐ๋ฅ
- Show Perf Monitor๋ก ํ๋ ์ ์ธก์ ๊ฐ๋ฅ
Flipper ํ์ด์ค๋ถ์ด ๋ง๋ ๋ชจ๋ฐ์ผ์ฑ ๋๋ฒ๊ฑฐ๋ ์ข์(๋ค๋ง ์ฐ๊ฒฐ ์ ์๋ฌ๋๋ ์ฌ๋ ๋ค์ ๋ฐ๊ฒฌ)
- [ios]์ค์น ์ ํ๊ฒฝ์ค์ -> ๊ฐ์ธ์ ๋ณด ๋ฐ ๋ณด์ ๋ฉ๋ด์์ Flipper๋ฅผ ํ์ฉํด์ฃผ์ด์ผ ํจ
- troubleshoot -> setup doctor ๋ฌธ์ ํด๊ฒฐํ ๊ฒ
npm i react-native-flipper redux-flipper rn-flipper-async-storage-advanced @react-native-async-storage/async-storage
npx pod-install # ์์ดํฐ ์ ์ฉ
- flipper-plugin-async-storage-advanced
- flipper-plugin-redux-debugger
- Layout, Network, Images, Database(sqlite), React Devtools, Hermes Debugger ์ฌ์ฉ ๊ฐ๋ฅ
์ฑ ์ด๋ฆ ๋ณ๊ฒฝ
\android\app\src\main\res\values\strings.xml
app.json์ displayName
\ios\FoodDeliveryApp\Info.plist์ CFBundleDisplayName
๋จ! 0.68๋ฒ์ ๋ถํฐ๋ app.json, strings.xml, CFBundleDisplayName์ ํ๊ธ๋กํ๋ฉด ํ๊ธฐ๋ ๋ฌธ์ ๋ฐ์. ๊ทธ๋ด๋๋ ์ ๋ถ ์์ด๋ก ๋๋๋ฆฌ๊ณ ios์์๋ ๋งํฌ ๋ฐ๋ผ์ ๋ค๊ตญ์ด ์ค์ ์ผ๋ก ํ๊ตญ์ด ์ค์ ํ ๊ฒ. ๋ํ ์๋๋ก์ด๋์์๋ \android\app\src\main\res\values\strings.xml์ ์์ด๋ก ๋๊ณ \android\app\src\main\res\values-ko\strings.xml ์ ์๋ก ๋ง๋ค์ด ์ฌ๊ธฐ์ ํ๊ธ๋ก ๋ณ๊ฒฝํ ๊ฒ
android/gradle.properties
FLIPPER_VERSION=0.145.0
ํ๋ฆฌํผ ๋ฒ์ ์ด 0.145.0๋ณด๋ค ๋ฎ๋ค๋ฉด 0.145.0์ผ๋ก ๋์ผ ๊ฒ. RN 0.72๋ฒ์ ์์๋ ์ด๋ฏธ 0.182.0์
๋ฆฌ์กํธ ๋ค์ดํฐ๋ธ ํด๋ ๊ตฌ์กฐ
- src ํด๋ ์์ฑ(์ง๊ธ ๋ฐ๋ก ์์ฑ ์ ํ๊ณ ํด๋ ์์ ํ์ผ์ด ๋ค ๋ ์์ฑํด๋ ๋จ)
- src/assets: ์ด๋ฏธ์ง, ํฐํธ ๋ฑ
- src/constants: ์์
- src/pages: ํ์ด์ง ๋จ์ ์ปดํฌ๋ํธ
- src/components: ๊ธฐํ ์ปดํฌ๋ํธ
- src/contexts: context api ๋ชจ์
- src/hooks: ์ปค์คํ ํ ๋ชจ์
- src/modules: ๋ค์ดํฐ๋ธ ๋ชจ๋
- src/store: ๋ฆฌ๋์ค ์คํ ์ด ์ธํ
- src/slices: ๋ฆฌ๋์ค ์ฌ๋ผ์ด์ค
- types: ํ์ ์ ์
์ฝ๋ฉ ์์!
App.tsx ๋ถ์
- View๊ฐ div, Text๊ฐ span์ด๋ผ๊ณ ์๊ฐํ๊ธฐ(1๋1 ๋งค์นญ์ ์๋)
- css๋ dp ๋จ์(density-independent pixels, ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์ ์ํฅ๋ฐ์ง ์์)
- css ์์ฑ ๋ฆฌ์คํธ: ์ข ์ค๋๋จ
- flex์์๋ flexDirection์ด Column์ด default
React Navigation
react-router-native๋ ๋์์(์น์์ ๋์ด์จ ๊ฐ๋ฐ์๋ค์๊ฒ ์น์, ์น์ฒ๋ผ ์ฃผ์ ๊ธฐ๋ฐ)
npm i @react-navigation/native
npm i @react-navigation/native-stack
npm i react-native-screens react-native-safe-area-context
npx pod-install # ๋งฅ ์ ์ฉ
android/app/src/main/java/FoodDeliveryApp/MainActivity.java
import android.os.Bundle;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
android/build.gradle
buildscript {
ext {
...
kotlin_version = '1.6.10'
}
...
dependencies {
...
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
...
}
App.tsx ๊ต์ฒด
import * as React from 'react';
import {NavigationContainer, ParamListBase} from '@react-navigation/native';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import {Text, TouchableHighlight, View} from 'react-native';
import {useCallback} from 'react';
type RootStackParamList = {
Home: undefined;
Details: undefined;
};
type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
type DetailsScreenProps = NativeStackScreenProps<ParamListBase, 'Details'>;
function HomeScreen({navigation}: HomeScreenProps) {
const onClick = useCallback(() => {
navigation.navigate('Details');
}, [navigation]);
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<TouchableHighlight onPress={onClick}>
<Text>Home Screen</Text>
</TouchableHighlight>
</View>
);
}
function DetailsScreen({navigation}: DetailsScreenProps) {
const onClick = useCallback(() => {
navigation.navigate('Home');
}, [navigation]);
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<TouchableHighlight onPress={onClick}>
<Text>Details Screen</Text>
</TouchableHighlight>
</View>
);
}
const Stack = createNativeStackNavigator<RootStackParamList>();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{title: 'Overview'}}
/>
<Stack.Screen name="Details">
{props => <DetailsScreen {...props} />}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
- safe-area๊ฐ ์ ์ฉ๋์ด ์์(์ค๋ช )
- NavigationContainer: ๋ด๋น๊ฒ์ด์ ์ํ ์ ์ฅ
- Navigator ์์ Screen๋ค ๋ฐฐ์น
- Screen name ๋์๋ฌธ์ ์๊ด ์์, component๋ ๋ณดํต ๋ ๊ฐ์ง ๋ฐฉ์ ์ฌ์ฉ(์ปดํฌ๋ํธ ๊ทธ ์์ฒด vs Render Callback)
- props๋ก navigation๊ณผ route๊ฐ ์ ๋ฌ๋จ
- Pressable, Button, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, TouchableNativeFeedback
- navigation.navigate๋ก ์ด๋ ๊ฐ๋ฅ
- navigation.push๋ก ์๊ธฐ ๊ฐ๋ฅ
- navigation.goBack์ผ๋ก ์ด์ ์ผ๋ก ์ด๋
- params ์ถ๊ฐ ๊ฐ๋ฅ(params์ user๊ฐ์ ๊ฐ์ฒด๋ฅผ ํต์งธ๋ก ๋ฃ์ง ๋ง๊ธฐ, id๋ฅผ ๋ฃ๊ณ user๋ ๊ธ๋ก๋ฒ ์คํ ์ด์ ๋ฃ๊ธฐ)
- Screen options.title: ์ ๋ชฉ
- Screen options์ ํจ์๋ฅผ ๋ฃ์ด route.params๋ก params ์ ๊ทผ ๊ฐ๋ฅ
- navigation.setOptions๋ก ์ต์ ๋ณ๊ฒฝ ๊ฐ๋ฅ
- Navigator screenOptions๋ก ๊ณตํต ์ต์ ์ค์
- Screen options.headerShown๋ก ํค๋ํ์์ฌ๋ถ
- Screen options.headerTitle๋ก ์ปค์คํ ์ปดํฌ๋ํธ
- Screen options.headerRight๋ก ์ฐ์ธก ๋ฒํผ(useLayoutEffect) ์ต์ ๋ชฉ๋ก
์ค์ ๋ผ์ฐํฐ ๋ง๋ค๊ธฐ (ch1)
npm install @react-navigation/bottom-tabs
App.tsx
- Tab.Navigator ๋์
- isLoggedIn ๋ถ๊ธฐ์ฒ๋ฆฌ
- Drawer๊ณผ Tab.Group ์ฌ์ฉ์ฒ ์๊ฐ src/pages/Delivery.tsx
- Navigator๋ nesting ๊ฐ๋ฅ
ํ์๊ฐ์ , ๋ก๊ทธ์ธ ํ๋ฉด ๋ง๋ค๊ธฐ
src/components/DismissKeyBoardView.tsx
import React from 'react';
import {
TouchableWithoutFeedback,
Keyboard,
StyleProp,
ViewStyle,
KeyboardAvoidingView,
Platform,
} from 'react-native';
const DismissKeyboardView: React.FC<{ style: StyleProp<ViewStyle> }> = ({children, ...props}) => (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<KeyboardAvoidingView
{...props}
style={props.style}
behavior={Platform.OS === 'android' ? undefined : 'padding'}>
{children}
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
);
export default DismissKeyboardView;
์ธํ ๋ฐ๊นฅ ํด๋ฆญ ์ ํค๋ณด๋๋ฅผ ๊ฐ๋ฆฌ๊ธฐ ์ํจ
- src/pages/SignIn.tsx
- src/pages/SignUp.tsx
- src/components/DismissKeyboardView.tsx
- TextInput, StyleSheet.compose ์ฌ์ฉ
- DismissKeyboardView ๋ง๋ค๊ธฐ(Keyboard, KeyboardAvoidingView)
- KeyboardAvoidingView๋ ๋ถํธํจ
- react-native-keyboard-aware-scrollview๋ฅผ ๋์์ผ๋ก ์ฌ์ฉ
npm i react-native-keyboard-aware-scrollview
- ํ์ดํ์ด ์์ผ๋ฏ๋ก ์ง์ ํ์ ์ถ๊ฐํด์ผ ํจ
- react-native-keyboard-aware-scroll-view ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ์ ์ด ์์
types/react-native-keyboard-aware-scroll-view
src/components/DismissKeyBoardView.tsx
์๋ฒ ์์ฒญ ๋ณด๋ด๊ธฐ(ch2)
back ์๋ฒ ์คํ ํ์, DB ์์ด๋ ๋๊ฒ๋ ๋ง๋ค์ด๋ . ์๋ฒ ์ฌ์์ ์ ๋ฐ์ดํฐ๋ ๋ ์๊ฐ๋ ์ฃผ์
# ํฐ๋ฏธ๋ ํ๋ ๋ ์ผ์
cd back
npm start
๋ฆฌ๋์ค ์ค์
npm i @reduxjs/toolkit react-redux redux-flipper
src/store/index.ts์ src/store/reducer.ts, src/slices/user.ts ์์ฑ
AppInner.tsx ์์ฑ ๋ฐ isLoggedIn์ redux๋ก ๊ต์ฒด(AppInner ๋ถ๋ฆฌ ์ด์ ๋ App.tsx์์ useSelector๋ฅผ ๋ชป ์)
App.tsx
import * as React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {Provider} from 'react-redux';
import store from './src/store';
import AppInner from './AppInner';
function App() {
return (
<Provider store={store}>
<NavigationContainer>
<AppInner />
</NavigationContainer>
</Provider>
);
}
export default App;
ํ์๊ฐ์ , ๋ก๊ทธ์ธ
์ก์ธ์คํ ํฐ/๋ฆฌํ๋ ์ํ ํฐ์ ๋ฐ์์ ๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ ์ฅ
npm install react-native-encrypted-storage
npx pod-install # ios ์ ์ฉ
์๋ฒ ์์ฒญ์ axios ์ฌ์ฉ(์์ฆ ky๋ got์ผ๋ก ๋์ด๊ฐ๋ ์ถ์ธ์ด๋ react-native์ ํธํ ์ฌ๋ถ ๋ถํฌ๋ช )
npm i axios
ํ๊ฒฝ๋ณ์, ํค ๊ฐ์ ์ ์ฅํ config ํจํค์ง
npm i react-native-config
import Config from 'react-native-config';
-Android์์ Config๊ฐ ์ ์ฉ์ด ์ ๋๋ฉด ๋ค์ ์ถ๊ฐํด์ผํจ
android/app/proguard-rules.pro
-keep class com.fooddeliveryapp.BuildConfig { *; }
android/app/build.gradle
apply plugin: "com.android.application"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
...
defaultConfig {
...
resValue "string", "build_config_package", "com.fooddeliveryapp"
}
- .env์ ํค=๊ฐ ์ ์ฅํด์(์๋ฅผ ๋ค์ด abc=def) Config.abc๋ก ๊บผ๋ด ์ .env
API_URL=http://10.0.2.2:3105
- ์๋๋ก์ด๋ ์์ดํผ๋ 10.0.2.2๋ก ํด์ผ ํจ(localhost๋ก ํ๋ฉด ์๋๋ก์ด๋์์ ์ ๋จ)
- 10.0.2.2๊ฐ ์ ๋๋ฉด ๋ค์ด๋ฒ์ ๋ด ์์ดํผ ์ณ์ ์ธ๋ถIP๋ ์ ๋ ฅํด๋ณด๊ณ , ipconfig ํฐ๋ฏธ๋์ ์ ๋ ฅํ ๋ ๋์ค๋ ๋ด๋ถIP๋ ์ ๋ ฅํด์ ๋๋ ๊ฒ ์ฐพ๊ธฐ
- ์๋ฎฌ๋ ์ดํฐ/์๋ฎฌ๋ ์ดํฐ/์ค์ ๊ธฐ๊ธฐ์์ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ผ์ ์์ดํผ:3105 ์ ๋ ฅํ์ ๋ ํ์ด์ง๊ฐ ์ ๋๋ก ๋จ๋ IP๊ฐ ์ค์ ๋ก ์๋ํ๋ IP
- [ios]์์๋ 127.0.0.1 IP๋ก ์ฐ๊ธฐ
- [ios]์์ ์ ๋ ๋๋ Podfile์ pod 'react-native-config', :path => '../node_modules/react-native-config/react-native-config.podspec' ์ถ๊ฐํด๋ณด๊ธฐ
์ํธํํด์ ์ ์ฅํ ๋ฐ์ดํฐ๋ ๋ค์ ํจํค์ง์
import EncryptedStorage from 'react-native-encrypted-storage';
await EncryptedStorage.setItem('ํค', '๊ฐ');
await EncryptedStorage.removeItem('ํค');
const ๊ฐ = await EncryptedStorage.getItem('ํค');
- redux์ ๋ฃ์ ๋ฐ์ดํฐ๋ ์ฑ์ ๋๋ฉด ๋ ์๊ฐ
- ์ฑ์ ๊บผ๋ ์ ์ฅ๋์ด์ผ ํ๊ณ ๋ฏผ๊ฐํ ๊ฐ์ encrypted-storage์
- ๊ฐ๋ฐ ํ๊ฒฝ๋ณ๋ก ๋ฌ๋ผ์ง๋ ๊ฐ์ react-native-config์ ์ ์ฅํ๋ฉด ์ข์(์ํธํ ์ ๋จ)
- ๊ทธ ์ธ์ ์ ์ง๋ง ๋๋ฉด ๋ฐ์ดํฐ๋ค์ async-storage์ ์ ์ฅ(npm install @react-native-async-storage/async-storage)
src/pages/SignUp.tsx, src/pages/SignIn.tsx
android์์ http ์์ฒญ์ด ์ ๋ณด๋ด์ง๋ฉด
- android/app/src/main/AndroidManifest.xml ์์ ํ๊ทธ์ android:usesCleartextTraffic="true" ์ถ๊ฐ
ActivityIndicator๋ก ๋ก๋ฉ์ฐฝ ๊พธ๋ฏธ๊ธฐ
์์ผIO ์ฐ๊ฒฐ
์น์์ผ ๊ธฐ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์์ฒญ-์๋ต ๋ฐฉ์์ด ์๋๋ผ ์ค์๊ฐ ์๋ฐฉํฅ ํต์ ๊ฐ๋ฅ
npm i socket.io-client
src/hooks/useSocket.ts
import {useCallback} from 'react';
import {io, Socket} from 'socket.io-client';
import Config from 'react-native-config';
let socket: Socket | undefined;
const useSocket = (): [Socket | undefined, () => void] => {
const disconnect = useCallback(() => {
if (socket) {
socket.disconnect();
socket = undefined;
}
}, []);
if (!socket) {
socket = io(`${Config.API_URL}`, {
transports: ['websocket'],
});
}
return [socket, disconnect];
};
export default useSocket;
AppInner.tsx
const [socket, disconnect] = useSocket();
useEffect(() => {
const helloCallback = (data: any) => {
console.log(data);
};
if (socket && isLoggedIn) {
console.log(socket);
socket.emit('login', 'hello');
socket.on('hello', helloCallback);
}
return () => {
if (socket) {
socket.off('hello', helloCallback);
}
};
}, [isLoggedIn, socket]);
useEffect(() => {
if (!isLoggedIn) {
console.log('!isLoggedIn', !isLoggedIn);
disconnect();
}
}, [isLoggedIn, disconnect]);
- login์ emitํ๋ฉด ๊ทธ๋๋ถํฐ ์๋ฒ๊ฐ hello๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด์ค *๋ก๊ทธ์์ ์์ disconnectํด์ฃผ๋ ๊ฒ ์์ง ๋ง๊ธฐ
๋ก๊ทธ์์
src/pages/Settings.tsx
์ค์ ์ฃผ๋ฌธ ๋ฐ๊ธฐ[ch3]
socket.io์์ ์ฃผ๋ฌธ ๋ด์ญ ๋ฐ์์ store์ ๋ฃ๊ธฐ
AppInner.tsx
useEffect(() => {
const callback = (data: any) => {
console.log(data);
dispatch(orderSlice.actions.addOrder(data));
};
if (socket && isLoggedIn) {
socket.emit('acceptOrder', 'hello');
socket.on('order', callback);
}
return () => {
if (socket) {
socket.off('order', callback);
}
};
}, [isLoggedIn, socket]);
์ฑ ๋ค์ ์ผค ๋ ์๋๋ก๊ทธ์ธ๋๊ฒ
encrypted-storage์์ ํ ํฐ ๋ถ๋ฌ์ค๊ธฐ
AppInner.tsx
// ์ฑ ์คํ ์ ํ ํฐ ์์ผ๋ฉด ๋ก๊ทธ์ธํ๋ ์ฝ๋
useEffect(() => {
const getTokenAndRefresh = async () => {
try {
const token = await EncryptedStorage.getItem('refreshToken');
if (!token) {
return;
}
const response = await axios.post(
`${Config.API_URL}/refreshToken`,
{},
{
headers: {
authorization: `Bearer ${token}`,
},
},
);
dispatch(
userSlice.actions.setUser({
name: response.data.data.name,
email: response.data.data.email,
accessToken: response.data.data.accessToken,
}),
);
} catch (error) {
console.error(error);
if ((error as AxiosError).response?.data.code === 'expired') {
Alert.alert('์๋ฆผ', '๋ค์ ๋ก๊ทธ์ธ ํด์ฃผ์ธ์.');
}
}
};
getTokenAndRefresh();
}, [dispatch]);
- ์ ๊น ๋ก๊ทธ์ธ ํ๋ฉด์ด ๋ณด์ด๋ ๊ฒ์ SplashScreen์ผ๋ก ์จ๊น
์ฃผ๋ฌธ ๋ฐ์ดํฐ ๋ฆฌ๋์ค์ ์ ์ฅํ๊ธฐ
src/slices/order.ts
์์ต๊ธ ํ์ธํ๊ธฐ
src/pages/Settings.tsx
์ฃผ๋ฌธ ํ๋ฉด ๋ง๋ค๊ธฐ(์๋ฝ/๊ฑฐ์ )
src/pages/Orders.tsx
- ScrollView + map ์กฐํฉ์ ์ข์ง ์์
- FlatList๋ฅผ ์ฐ๊ธฐ
- ๋ฐ๋ณต๋๋ ๊ฒ์ ์ปดํฌ๋ํธ๋ก ๋นผ๋ ๊ฒ์ด ์ข์
- keyExtractor ๋ฐ๋์ ์ค์ ํ๊ธฐ
src/components/EachOrder.tsx
accessToken ๋ง๋ฃ์ ์๋์ผ๋ก refresh๋๊ฒ
axios.interceptor ์ค์ ํ๊ธฐ
useEffect(() => {
axios.interceptors.response.use(
response => {
return response;
},
async error => {
const {
config,
response: {status},
} = error;
if (status === 419) {
if (error.response.data.code === 'expired') {
const originalRequest = config;
const refreshToken = await EncryptedStorage.getItem('refreshToken');
// token refresh ์์ฒญ
const {data} = await axios.post(
`${Config.API_URL}/refreshToken`, // token refresh api
{},
{headers: {authorization: `Bearer ${refreshToken}`}},
);
// ์๋ก์ด ํ ํฐ ์ ์ฅ
dispatch(userSlice.actions.setAccessToken(data.data.accessToken));
originalRequest.headers.authorization = `Bearer ${data.data.accessToken}`;
// 419๋ก ์์ฒญ ์คํจํ๋ ์์ฒญ ์๋ก์ด ํ ํฐ์ผ๋ก ์ฌ์์ฒญ
return axios(originalRequest);
}
}
return Promise.reject(error);
},
);
}, [dispatch]);
๋ค์ด๋ฒ ์ง๋ ์ฌ์ฉํ๊ธฐ[ch4]
npm i https://github.com/ZeroCho/react-native-naver-map
npm i react-native-nmap์ ํ๋ฉด ์ ์ง๋ณด์๊ฐ ์ ๋๋ ํจํค์ง๊ฐ ์ค์น๋๋ฏ๋ก ๊ฐ์๋ฅผ ์ํด ์ ์๋ ํจํค์ง๋ฅผ ๋์ ์ค์น
- [ios]git-lfs๋ก ์ถ๊ฐ ์ค์น ํ์ ์ฐธ๊ณ
- [ios] Xcode ๋น๋ํ๋ ๋ฒ, Rosetta๋ก ๋๋ฆฌ๊ธฐ
- [ios]์ค์ ๊ธฐ๊ธฐ์์ ๋ค์ด๋ฒ ์ง๋ ํ๋ ๋ฒ ๋งํฌ Podfile
...
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
...
pod 'NMapsMap'
...
target 'FoodDeliveryAppTests' do
...
npx pod-install # ios ์ ์ฉ
android/build.gradle
buildscript {
...
}
allprojects {
repositories {
google()
jcenter()
// ๋ค์ด๋ฒ ์ง๋ ์ ์ฅ์
maven {
url 'https://naver.jfrog.io/artifactory/maven/'
}
}
}
- ์๋๋ก์ด๋ ์ฑ ํจํค์ง ์ด๋ฆ: com.[์ํ๋์ด๋ฆ].fooddeliveryapp (ex: com.zerocho.fooddeliveryapp)
- ์ปค๋ฐ ์ฐธ์กฐ (ํด๋ ๋ฑ ๋ณ๊ฒฝํ ๊ฒ ๋ง์)
- [ios]Xcode๋ก๋ xcworkspace ํ์ผ์ ์ด์ด์ผํจ(xcodeproj ์ด๋ฉด ์๋จ, xcworkspace๊ฐ ์๋ค๋ฉด ios ํด๋์์ pod install ํ ๋ฒ ์ ๋ ฅํด๋ณผ ๊ฒ)
- [ios]iOS Bundle ID: com.[์ํ๋์ด๋ฆ].fooddeliveryapp(ex: com.zerocho.fooddeliveryapp)๋ก ์์
src/components/EachOrder.tsx
<View
style={{
width: Dimensions.get('window').width - 30,
height: 200,
marginTop: 10,
}}>
<NaverMapView
style={{width: '100%', height: '100%'}}
zoomControl={false}
center={{
zoom: 10,
tilt: 50,
latitude: (start.latitude + end.latitude) / 2,
longitude: (start.longitude + end.longitude) / 2,
}}>
<Marker
coordinate={{
latitude: start.latitude,
longitude: start.longitude,
}}
pinColor="blue"
/>
<Path
coordinates={[
{
latitude: start.latitude,
longitude: start.longitude,
},
{latitude: end.latitude, longitude: end.longitude},
]}
/>
<Marker
coordinate={{latitude: end.latitude, longitude: end.longitude}}
/>
</NaverMapView>
</View>
์์น ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
๊ถํ ์ป๊ธฐ(์์น์ ๋ณด, ์นด๋ฉ๋ผ, ๊ฐค๋ฌ๋ฆฌ)
npm i react-native-permissions
ios/Podfile
pod 'NMapsMap', '3.16.0'
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
pod 'Permission-LocationAccuracy', :path => "#{permissions_path}/LocationAccuracy"
pod 'Permission-LocationAlways', :path => "#{permissions_path}/LocationAlways"
pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse"
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications"
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
ios/FoodDeliveryApp/Info.plist
<key>NSCameraUsageDescription</key>
<string>๋ฐฐ์ก์๋ฃ ์ฌ์ง ์ดฌ์์ ์ํด ์นด๋ฉ๋ผ ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>๋ฐฐ์ก์ค ์์น ํ์ธ์ ์ํด์ ์์น ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>๋ฐฐ์ก์ค ์์น ํ์ธ์ ์ํด์ ์์น ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>๋ฐฐ์ก์ค ์์น ํ์ธ์ ์ํด์ ์์น ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSMotionUsageDescription</key>
<string>๋ฐฐ์ก์ค ์์น ํ์ธ์ ์ํด์ ์์น ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>๋ฐฐ์ก์๋ฃ ์ฌ์ง ์ ํ์ ์ํด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ทผ ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>๋ฐฐ์ก์๋ฃ ์ฌ์ง ์ ํ์ ์ํด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ทผ ๊ถํ์ด ํ์ํฉ๋๋ค.</string>
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
npx pod-install
- ํ๋ก์ฐ๋ฅผ ์ ๋ณผ ๊ฒ
src/hooks/usePermissions.ts
import {useEffect} from 'react';
import {Alert, Linking, Platform} from 'react-native';
import {check, PERMISSIONS, request, RESULTS} from 'react-native-permissions';
function usePermissions() {
// ๊ถํ ๊ด๋ จ
useEffect(() => {
if (Platform.OS === 'android') {
check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION)
.then(result => {
console.log('check location', result);
if (result === RESULTS.BLOCKED || result === RESULTS.DENIED) {
Alert.alert(
'์ด ์ฑ์ ์์น ๊ถํ ํ์ฉ์ด ํ์ํฉ๋๋ค.',
'์ฑ ์ค์ ํ๋ฉด์ ์ด์ด์ ํญ์ ํ์ฉ์ผ๋ก ๋ฐ๊ฟ์ฃผ์ธ์.',
[
{
text: '๋ค',
onPress: () => Linking.openSettings(),
},
{
text: '์๋์ค',
onPress: () => console.log('No Pressed'),
style: 'cancel',
},
],
);
}
})
.catch(console.error);
} else if (Platform.OS === 'ios') {
check(PERMISSIONS.IOS.LOCATION_ALWAYS)
.then(result => {
if (result === RESULTS.BLOCKED || result === RESULTS.DENIED) {
Alert.alert(
'์ด ์ฑ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์น ๊ถํ ํ์ฉ์ด ํ์ํฉ๋๋ค.',
'์ฑ ์ค์ ํ๋ฉด์ ์ด์ด์ ํญ์ ํ์ฉ์ผ๋ก ๋ฐ๊ฟ์ฃผ์ธ์.',
[
{
text: '๋ค',
onPress: () => Linking.openSettings(),
},
{
text: '์๋์ค',
onPress: () => console.log('No Pressed'),
style: 'cancel',
},
],
);
}
})
.catch(console.error);
}
if (Platform.OS === 'android') {
check(PERMISSIONS.ANDROID.CAMERA)
.then(result => {
if (result === RESULTS.DENIED || result === RESULTS.GRANTED) {
return request(PERMISSIONS.ANDROID.CAMERA);
} else {
console.log(result);
throw new Error('์นด๋ฉ๋ผ ์ง์ ์ ํจ');
}
})
.catch(console.error);
} else {
check(PERMISSIONS.IOS.CAMERA)
.then(result => {
if (
result === RESULTS.DENIED ||
result === RESULTS.LIMITED ||
result === RESULTS.GRANTED
) {
return request(PERMISSIONS.IOS.CAMERA);
} else {
console.log(result);
throw new Error('์นด๋ฉ๋ผ ์ง์ ์ ํจ');
}
})
.catch(console.error);
}
}, []);
}
export default usePermissions;
- [ios]ํน์๋ ์ฑ ์ค์ ํ๋ฉด์ ์์น ๊ถํ์ด ์์ ๊ฒฝ์ฐ Delivey ํ์ด์ง๊น์ง ํ ๋ฒ ๋ค์ด๊ฐ๋ค ๋์ค๊ธฐ. ๊ทธ๋ผ ์๊ฒจ์์.
- Platform์ผ๋ก ์ด์์ฒด์ ๊ตฌ๋ณ
- Linking์ผ๋ก ๋ค๋ฅธ ์๋น์ค ์ด๊ธฐ ๊ฐ๋ฅ ์์น ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
npm i @react-native-community/geolocation
src/pages/Ing.tsx
์ด๋ฏธ์ง ์ ํํ๊ธฐ(์ฃผ๋ฌธ ์๋ฃ)
src/pages/Complete.tsx
์ด๋ฏธ์ง ์ ํ ํ ๋ฆฌ์ฌ์ด์ง
npm i react-native-image-crop-picker
npm i react-native-image-resizer
npx pod-install # ios ์ ์ฉ
- ์ด๋ฏธ์ง ์ ๋ก๋์๋ multipart/form-data๋ฅผ ์ฌ์ฉํจ
- ์ด๋ฏธ์ง๋ { uri: ์ฃผ์, name: ํ์ผ๋ช , type: ํ์ฅ์ } ๊ผด
- base64๋ก ์ด๋ฏธ์ง๋ฅผ ํ ์คํธ๊ผด๋ก ํํ ๊ฐ๋ฅ(์ฉ๋ 33% ์ฆ๊ฐ)
- resizeMode: cover(๊ฝ ์ฐจ๊ฒ), contain(๋ฑ ๋ง๊ฒ), stretch(๋น์จ ๋ฌด์ํ๊ณ ๋ฑ ๋ง๊ฒ), repeat(๋ฐ๋ณต๋๊ฒ), center(์ค์ ์ ๋ ฌ)
์ฌ์ง ์ฐ์ ๋ ์ด๋ฏธ์ง๋ฅผ ์นด๋ฉ๋ผ๋กค/๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅํ๊ณ ์ถ์[ch5]
Native Module Patching
npm i patch-package
package.json
"scripts": {
"postinstall": "patch-package",
"android": "react-native run-android",
- patch ํ ์ ์ฉํ๊ธฐ
npx patch-package react-native-image-crop-picker
- ์์ผ๋ก npm i ํ ๋๋ง๋ค ์๋์ผ๋ก ํจ์น๊ฐ ์ ์ฉ๋จ(postinstall ์คํฌ๋ฆฝํธ ๋๋ถ)
- ์ด๋ฐ ๊ฒ ๋๋ฌธ์ ๋ค์ดํฐ๋ธ๋ฅผ ์์์ผํจ ใ
Tmap ์ฐ๊ฒฐํ๊ธฐ(Native Modules)
- My Project - ํ๋ก์ ํธ ์์ฑ - TMap API ์ ์ฒญ(๋ฌด๋ฃ)
- sdk
- ์๋๋ก์ด๋ ์ฐ๋
- [ios]iOS ์ฐ๋
- [ios]iOS ์ฐ๋์ Header ํ์ผ๋ค์ด project.pbxproj์ ๋ฑ๋ก๋์๋ ํ์ธ(๋ค๋ฅธ ๊ฒ๋ ๋น์ฐํ)
- android/app/src/java/com/zerocho/fooddeliveryapp/TMapModule.java ์์ฑ
- android/app/src/java/com/zerocho/fooddeliveryapp/TMapPackage.java ์์ฑ
- android/app/src/java/com/zerocho/fooddeliveryapp/MainApplication์ TMapPackage ์ฐ๊ฒฐ
- [ios]ios/FoodDeliveryApp/RCTTMap.h
- [ios]ios/FoodDeliveryApp/RCTTMap.m
- [ios]ios/FoodDeliveryApp-Bridging-Header.h
- src/modules/TMap.ts
android/app/src/main/AndroidManifest.xml
...
<queries>
<package android:name="com.skt.tmap.ku" />
</queries>
</manifest>
src/pages/Ing.tsx
TMap.openNavi(
'๋์ฐฉ์ง',
end.longitude.toString(),
end.latitude.toString(),
'MOTORCYCLE',
).then(data => {
console.log('TMap callback', data);
if (!data) {
Alert.alert('์๋ฆผ', 'ํฐ๋งต์ ์ค์นํ์ธ์.');
}
});
react-native-splash-screen
npm i react-native-splash-screen
- ์ฌ๊ธฐ์ Third step๊ณผ Getting Started ๋ฐ๋ผํ๊ธฐ
- android/app/src/main/res/drawable ํด๋ ๋ง๋ค๊ณ ๊ทธ ์์ launch_screen.png ๋ฃ๊ธฐ AppInner.tsx
...
const token = await EncryptedStorage.getItem('refreshToken');
if (!token) {
SplashScreen.hide();
return;
}
...
} finally {
SplashScreen.hide();
}
};
getTokenAndRefresh();
}, [dispatch]);
์ฑ icon ๋ณ๊ฒฝ
- Android ๋ค์ด๋ฐ์ ํ android/app/src/main ์๋์ ๋ฃ๊ธฐ
- [ios] ๋งํฌ ์์ ๋ค์ด๋ก๋๋ Assets.xcassets๋ฅผ ios/FoodDeliveryApp ๋ด๋ถ์ ๋ฃ๊ธฐ
- [ios]Xcode์์ ์์ด์ฝ ์ฐ๊ฒฐ ํ์
์ฑ ํ๋จ ๋ฉ๋ด ์์ด์ฝ
npm i react-native-vector-icons
npm i -D @types/react-native-vector-icons
- android/app/src/main/assets/fonts์ node_modules/react-native-vector-icons/Fonts ํด๋ ๋ณต์ฌ
- [ios]Xcode์์ New Group์ผ๋ก ๋ฉ๋ด๋ฅผ ์์ฑํ๊ณ Fonts ๊ทธ๋ฃน์ node_modules/react-native-vector-icons/Fonts ํฐํธ๋ค์ ์ถ๊ฐ
์ฃผ๋ฌธ์๋ฃ ์ฌ์ง๋ค ๋ณด์ฌ์ฃผ๊ธฐ
npm i react-native-fast-image
๋งํฌ src/slices/order.ts
interface InitialState {
...
completes: Order[];
}
const initialState: InitialState = {
...
completes: [],
};
...
setCompletes(state, action) {
state.completes = action.payload;
},
src/pages/Settings.tsx
FCM
ํธ์ฌ์๋ฆผ ๋ณด๋ด๊ธฐ
- ๋งํฌ์์ ์ฑ ๋ง๋ค๊ธฐ
npm i @react-native-firebase/analytics @react-native-firebase/app @react-native-firebase/messaging
npm i react-native-push-notification @react-native-community/push-notification-ios
npm i -D @types/react-native-push-notification
npx pod-install
[ios] pod install ์ ์๋ฌ ๋ฐ์ ์ ์ฐธ๊ณ
[ios] ๋ฐ๋ผํ ๊ฒ
- firebase ํ๋ก์ ํธ ์ค์ - Admin SDK - Node.js - ์ ๋น๊ณต๊ฐํค ์์ฑ - back ํด๋ ์์ ๋ฃ๊ณ app.js ์์ค ์์
- ์๋๋ก์ด๋ ์ฑ ์ค์ ํ google-services.json์ android/app์ ๋ฃ๊ธฐ
- [ios] ์์ดํฐ ์ฑ ์ค์ ํ ios/GoogleService-Info.plist ์์ฑ
- ๋ฐฐ์ก ์๋ฃ์ push ์๋ฆผ์ด ์ฌ ๊ฒ์(์๋ฎฌ๋ ์ดํฐ์์๋ ์ ์ฌ ์ ์์)
๋ฆฌ์กํธ ๋ด๋น๊ฒ์ด์ ๊ณผ ์ฐ๋
App.tsx
์ค๊ธฐ๊ธฐ ์ฌ์ฉํ๊ธฐ[ch6]
- samsung dex๊ฐ์ ๊ฑด ๋๊ธฐ
- ํธ๋ํฐ usb ์ฐ๊ฒฐ ์ usb ๋๋ฒ๊น ํ์ฉํ๊ธฐ
- .env์์ ip์ฃผ์ ๋ฐ๊พธ๊ธฐ
adb devices
adb -s <๊ธฐ๊ธฐ์ด๋ฆ> reverse tcp:8081 tcp:8081
์ฌ๋ฌ ๋ฌธ์ ๋ฐ๊ฒฌ ๊ฐ๋ฅ
- ํฐํธ๊ฐ ํฐ์: style์ color ์ฃผ๊ธฐ
- vector-icons ์ ๋ธ: ์ญ์ style์ color ์ฃผ๊ธฐ(ch6 AppInner.tsx ์ฐธ๊ณ )
๋ฐฐํฌ ๊ด๋ จ
Android
android/app/build.gradle
def enableSeparateBuildPerCPUArchitecture = true
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = true
package.json
"scripts": {
...
"build:android": "npm ci && cd android && ./gradlew bundleRelease && cd .. && open android/app/build/outputs/bundle/release",
"apk:android": "npm ci && cd android && ./gradlew assembleRelease && cd .. && open android/app/build/outputs/apk/release",
iOS
iOS ๊ฐ๋ฐ์ ๋ฉค๋ฒ์ฝ ๊ฐ์ ํ์
- Xcode๋ก Archive(์ด ๋ simulator๋ฅผ ์ ํํ ์ํ์ด๋ฉด ์ ๋จ)
[ios]fastlane
๋ฒ์ ๋, ๋ฐฐํฌ ์๋ํ ๊ฐ๋ฅ
CodePush
- ์ค์๊ฐ์ผ๋ก ์ฑ ์์ ๊ฐ๋ฅ(JS์ฝ๋, ์ด๋ฏธ์ง, ๋น๋์ค๋ง)
- ๋ ธ๋๋ชจ๋, ๋ค์ดํฐ๋ธ์ชฝ ์์ ์ ์ฑ ๋ฐฐํฌ ํ์
- ์ฌ๊ธฐ์ ์ฑ ๋ง๋ค๊ธฐ(iOS, Android ๋ฐ๋ก)
npm i react-native-code-push
npm install appcenter appcenter-analytics appcenter-crashes
npm i -g appcenter-cli (๋งฅ์์๋ sudo ํ์)
appcenter login
appcenter codepush deployment list -a zerohch0/food-delivery-app-android -k
- android/app/src/main/assets/appcenter-config.json
- android/app/src/main/res/values/strings.xml ์์
- ์ถ๊ฐ ์์
- [ios] ios/AppCenter-Config.plist
- [ios] ์ถ๊ฐ ์์
App.tsx
import codePush from "react-native-code-push";
const codePushOptions: CodePushOptions = {
checkFrequency: CodePush.CheckFrequency.MANUAL,
// ์ธ์ ์
๋ฐ์ดํธ๋ฅผ ์ฒดํฌํ๊ณ ๋ฐ์ํ ์ง๋ฅผ ์ ํ๋ค.
// ON_APP_RESUME์ Background์์ Foreground๋ก ์ค๋ ๊ฒ์ ์๋ฏธ
// ON_APP_START์ ์ฑ์ด ์คํ๋๋(์ผ์ง๋) ์๊ฐ์ ์๋ฏธ
installMode: CodePush.InstallMode.IMMEDIATE,
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
// ์
๋ฐ์ดํธ๋ฅผ ์ด๋ป๊ฒ ์ค์นํ ๊ฒ์ธ์ง (IMMEDIATE๋ ๊ฐ์ ์ค์น๋ฅผ ์๋ฏธ)
};
function App() {
}
export default codePush(codePushOptions)(App);
"codepush:android": "appcenter codepush release-react -a ์์ด๋/์ฑ์ด๋ฆ -d ๋ฐฐํฌ์ด๋ฆ --sourcemap-output --output-dir ./build -m -t ํ๊ฒ๋ฒ์ ",
"codepush:ios": "appcenter codepush release-react -a ์์ด๋/์ฑ์ด๋ฆ -d ๋ฐฐํฌ์ด๋ฆ --sourcemap-output --output-dir ./build -m -t ํ๊ฒ๋ฒ์ ",
"bundle:android": "react-native bundle --assets-dest build/CodePush --bundle-output build/CodePush/index.android.bundle --dev false --entry-file index.js --platform android --sourcemap-output build/CodePush/index.android.bundle.map",
"bundle:ios": "react-native bundle --assets-dest build/CodePush --bundle-output build/CodePush/main.jsbundle --dev false --entry-file index.js --platform ios --sourcemap-output build/CodePush/main.jsbundle.map",
- ์ค์ ์์๋ package.json ์ฐธ์กฐ
iOS Pod ๊ด๋ จ
[๋งฅ ์ ์ฉ]ios ํด๋ ์์์ pod ๋ช ๋ น์ด ์ํ ๊ฐ๋ฅ, but npx pod-install์ ํ๋ก์ ํธ ํด๋ ์ด๋์๋ ๊ฐ๋ฅ
- Podfile: ์ค์นํ Pod๊ณผ ๊ฐ๋ณ์ค์ ๋ค ๊ธฐ๋ก
- pod deintegrate: ๊ธฐ์กด pod๋ค ์ ๊ฑฐ
- pod update: ๊ธฐ์กด pod ๋ฒ์ ์ ๊ทธ๋ ์ด๋(pod install ์)
- pod install: npx pod-install ์ญํ Podfile.lock์ ๋ฐ๋ผ ์ค์น
- pod install --repo-update: pod๋ค ์ค์นํ๋ฉด์ ์ต์ ์ผ๋ก ์ ์ง
Hermes ์ผ๊ธฐ
์์ ์ฑ๋ฅ ๋นจ๋ผ์ง๊ณ , ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ ๊ณ , ์ฑ ์ฌ์ด์ฆ ์์์ง
๊ฟํ๋ค
- patch-package: ๋ ธ๋๋ชจ๋์ฆ ์ง์ ์์ ๊ฐ๋ฅ, ์ ์ง๋ณด์ ์ ๋๋ ํจํค์ง ์ ๋ฐ์ดํธ ์ ์ ์ฉ, ๋ค๋ง patch-packageํ ํจํค์ง๋ ์ถํ ๋ฒ์ ์ ์ฌ๋ฆฌ๋ ๊ฒ ์ข์
- Sentry: ๋ฐฐํฌ ์ React Native์ฉ์ผ๋ก ๋ถ์ฌ์ ์๋ฌ ๋ชจ๋ํฐ๋งํ๋ฉด ์ข์(๋ฌด๋ฃ ์ง์)
- react-native-upgrade helper: ๋ฒ์ ์ ๊ทธ๋ ์ด๋ ๋ฐฉ๋ฒ ๋์ด
์๋ฌ๋ค
Error: listen EADDRINUSE: address already in use :::8081
์ด๋ฏธ ๋ฉํธ๋ก ์๋ฒ๊ฐ ๋ค๋ฅธ ๋ฐ์ ์ผ์ ธ ์๋ ๊ฒ์. ๋ฉํธ๋ก ์๋ฒ๋ฅผ ์คํํ๊ณ ์๋ ํฐ๋ฏธ๋ ์ข ๋ฃํ๊ธฐ
npm run android ์ Running jetifier to migrate libraries to AndroidX.์ชฝ์์ ์ ๋์ด๊ฐ๋ ๊ฒฝ์ฐ
๋ฉํธ๋ก ์๋ฒ ๊บผ๋ณผ ๊ฒ
์๋ฃ์ฒ๋ฆฌ ์ "์ ํจํ์ง ์์ ์ฃผ๋ฌธ์ ๋๋ค."
[email protected] ์ค์น([email protected]์ ๋ฌธ์ ์์) ๋งํฌ
java.lang.RuntimeException: Unable to load script. Make sure you're either running Metro (run 'npx react-native start') or that your bundle 'index.android.bundle' is packaged correctly for release.
- android/app/src/main/assets ํด๋ ๋ง๋ค๊ธฐ
cd android
./gradlew clean
cd ..
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle
Execution failed for task ':app:packageDebug'. > java.lang.OutOfMemoryError (no error message)
android/gradle.properties์ ๋ค์ ์ค ์ถ๊ฐ
org.gradle.jvmargs=-XX\:MaxHeapSize\=1024m -Xmx1024m
๋๋
android/app/src/main/AndroidManifest.xml ์์ ํ๊ทธ์ android:largeHeap="true" ์ถ๊ฐ
warn No apps connected. Sending "reload" to all React Native apps failed. Make sure your app is running in the simulator or on a phone connected via USB.
npx react-native start --reset-cache
cd android && ./gradlew clean
cd ..
npx react-native run-android
ERR_OSSL_DSO_COULD_NOT_LOAD_THE_SHARED_LIBRARY
์๋์์ ๋ฐ์ํ๋ ์๋ฌ์ธ๋ฐ choco๋ก openssl ๋ค์ ์ค์นํ๊ธฐ
Error: spawn ./gradlew EACCES
chmod 755 android/gradlew
error: bundling failed: TypeError: Cannot read property 'transformFile' of undefined
node.js 16๋ฒ์ ์ผ๋ก ํ ๊ฒ, node 17๋ฒ์ ๋ถํฐ ํด๋น ์๋ฌ ๋ฐ์ํจ.
ERROR Invariant Violation: Module AppRegistry is not a registered callable module (calling runApplication)
๋ณดํต App.tsx ๋ถ๋ถ์ด ์ฌ๋ฌ๋ฒ ์คํ๋์ด์ ๋ฐ์ํจ. Metro ์๋ฒ๋ฅผ ๊ป๋ค๊ฐ ์ผ๊ณ , ์๋ฎฌ๋ ์ดํฐ์์ ์ฑ์ ์ง์ ๋ค๊ฐ ๋ค์ ์ค์นํ๋ฉด ํด๊ฒฐ ๋จ
android:exported
when the corresponding component has an intent filter defined
Manifest merger failed : android:exported needs to be explicitly specified for element <receiver#com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver>. Apps targeting Android 12 and higher are required to specify an explicit value for
์ค์ค๋ก ํด๋ณด๋ฉด ์ข์ ๊ฒ
- loading, disabled ์ฒ๋ฆฌ ๋ชจ๋ ๋ค ํ๊ธฐ
- ๋ด ์์น ์ฑ ์์ํ๊ณ ๊ถํ ์์ ๋ ๋ฏธ๋ฆฌ ๋ฐ์๋๊ธฐ
- refreshtoken์ด ๋ง๋ฃ๋๋ฉด ์ด๋ป๊ฒ?(ํ์ฌ๋ ๋ฌดํ 419๋ธ)