React Native (feat. Ionic, Flutter)

lwoody·2021년 1월 16일
0
post-thumbnail

개요

  1. React Native
  2. Ionic 간단 소개
  3. Flutter 간단 소개
  4. React Native, Ionic, Flutter 비교

React Native

리액트를 사용해 네이티브 앱 만들게 해주는 프레임워크

  • 크로스 플랫폼 지원(ios, android)
  • 다수의 리액트 컴포넌트 컬렉션이 있음
  • UI 컴포넌트들은 네이티브 위젯으로 컴파일됨
  • 네이티브 플랫폼 api를 자바스크립트로 사용하기 편리하게 제공

View 구현

  • 리액트 네이티브의 View 컴포넌트는 각 플랫폼에 맞게 컴파일됨
  • View 를 제외한 자바스크립드 코드들은 컴파일되지 않고, Bridge를 통해 네이티브 플랫폼과 통신해서 동작함
  • html, css를 지원하지 않고 javascript로 스타일링 구현함.
    • style name, value들이 camelCase로 css과 유사함.
      (React 컴포넌트내에서 스타일링 적용하는 방식과 유사함)

개발 툴

Expo CLI

  • 개발환경 및 테스트 툴을 제공해주는 오픈소스 플랫폼 (네이티브 기능 동작 테스트 등에 필요)
  • 개발 후 네이티브 앱으로 빌드 및 배포 지원해줌
  • 네이티브 플랫폼 api 사용을 더욱 쉽게 해줌

리액트 네이티브에서 제공하는 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 코드 찍으면 해당 프로젝트의 앱을 모바일 디바이스에서 확인 가능

core components

View

  • div 와 유사하게 기본적인 컨테이너 역할
  • style 속성에 스타일링 설정 부여 가능
  • 기본적인 레이아웃이나, 추가적인 스타일링 적용시 가장 많이 사용함
<View style={{backGroundColor:"#32a852"}}>
      <View></View>
      <Text>{textState}</Text>
      <StatusBar style="auto" />
      <Button title="Change Text" onPress={onPressChangeText}/>
</View>

Text

  • 텍스트값을 적용하고 싶을 경우 wrapping해 사용
    <Text>foo</Text>
  • 웹과 다르게 없으면 에러남

목록 뷰

웹과 마찬가지로 map 함수 사용해 구현할 수 있음

<View>{textList.map(item=><Text key={item.key}>{item.text}</Text>)}</View>

ScrollView, FlatList

  • 웹과 다르게 모바일의 경우 자동으로 스크롤링 제공하지 않기 때문에 ScrollViewFlatList 사용해야함
    • 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>

https://reactnative.dev/docs/flatlist

Touchable, TouchableOpacity

  • 직접 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

Styling

기본적으로 CSS 를 모방해서 만들어서 매우 유사함(javascript로 구현된 스타일링)

Flexbox

  • 컴포넌트들의 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>

StyleSheet Object

  • 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)
  • 그림자 처리같은 경우 iosshadow 속성, androidelevation 속성을 적용해줘야함
    • 크로스플랫폼 지원을 위해서 별도로 구현해줘야하는 부분들이 이런식으로 어쩔수 없이 존재함
      ex ) 네이티브 앱의 터치 반응 효과 : TouchableOpacity(IOS), TouchableNativeFeedBack(Android)

써드파티 UI 라이브러리

디바이스 제어

expo를 활용하는것이 편리함

https://docs.expo.io/versions/latest/
api reference 참고

Camera

ImagePicker api

내장 카메라앱이나 갤러리 실행해 이미지 가져와서 사용 가능

  • expo-image-picker 패키지에서 가져옴
  • ios의 경우 이미지 피커에 permission 허용해줘야함
    • Permissions 패키지 사용
  • api는 기본적으로 promise로 사용됨 > async, await 으로 권한 부여 받고 통과시 사용
  • api 사용시 파라미터에 카메라 설정 부여 가능
...

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;

Location

Location api
  • expo-location 패키지에서 가져옴
  • 카메라 api와 동일하게 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;

MapView, Marker

  • react-native-maps 패키지에서 가져옴
  • location api를 통해 얻은 정보로 네이티브 지도앱을 사용해 마커를 찍고 해당 좌표 얻는 등의 상호작용 가능함
  • MapView 에 좌표등의 지역정보를 할당해주면 표시 가능
    • ios는 apple맵이 디폴트이고 구글맵으로도 설정 가능, android는 구글맵만 가능
  • 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, 디바이스 모션, 자이로스코프, 키보드, 바이브레이션, 푸시 알림 등 지원하는 기능들이 꽤 있음.

https://docs.expo.io/versions/latest/

추가 설명

  • 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}])
    • 얼럿 내용, 버튼 텍스트, 스타일, press 이벤트 핸들러를 할당받아 사용
  • 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'
    • 아래와 같이 select 메소드로 플랫폼 키값에 따라 값을 사용할 수 있음
    Platform.select({ios: fooValue, android: fooValue})
  • Platform-specific 파일로 구분해서 개발도 가능함

    • CustomButton.android.js
    • CustomButton.ios.js
    • import시에는 postfix 제외하고 /CustomButton사용
  • SafeAreaView : 아이폰 노치, 하단의 task manage bar같은 부분들에 대한 보정 처리를 자동으로 해줌

    • 최상위 컴포넌트를 래핑만 해주면 가능
라우팅 (Web과 차이점)
  • web : urlsource of truth로 라우팅 처리함
  • app : tab, stack 같은 걸 사용해 이동함

