리액트를 사용해 네이티브 앱 만들게 해주는 프레임워크
ios
, android
)View
컴포넌트는 각 플랫폼에 맞게 컴파일됨View
를 제외한 자바스크립드 코드들은 컴파일되지 않고, Bridge를 통해 네이티브 플랫폼과 통신해서 동작함html
, css
를 지원하지 않고 javascript
로 스타일링 구현함.Expo CLI
리액트 네이티브에서 제공하는 React Native CLI
도 있지만, 기본 셋업만 제공하기 때문에 expo
를 많이 사용함.
Expo 동작 방식과 배포 방식
https://expo.io/$ sudo npm install expo-cli --global $ expo init foo-rn-app $ cd foo-rn-app $ npm start
expo 용 로컬 서버 실행 후 expo 툴 사용
expo Client 앱(https://expo.io/tools#client) 사용해서 QR 코드 찍으면 해당 프로젝트의 앱을 모바일 디바이스에서 확인 가능
div
와 유사하게 기본적인 컨테이너 역할style
속성에 스타일링 설정 부여 가능<View style={{backGroundColor:"#32a852"}}>
<View></View>
<Text>{textState}</Text>
<StatusBar style="auto" />
<Button title="Change Text" onPress={onPressChangeText}/>
</View>
<Text>foo</Text>
웹과 마찬가지로 map
함수 사용해 구현할 수 있음
<View>{textList.map(item=><Text key={item.key}>{item.text}</Text>)}</View>
ScrollView
나 FlatList
사용해야함ScrollView
: 화면에 표시되지 않는 내용까지 렌더링되므로 성능상 비효율적임FlatList
: data
, renderItem
, keyExtractor
사용해서 렌더링<FlatList
keyExtractor={(item,index)=>item.key}
data={textList}
renderItem={itemData=>(
<View style={styles.listItem}>
<Text {itemData.item.text}</Text>
</View>)}
/>
Pull to Refresh
아래로 드래그해서 새로고침하기
FlatList
컴포넌트에서onRefresh
,refreshing
속성 제공함 (현재 새로고침 중인지에 대한 정보를 알아야해서 항상 같이 제공되어야함)<FlatList onRefesh={()=>{}} refreshing={bool}> ... </FlatList>
직접 View
에 터치 이벤트 사용하게 될 경우 미세한 터치 감도 조절 어려움
또, onTouchEnd
, onTouchStart
사용하면 직접 timer 구현해서 누르는 정도 조절해야함
Touchable
컴포넌트로 래핑하여 간단하게 적용 가능
TouchableOpacity
도 사용 : high level의 터치 이벤트들(longPress
, activeOpacity
등) 속성 적용 가능함
<TouchableOpacity activeOpacity={0.8} onPress={props.onDelete}>
<View style={styles.listItem}>
<Text>{props.item.text}</Text>
</View>
</TouchableOpacity>
아래와 같이 Modal
컴포넌트로 래핑하고, animationType
설정해 모달 동작 구현 가능함
(slide : 하단에서 올라옴)
<Modal visible={props.visible} animationType={'slide'}>
<TouchableOpacity activeOpacity={0.8} onPress={props.onDelete}>
<View style={styles.listItem}>
<Text>{props.item.text}</Text>
</View>
</TouchableOpacity>
</Modal>
이외의 컴포넌트들은 아래 참고
https://reactnative.dev/docs/components-and-apis
기본적으로 CSS
를 모방해서 만들어서 매우 유사함(javascript
로 구현된 스타일링)
layout
정렬 기준 적용 가능responsive
하게 적용 가능flex
속성 사용 가능함flex
속성값에 따라 비율을 맞춰 공간을 차지하게 됨 <View style={{flex: 1, flexDirection: 'row'}}>
<View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
<View style={{width: 50, height: 50, backgroundColor: 'skyblue'}} />
<View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
</View>
inline style
은 웹에서와 마찬가지로 관리하기 어렵기 때문에 StyleSheet Object
를 사용함.Object
를 사용해 생성하는 이유validation
및 성능개선 효과...
<View style={styles.container}>
<Button style={styles.customButton}>
</Button>
</View>
...
const styles = StyleSheet.create({
container: {
padding: 50,
flex: 1,
},
customButton: {
padding: 50,
flex: 2,
}
});
참고
style
속성에 할당해야하는 값들이 있고, 직접 컴포넌트 속성으로 입력해야하는 값들이 있음View
컴포넌트가 다양한 스타일링 가능해 보통 래핑해서 사용함- 위의 컴포넌트들(
<View>
,<Text>
등)이 네이티브 위젯으로 변환됨(UIView
,widget.view
)- 그림자 처리같은 경우
ios
는shadow
속성,android
는elevation
속성을 적용해줘야함
- 크로스플랫폼 지원을 위해서 별도로 구현해줘야하는 부분들이 이런식으로 어쩔수 없이 존재함
ex ) 네이티브 앱의 터치 반응 효과 :TouchableOpacity
(IOS),TouchableNativeFeedBack
(Android)
써드파티 UI 라이브러리
expo
를 활용하는것이 편리함
https://docs.expo.io/versions/latest/
api reference
참고
ImagePicker api
내장 카메라앱이나 갤러리 실행해 이미지 가져와서 사용 가능
expo-image-picker
패키지에서 가져옴permission
허용해줘야함Permissions
패키지 사용promise
로 사용됨 > async
, await
으로 권한 부여 받고 통과시 사용...
const ImgPicker = props => {
const [pickedImage, setPickedImage] = useState();
const verifyPermissions = async () => {
const result = await Permissions.askAsync(
Permissions.CAMERA_ROLL,
Permissions.CAMERA
);
if (result.status !== 'granted') {
Alert.alert(
'Insufficient permissions!',
'You need to grant camera permissions to use this app.',
[{ text: 'Okay' }]
);
return false;
}
return true;
};
const takeImageHandler = async () => {
const hasPermission = await verifyPermissions();
if (!hasPermission) {
return;
}
const image = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [16, 9],
quality: 0.5
});
setPickedImage(image.uri);
props.onImageTaken(image.uri);
};
return (
<View style={styles.imagePicker}>
<View style={styles.imagePreview}>
{!pickedImage ? (
<Text>No image picked yet.</Text>
) : (
<Image style={styles.image} source={{ uri: pickedImage }} />
)}
</View>
<Button
title="Take Image"
color={Colors.primary}
onPress={takeImageHandler}
/>
</View>
);
};
export default ImgPicker;
expo-location
패키지에서 가져옴Permission
사용해 권한 체크 절차 진행해야함...
const LocationPicker = props => {
const [isFetching, setIsFetching] = useState(false);
const [pickedLocation, setPickedLocation] = useState();
const mapPickedLocation = props.navigation.getParam('pickedLocation');
const { onLocationPicked } = props;
useEffect(() => {
if (mapPickedLocation) {
setPickedLocation(mapPickedLocation);
onLocationPicked(mapPickedLocation);
}
}, [mapPickedLocation, onLocationPicked]);
const verifyPermissions = async () => {
const result = await Permissions.askAsync(Permissions.LOCATION);
if (result.status !== 'granted') {
Alert.alert(
'Insufficient permissions!',
'You need to grant location permissions to use this app.',
[{ text: 'Okay' }]
);
return false;
}
return true;
};
const getLocationHandler = async () => {
const hasPermission = await verifyPermissions();
if (!hasPermission) {
return;
}
try {
setIsFetching(true);
const location = await Location.getCurrentPositionAsync({
timeout: 5000
});
setPickedLocation({
lat: location.coords.latitude,
lng: location.coords.longitude
});
props.onLocationPicked({
lat: location.coords.latitude,
lng: location.coords.longitude
});
} catch (err) {
Alert.alert(
'Could not fetch location!',
'Please try again later or pick a location on the map.',
[{ text: 'Okay' }]
);
}
setIsFetching(false);
};
const pickOnMapHandler = () => {
props.navigation.navigate('Map');
};
return (
<View style={styles.locationPicker}>
<MapPreview
style={styles.mapPreview}
location={pickedLocation}
onPress={pickOnMapHandler}
>
{isFetching ? (
<ActivityIndicator size="large" color={Colors.primary} />
) : (
<Text>No location chosen yet!</Text>
)}
</MapPreview>
<View style={styles.actions}>
<Button
title="Get User Location"
color={Colors.primary}
onPress={getLocationHandler}
/>
<Button
title="Pick on Map"
color={Colors.primary}
onPress={pickOnMapHandler}
/>
</View>
</View>
);
};
export default LocationPicker;
react-native-maps
패키지에서 가져옴location
api를 통해 얻은 정보로 네이티브 지도앱을 사용해 마커를 찍고 해당 좌표 얻는 등의 상호작용 가능함MapView
에 좌표등의 지역정보를 할당해주면 표시 가능Marker
컴포넌트는 MapView
컴포넌트 내부에 선언하여 구현함...
const MapScreen = props => {
const initialLocation = props.navigation.getParam('initialLocation');
const readonly = props.navigation.getParam('readonly');
const [selectedLocation, setSelectedLocation] = useState(initialLocation);
const mapRegion = {
latitude: initialLocation ? initialLocation.lat : 37.78,
longitude: initialLocation ? initialLocation.lng : -122.43,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421
};
const selectLocationHandler = event => {
if (readonly) {
return;
}
setSelectedLocation({
lat: event.nativeEvent.coordinate.latitude,
lng: event.nativeEvent.coordinate.longitude
});
};
const savePickedLocationHandler = useCallback(() => {
if (!selectedLocation) {
// could show an alert!
return;
}
props.navigation.navigate('NewPlace', { pickedLocation: selectedLocation });
}, [selectedLocation]);
useEffect(() => {
props.navigation.setParams({ saveLocation: savePickedLocationHandler });
}, [savePickedLocationHandler]);
let markerCoordinates;
if (selectedLocation) {
markerCoordinates = {
latitude: selectedLocation.lat,
longitude: selectedLocation.lng
};
}
return (
<MapView
style={styles.map}
region={mapRegion}
onPress={selectLocationHandler}
>
{markerCoordinates && (
<Marker title="Picked Location" coordinate={markerCoordinates} />
)}
</MapView>
);
};
MapScreen.navigationOptions = navData => {
const saveFn = navData.navigation.getParam('saveLocation');
const readonly = navData.navigation.getParam('readonly');
if (readonly) {
return {};
}
return {
headerRight: (
<TouchableOpacity style={styles.headerButton} onPress={saveFn}>
<Text style={styles.headerButtonText}>Save</Text>
</TouchableOpacity>
)
};
};
export default MapScreen;
그 이외에도 파일 시스템, 내장 DB, 디바이스 모션, 자이로스코프, 키보드, 바이브레이션, 푸시 알림 등 지원하는 기능들이 꽤 있음.
orientation
: expo
설정에 portrait
, landscape
등 설정 가능
ScreenOrientation api
: expo
에서 제공하는 오리엔테이션 관련 api
KeyboardAvoidingView
: 키보드 노출된 케이스에서 ui 포지션 설정 가능
behavior
, keyboardVerticalOffset
등의 속성 사용KeyBoard api
제공 : 네이티브 키보드와 상호작용 가능
KeyBoard.dissmiss()
Alert api
제공 : 네이티브 얼럿 사용
Alert.alert("foo",[{text:'OK', style:'destructive', onPress: customHandler}])
AppLoading
: expo
에서 제공하는 api로 앱 로딩 화면에서 실행할 로직 적용가능asset
들 불러옴<AppLoading
startAsync={fetchFonts}
onFinish={()=>setDataLoaded(true)}
onError={(err)=>console.log(err)} />
Platform api
: 현재 디바이스 플랫폼 정보를 제공해줌
Platfrom.OS == 'android' && Platform.Version >= 21 ? Colors.primary : 'white'
Platform.select({ios: fooValue, android: fooValue})
Platform-specific
파일로 구분해서 개발도 가능함
CustomButton.android.js
CustomButton.ios.js
import
시에는 postfix
제외하고 /CustomButton
사용SafeAreaView
: 아이폰 노치, 하단의 task manage bar
같은 부분들에 대한 보정 처리를 자동으로 해줌
web
: url
이 source of truth
로 라우팅 처리함app
: tab
, stack
같은 걸 사용해 이동함react-navigation
: react-native
의 네비게이션을 위한 써드파티 라이브러리 (https://reactnavigation.org/)React Native
와는 다르게 웹앱을 네이티브앱으로 래핑하는 개발 방식 사용.
ionic
회사가 만든 프레임워크로 아래와 같이 분리해서 봐야함
Ionic UI Frameworks
: 모바일 UI Look-alike + 웹UI 까지 커버하기 쉽게 도와주는 UI 컴포넌트들을 제공
Ionic Native
: Capcaitor
나 Cordova
플러그인을 활용해 쉽게 디바이스 제어 기능 제공
Capcaitor
: Ionic팀에서 개발한 것으로 Cordova
에서 진화한 버전이라고 보면 됨. 웹앱 빌드를 네이티브 앱으로 래핑해 빌드해주고, 디바이스 제어 가능한 플러그인, 런타임 환경 제공참고
react
로 정식 release된건 2019년 8월로 얼마되지 않음(초기엔 angular
만 가능했음)
html
, css
로 디자인, 마크업 작성 가능Components : https://ionicframework.com/docs/components
import React, {useEffect, useState} from 'react';
import {
IonApp,
IonContent,
IonHeader,
IonToolbar,
IonGrid,
IonCol,
IonRow,
IonItem,
IonLabel,
IonInput,
IonTitle,
IonButton,
IonRippleEffect
} from '@ionic/react';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
/* Theme variables */
import './theme/variables.css';
const App: React.FC = () => {
useEffect(() => {
setTitleState('changed by useEffect');
}, []);
const [titleState, setTitleState] = useState<string>('default title');
const handleChangeTitle = () => {
setTitleState("changed by Button");
};
return (
<IonApp>
<IonHeader>
<IonToolbar>
<IonTitle>{titleState}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol>
<h2 style={{margin:"auto", padding:'auto'}}>{titleState}</h2>
</IonCol>
<IonCol>
<IonButton style={{margin:"auto", padding:'auto'}} mode="ios" onClick={handleChangeTitle}>button</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonApp>
);
}
export default App;
Capacitor
플러그인 사용 권장하지만, 많은 기능에서 Cordova
지원을 필요로함 $ ionic integrations enable capacitor
$ ionic capacitor add android
$ ionic capacitor add ios
ionic-native
에서 플러그인들을 활용한 많은 기능 제공promise
객체를 사용함.예시) 바코드 스캐너
import { BarcodeScanner } from '@ionic-native/barcode-scanner';
const Tab1: React.FC = () => {
const openScanner = async () => {
const data = await BarcodeScanner.scan();
console.log(`Barcode data: ${data.text}`);
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonButton onClick={openScanner}>Scan barcode</IonButton>
</IonContent>
</IonPage>
);
};
제공 기능 목록 : https://ionicframework.com/docs/native
info.plist
파일AndroidManifest.xml
파일@capacitor/core
패키지 모듈의 가져와 api 사용 (혹은 Cordova
)import {CameraResultType, CameraSource, Plugins} from '@capacitor/core'
const {Camera} = Plugins;
const App: React.FC = () => {
const photoHandler = async () => {
const photoResult = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 80,
width: 500,
});
console.log(photoResult);
};
return (
<IonApp>
...
<IonButton style={{margin:"auto", padding:'auto'}} mode="ios" onClick={handleChangeTitle}>button</IonButton>
...
</IonApp>
);
}
export default App;
모바일용 위젯 UI 프레임워크 + SDK(네이티브앱 빌드, 런타임 환경 등)
Dart
사용 (dart, flutter 모두 구글에서 만듬)
네이티브 앱개발자가 크로스플랫폼 지향 개발시 사용하기 쉬운 구조
두 플랫폼의 네이티브 UI 위젯을 사용하지 않고, Skia
엔진을 사용해 직접 렌더링하고 컨트롤함
(실제 네이티브 UI가 아니고 유사 UI라 완전히 스타일이 동일하지 않을 수 있지만 거의 흡사함)
플러터 엔진(C,C++ 사용)에 포함된
Skia
엔진은 안드로이드와 동일한 렌더링 엔진이며,Dart
는AOT
를 지원하기 때문에 성능이 좋음.참고
장점
Skia Engine
사용ios
, android
, windows
, mac
, web
모두 지원함( web
은 아직 불안정)cpu
, gpu
를 헤비하게 사용하는 앱에서는 RN보다 유리함성능 비교
- Flutter vs React Native vs Native: Deep Performance Comparison
- Flutter vs Native vs React-Native: Examining performance
플러터가 렌더링 이외에 연산 성능에 있어서는 실제 네이티브 앱과 거의 동일한 퍼포먼스를 가짐
단점
admob
적용 불편Dart
사용 (아마도 거의 플러터에서만 사용됨)장점
React
웹 개발자들이 진입하기 굉장히 쉬움단점
ios
, android
환경에 맞춰 별도 구현이 필요한 상황이 생기게됨 장점
단점
Flutter
보다 모바일 네이티브와 같은 looking, ux면에서 떨어짐.