네이티브앱 통합 관점에서 RN vs Flutter
요약)
가장 중요한 차이는
RN은 JavaScript 런타임과 네이티브 의존성을 따로 관리 하는 반면, Flutter는 자체 내장 엔진에서 모든 의존성을 자체 관리
React Native | Flutter | |
의존성관리 | JavaScript 런타임과 네이티브 의존성을 Podfile로 따로 관리 | - 모듈 생성시 생성된 .ios/Flutter/podhelper.rb를 통해 CocoaPods 의존성을 자동으로 관리 - Flutter 모듈의 모든 네이티브 의존성을 캡슐화 |
통합방식 | 기존 iOS 프로젝트의 Podfile과 RN의 Podfile을 병합해야 함 Why? ) React Native는 JavaScript 코드를 실행하기 위해 React, React-Core, React-RCTBridge 등 다양한 네이티브 모듈에 의존 CocoaPods으로 개별 설치해야 함 (ios폴더 내 Podfile이 RN의 네이티브 의존성을 관리) => 기존 iOS 프로젝트와 통합하려면, 기존 iOS프로젝트의 Podfile과 React Native에서 사용하는 Podfile을 통합해주어야함. |
Podfile에 Flutter 모듈 경로만 추가하면 됨 Why? ) Flutter는 자체 내장 엔진을 가지고 있음 => iOS프로젝트의 Podfile에 Flutter 모듈 경로만 추가하면 됨 |
통합용 이성 |
RN 프로젝트의 Podfile에서 사용하는 라이브러리와 기존 프로젝트의 동일한 라이브러리가 충돌 | Flutter모듈의 독립적인 의존성 관리로 의존성 충돌 가능성이 낮음 |
빌드성능 | Bridge의 오버헤드로 인해 상대적으로 느림 | Flutter 엔진 내장으로 네이티브와 거의 동일한 성능 제공 |
통신방식 | CocoaPod을 통해 설치된 React-RCTBridge를 통해 JavaScript와 네이티브간 통신 | MethodChannel을 통해 Dart와 네이티브 간 직접적으로 통신 |
환경셋팅만 잘 이루어진다면.. RN이 더 나을지도 모르겠다
( GPT의 반대 관점 !!!! 혼란스럽다 )
iOS와 Android의 네이티브 기능을 많이 활용해야 하는 경우 React Native를 선호할 수 있는 이유는 다음과 같습니다:
1. 네이티브 코드와의 손쉬운 통합
React Native는 기본적으로 네이티브 모듈(Swift, Objective-C, Java, Kotlin)과 매우 쉽게 연동할 수 있는 구조를 제공합니다.
• iOS와 Android 네이티브 모듈 확장: 네이티브에서 직접 구현한 기능을 React Native에서 호출할 수 있어, 플랫폼에 특화된 기능을 개발하고 통합하기에 적합합니다.
• 예: iOS의 Core Location, Face ID, Android의 CameraX 등의 플랫폼별 API를 React Native의 네이티브 모듈로 래핑하여 사용할 수 있습니다.
(ReactNative의 iOS 폴더 내에서 네이티브 모듈을 구현한 후, ReactNative에서 바로 호출하는 구조. 네이티브 프로젝트에서 코드관리X 반면 Flutter는 네이티브 프로젝트에서 플러터ViewController를 생성한 뒤, MethodChannel로 받아주어야함
>> 예시코드는 밑에 있음)
2. 기존 네이티브 프로젝트와의 병합 가능성
React Native는 기존의 네이티브 앱 프로젝트에 일부 화면으로 통합하는 데 적합합니다.
• 이미 iOS 또는 Android로 개발된 앱이 있을 때, 특정 화면이나 기능을 React Native로 구현해 추가할 수 있습니다.
• iOS ViewController나 Android Activity/Fragment를 React Native 화면과 결합하는 것이 비교적 간단합니다.
3. 커스텀 네이티브 코드 개발이 용이
React Native는 네이티브 API에서 제공하지 않는 고유 기능을 개발해야 할 때도 유연합니다.
• Custom Native Module: iOS와 Android에서 별도로 네이티브 기능을 개발한 후, JavaScript에서 호출할 수 있도록 연결할 수 있습니다.
• 예: iOS에서 Metal API로 고성능 그래픽을 구현하거나, Android에서 BLE(Bluetooth Low Energy) 기능을 네이티브로 구현한 후 React Native와 연동 가능.
4. 네이티브 코드와 JavaScript 간 통신 구조 (Bridge)
React Native는 Bridge 아키텍처를 사용하여 JavaScript와 네이티브 간 데이터를 주고받을 수 있습니다.
• 복잡한 네이티브 로직은 네이티브 코드로 구현하고, React Native는 UI 및 간단한 로직을 처리하는 방식으로 역할 분리가 가능합니다.
• 이 방식은 성능 병목 현상을 줄이고 네이티브 기능을 쉽게 호출할 수 있도록 합니다.
5. 풍부한 네이티브 플러그인 생태계
React Native는 커뮤니티에서 개발된 수많은 네이티브 플러그인을 제공합니다.
• 네이티브 기능(카메라, 지도, 파일 저장소 등)을 구현할 때, 기존 플러그인을 활용하면 추가 구현 없이 바로 사용할 수 있습니다.
• 예: react-native-camera, react-native-maps, react-native-fs 등이 있으며, 필요한 경우 직접 커스터마이징도 가능합니다.
6. 코드 재사용성 극대화
React Native는 iOS와 Android에 공통 코드베이스를 사용하면서도, 플랫폼별 네이티브 코드와 연동이 가능하기 때문에, 네이티브 기능을 활용하면서도 중복 작업을 최소화할 수 있습니다.
7. React Native의 한계점
• Bridge 기반 구조로 인해, 네이티브와 JavaScript 간 데이터 통신 속도가 병목 현상을 유발할 수 있습니다. 이는 매우 고성능을 요구하는 앱에서는 문제가 될 수 있습니다.
• 복잡한 네이티브 기능을 많이 사용하는 경우, 네이티브 코드 작성량이 많아져 크로스플랫폼의 이점이 줄어들 수 있습니다.
Flutter와 비교
Flutter도 네이티브 기능 연동이 가능하지만, React Native보다 초기 설정이 복잡하고, 플랫폼별 코드를 작성할 때 조금 더 많은 작업이 필요합니다.
• Flutter는 네이티브 코드와 연동하기 위해 MethodChannel을 사용하지만, 네이티브 개발 경험이 없는 개발자에게는 추가 학습 부담이 될 수 있습니다.
• 반면 React Native는 기존 네이티브 코드 경험을 살려 더 직관적으로 접근할 수 있습니다.
- Flutter는 MethodChannel을 사용하여 Flutter와 네이티브 간의 직접적인 통신을 설정
- React Native는 네이티브 모듈을 통해 Bridge 방식으로 통신 >> 초기에 작업해야 할 설정의 복잡성과 사용 편의성에서 차이가 발생
1. Flutter: MethodChannel 사용
Flutter는 네이티브와의 통신을 위해 MethodChannel이라는 구조를 사용
• Flutter와 네이티브 간 통신을 위한 MethodChannel 설정.
• 플랫폼별로 명시적으로 MethodHandler를 작성하여 각 네이티브 기능을 처리.
• 결과값을 다시 Flutter로 반환.
[Flutter 코드]
import 'package:flutter/services.dart';
class NativeUtils {
static const platform = MethodChannel('com.example/native');
// 네이티브 메서드 호출
static Future<String> getBatteryLevel() async {
try {
final String batteryLevel = await platform.invokeMethod('getBatteryLevel');
return 'Battery level: $batteryLevel%';
} catch (e) {
return 'Failed to get battery level: $e';
}
}
}
[ iOS (Swift): MethodChannel 구현]
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "com.example/native",
binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "getBatteryLevel" {
self.getBatteryLevel(result: result)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == .unknown {
result(FlutterError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
• MethodChannel 작성: Flutter와 네이티브 간 직접적인 메시지 전달 방식을 설정해야 하므로 네이티브 처리에 대한 이해가 필요
• 플랫폼별 코드 작성: iOS와 Android 각각에 대해 별도의 코드 작성이 필수
2. React Native: Native Modules 사용
- React Native는 네이티브와 통신하기 위해 Native Module이라는 방식을 사용
- Bridge 아키텍처를 통해 JavaScript와 네이티브 코드 간의 통신을 추상화하여 더 직관적으로 접근
React Native 네이티브 연동 단계
1. RN프로젝트 내에서 ios/android폴더 내에 네이티브 모듈 정의
2. JavaScript에서 NativeModules를 통해 네이티브 호출.
3. 네이티브 코드에서 처리 후 결과 반환.
React Native 에서 네이티브 모듈 호출
import { NativeModules } from 'react-native';
const { NativeUtils } = NativeModules;
export const getBatteryLevel = async () => {
try {
const batteryLevel = await NativeUtils.getBatteryLevel();
console.log(`Battery level: ${batteryLevel}%`);
return batteryLevel;
} catch (error) {
console.error('Failed to get battery level:', error);
return null;
}
};
ReactNative의 iOS 폴더 내에서 네이티브 모듈 정의
@objc(NativeUtils)
class NativeUtils: NSObject {
@objc
func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == .unknown {
reject("UNAVAILABLE", "Battery info unavailable", nil)
} else {
resolve(Int(device.batteryLevel * 100))
}
}
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
}
iOS네이티브 프로젝트에서 모듈 등록
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(NativeUtils, NSObject)
RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end
React Native 연동의 학습 부담
• React Native는 네이티브 모듈을 생성하면 해당 기능이 JavaScript로 자동 연결되므로 설정이 간소화됩니다.
• 플랫폼별 코드 작성이 필요하지만, 기존 네이티브 개발자라면 친숙한 환경에서 작업할 수 있습니다.
• JavaScript로 네이티브 모듈을 쉽게 호출할 수 있으므로 사용이 직관적입니다.
비교: Flutter vs React Native
- 설정 복잡성 측면: Fluutter는 MethodChannel 생성, 각 네이티브 프로젝트 코드에서 MethodChannel을 받아서 처리해야함
(플랫폼별 코드를 각각 작성)
반면 RN은 RN폴더 내에서 네이티브 모듈 생성 후 바로 사용 가능 (호출이 직관적 ??)
>> iOS/Android의 네이티브 기능 연동이 빈번하다면 React Native가 더 적합할 가능성이 높다 ???
(참고)
Flutter의 자체 엔진이 주는 장점
Flutter는 React Native와 달리 자체 엔진을 포함하고 있어 여러 면에서 차별화됩니다.
4.1. 자체 엔진의 특징
- Flutter는 C++ 기반의 렌더링 엔진을 사용하여 모든 UI 및 로직을 실행합니다.
- Dart VM은 Flutter 엔진 내부에서 동작하며, UI를 직접 그리는 방식(Skia)을 사용합니다.
- 네이티브 플랫폼의 컴포넌트를 활용하지 않고, 완전히 독립적인 렌더링 방식을 사용합니다.
4.2. 장점
- 플랫폼 독립성:
- Flutter 엔진은 iOS와 Android에서 동일하게 동작하므로, UI 일관성이 유지됩니다.
- 네이티브 의존성이 최소화되며, 플랫폼 간 동작 차이를 걱정할 필요가 없습니다.
- 고성능:
- React Native는 JSCore와 네이티브 간 통신(Bridge)에 오버헤드가 발생하지만, Flutter는 자체 엔진 내에서 모든 UI 렌더링과 로직 처리를 수행하므로 네이티브와 거의 동일한 성능을 제공합니다.
- 통합 간소화:
- Flutter는 .ios/Flutter를 통해 모든 의존성을 캡슐화하여 기존 네이티브 프로젝트와의 충돌 가능성을 최소화합니다.
- Podfile에 Flutter 모듈 경로를 추가하는 것만으로도 통합이 완료됩니다.