코드 푸시
  • 앱 심사 없이 실시간 업데이트를 가능하게 해주는것
  • 네이티브 코드, 설정 이외의 js 코드와 asset(이미지, 폰트)만 변경된 경우 가능
    • 네이티브 영역이 수정되었다면 비정상 종료되고 다시 패키징해서 심사를 거쳐 배포해야함

Ionic

React Native 와는 다르게 웹앱을 네이티브앱으로 래핑하는 개발 방식 사용.
ionic 회사가 만든 프레임워크로 아래와 같이 분리해서 봐야함

  • Ionic UI Frameworks : 모바일 UI Look-alike + 웹UI 까지 커버하기 쉽게 도와주는 UI 컴포넌트들을 제공

  • Ionic Native : CapcaitorCordova 플러그인을 활용해 쉽게 디바이스 제어 기능 제공

    • Capcaitor : Ionic팀에서 개발한 것으로 Cordova에서 진화한 버전이라고 보면 됨. 웹앱 빌드를 네이티브 앱으로 래핑해 빌드해주고, 디바이스 제어 가능한 플러그인, 런타임 환경 제공

      참고

  • react로 정식 release된건 2019년 8월로 얼마되지 않음(초기엔 angular만 가능했음)

Basic

View, Styling

  • web component 제공 (using custom html tag)
  • vanlia css, built-in css(CSS Variables) 모두 사용 가능
  • 웹앱이기 때문에 html, css로 디자인, 마크업 작성 가능
    • 다만 모바일앱처럼 동작하도록 디테일한 수정 작업이 필요하고, 리소스가 부족하다면 ionic에서 제공하는 UI 컴포넌트, 빌트인 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 프로젝트에서 capaitor 활성화
	$ 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

기본 permission 설정
  • ios 빌드 디렉토리 > info.plist 파일
  • android 빌드 디렉토리 > AndroidManifest.xml 파일
예시 without ionic-native (카메라)
  • @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;

Flutter 간단 소개와 비교

1. Flutter

간단 소개
  • 모바일용 위젯 UI 프레임워크 + SDK(네이티브앱 빌드, 런타임 환경 등)

  • Dart 사용 (dart, flutter 모두 구글에서 만듬)

  • 네이티브 앱개발자가 크로스플랫폼 지향 개발시 사용하기 쉬운 구조

    • OOP 적극 사용, 네이티브 코드 컴파일
  • 두 플랫폼의 네이티브 UI 위젯을 사용하지 않고, Skia 엔진을 사용해 직접 렌더링하고 컨트롤함
    (실제 네이티브 UI가 아니고 유사 UI라 완전히 스타일이 동일하지 않을 수 있지만 거의 흡사함)

플러터 엔진(C,C++ 사용)에 포함된Skia 엔진은 안드로이드와 동일한 렌더링 엔진이며, DartAOT를 지원하기 때문에 성능이 좋음.

참고

장점

  • Skia Engine 사용
    • material(android), cupertino(ios) 디자인을 각각 적용 가능
    • 네이티브 위젯이 아니라서 이질감은 있을 수 있지만, 성능 및 완성도는 높음
  • ios, android, windows, mac, web 모두 지원함( web은 아직 불안정)
  • cpu, gpu 를 헤비하게 사용하는 앱에서는 RN보다 유리함

단점

  • 플러그인 제공이 아직 부족 : ex) admob 적용 불편
  • Dart 사용 (아마도 거의 플러터에서만 사용됨)

2. RN

장점

  • React 웹 개발자들이 진입하기 굉장히 쉬움
  • 플러터보다 더 빨리 출시되었기 때문에 커뮤니티도 아직은 더 활성화 되어있음. (work around 방식들도 찾기 수월함)
  • 아직까지는 네이티브 기능을 더 많이 활용 가능함

단점

  • Javascript 네이티브 브릿지 사용
    • 뷰만 컴파일해서 네이티브 위젯으로 변환하고, 이외의 자바스크립트 로직들은 네이티브 코드가 아닌 브리지 통해서 통신함
    • ios, android 환경에 맞춰 별도 구현이 필요한 상황이 생기게됨
    • 코드 디버깅을 항상 두 플랫폼 진행하게됨
  • 써드파티 라이브러리없이는 구현하기 힘듬
    • 기본적으로 제공되는 UI 컴포넌트가 적음
    • expo 패키지 이용해야 개발 수월함
  • pc app, web은 지원 x

3. Ionic

장점

  • 굉장히 많은 pre-built UI 컴포넌트 제공하고 안정적으로 동작함
  • 웹개발자 입장에서는 one source로 크로스 플랫폼 지원 지향시 가장 쉽게 개발 가능
    • 웹앱(네이티브앱으로 래핑된 웹뷰 사용하는 웹앱)이기 때문에 가장 웹기술를 많이 사용
  • 위 2개의 프레임워크, 플랫폼보다는 개발의 자유도가 높음
    • ex) react, angular, vue 등 많은 라이브러리 지원

단점

  • IE 지원 안함 (모바일 환경 타겟이면 상관없긴함)
  • 네이티브 코드로 컴파일되지 않음 => 래핑된 웹앱이기 때문에 성능면에서는 가장 하위임
    • 어떤 유형의 서비스를 제공하냐에 따라, 개발된 코드 수준에 따라 달라지는 부분이라 애매함
  • 디바이스 기능 제어 부분에서 제공하는 api가 위 2개보다는 적은편
    • Cordva, Capacitor 같은 플러그인에 의지
  • UI 프레임워크를 사용한다면 Flutter보다 모바일 네이티브와 같은 looking, ux면에서 떨어짐.
profile
Web Service Developer

0개의 댓글