반응형
Flutter가 단순히 코드를 네이티브로 변환하는 건 아니라고요? 그럼 어떻게 작동하는 걸까요? Flutter의 독특한 렌더링 방식부터 네이티브 기능 연동까지, 깊이 있게 파헤쳐보겠습니다.
이 글에서 배우는 것
- Flutter의 독특한 렌더링 아키텍처
- 다른 크로스플랫폼 프레임워크와의 차이점
- 네이티브 개발이 필수인 상황들
- Flutter에서 네이티브 기능 연동하는 방법
- Platform Channel의 동작 원리
Flutter는 포팅이 아니다!
많은 사람들이 Flutter를 "Dart 코드를 iOS/Android 코드로 변환해주는 도구"라고 생각하지만, 실제로는 완전히 다른 방식으로 작동합니다.
일반적인 오해
❌ 잘못된 생각
Dart 코드 → iOS Swift 코드 변환
Dart 코드 → Android Kotlin 코드 변환
실제 Flutter 동작 방식
✅ 실제 동작
Dart 코드 → Flutter Engine → Skia → GPU → 화면에 직접 그리기
Flutter는 자체 렌더링 엔진을 사용해서 모든 UI를 직접 그립니다!
Flutter 아키텍처 완전 분석
3층 구조로 이해하기
┌─────────────────────────────────┐
│ Framework (Dart) │ ← 위젯, 애니메이션, 제스처
│ Material, Cupertino, etc. │ 우리가 주로 작업하는 영역
├─────────────────────────────────┤
│ Engine (C/C++) │ ← Skia, Dart Runtime
│ Skia Graphics, Text Layout │ 핵심 렌더링 엔진
├─────────────────────────────────┤
│ Embedder (Platform) │ ← iOS/Android 호스트
│ iOS Runner, Android Host │ 플랫폼별 최소한의 코드
└─────────────────────────────────┘
각 레이어의 역할
1. Framework Layer (Dart)
// 우리가 작성하는 코드
Container(
width: 100,
height: 100,
color: Colors.blue,
child: Text('Hello Flutter'),
)
역할:
- 위젯 시스템 (Material, Cupertino)
- 애니메이션과 제스처 처리
- 상태 관리와 라이프사이클
2. Engine Layer (C/C++)
// 내부적으로 실행되는 C++ 코드 (간소화)
skia_canvas.drawRect(Rect(0, 0, 100, 100), blue_paint);
skia_canvas.drawText("Hello Flutter", font, black_paint);
역할:
- Skia Graphics Engine: 2D 그래픽 렌더링
- Dart Runtime: Dart 코드 실행
- Text Layout Engine: 텍스트 렌더링
3. Embedder Layer (Platform)
// iOS AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
역할:
- 플랫폼별 최소한의 호스팅 코드
- 시스템 이벤트 처리
- 플랫폼 서비스 연결
다른 크로스플랫폼과 비교
React Native 방식
┌──────────────────┐ Bridge ┌──────────────────┐
│ JavaScript │ ←----------→ │ Native Widgets │
│ React Components│ │ UIView, TextView│
└──────────────────┘ └──────────────────┘
특징:
- JavaScript와 네이티브 간 브리지 통신
- 실제 네이티브 위젯 사용
- 플랫폼별 Look & Feel 유지
Xamarin 방식
┌─────────────────┐ Compile ┌──────────────────┐
│ C# Code │ ----------→ │ Native Code │
│ .NET │ │ iOS/Android │
└─────────────────┘ └──────────────────┘
특징:
- C# 코드를 네이티브 코드로 컴파일
- 플랫폼별 UI 코드 필요
- 진짜 네이티브 성능
Flutter 방식
┌─────────────────┐ Direct ┌──────────────────┐
│ Dart Code │ ----------→ │ Skia Canvas │
│ Flutter │ │ Direct Drawing │
└─────────────────┘ └──────────────────┘
특징:
- 브리지 없이 직접 렌더링
- 모든 플랫폼에서 픽셀 단위 동일
- 자체 위젯 시스템
Skia Graphics Engine이란?
Skia의 정체
Skia는 Google이 개발한 오픈소스 2D 그래픽 라이브러리입니다.
사용 사례:
- Google Chrome: 웹페이지 렌더링
- Android: UI 렌더링
- Chrome OS: 전체 화면 렌더링
- Flutter: 앱 UI 렌더링
Flutter에서 Skia 활용
// Flutter 위젯
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
boxShadow: [BoxShadow(blurRadius: 5)],
),
)
Skia가 실제로 하는 일:
- 100x100 픽셀 사각형 그리기
- 파란색으로 채우기
- 모서리 10픽셀 둥글게 처리
- 그림자 효과 5픽셀 블러 적용
직접 렌더링의 장점
✅ 장점
- 모든 플랫폼에서 픽셀 단위 동일
- 60fps 부드러운 애니메이션
- 브리지 오버헤드 없음
- 자유로운 커스텀 UI 가능
❌ 단점
- 앱 크기 증가 (Skia 엔진 포함)
- 플랫폼 네이티브 Look & Feel과 차이
- 접근성 기능 구현 복잡
네이티브 개발이 필수인 상황들
Flutter가 아무리 강력해도, 네이티브 개발이 꼭 필요한 영역들이 있습니다.
1. 플랫폼별 고유 기능
iOS 고유 기능
// Face ID / Touch ID 인증
import LocalAuthentication
let context = LAContext()
context.evaluatePolicy(.biometryAny, localizedReason: "인증이 필요합니다") { success, error in
// 인증 결과 처리
}
Android 고유 기능
// Android 위젯 (홈 화면 위젯)
class MyAppWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// 위젯 업데이트 로직
}
}
2. 하드웨어 직접 제어
센서 데이터 처리
// Android - 가속도계 센서
class SensorManager {
fun startAccelerometer() {
sensorManager.registerListener(
this,
accelerometer,
SensorManager.SENSOR_DELAY_NORMAL
)
}
}
카메라 고급 제어
// iOS - 카메라 세부 설정
let camera = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back)
try camera?.lockForConfiguration()
camera?.exposureMode = .custom
camera?.setExposureModeCustom(duration: CMTime, iso: Float)
3. 백그라운드 작업
백그라운드 서비스
// Android - 백그라운드 위치 추적
class LocationService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, createNotification())
startLocationUpdates()
return START_STICKY
}
}
푸시 알림 고급 처리
// iOS - 커스텀 알림 액션
let actionIdentifier = "REPLY_ACTION"
let action = UNTextInputNotificationAction(
identifier: actionIdentifier,
title: "답장",
options: [],
textInputButtonTitle: "보내기",
textInputPlaceholder: "메시지를 입력하세요"
)
4. 성능 최적화가 중요한 영역
이미지/비디오 처리
// NDK를 활용한 이미지 처리
extern "C" JNIEXPORT void JNICALL
Java_com_example_ImageProcessor_processImage(JNIEnv *env, jobject thiz, jintArray pixels) {
// C++로 고성능 이미지 처리
jint *pixelArray = env->GetIntArrayElements(pixels, 0);
// 픽셀 데이터 직접 조작
}
5. 보안이 중요한 기능
키체인/키스토어 접근
// iOS - 키체인에 민감한 데이터 저장
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: password.data(using: .utf8)!
]
SecItemAdd(query as CFDictionary, nil)
Flutter에서 네이티브 기능 연동하기
Flutter는 Platform Channel을 통해 네이티브 기능과 통신합니다.
Platform Channel 아키텍처
┌─────────────────┐ Message ┌──────────────────┐
│ Flutter │ ←----------→ │ Native Code │
│ (Dart) │ Channel │ (Swift/Kotlin) │
└─────────────────┘ └──────────────────┘
1. MethodChannel 구현 예시
Dart 코드 (Flutter)
class BatteryService {
static const platform = MethodChannel('com.example.app/battery');
static Future<int?> getBatteryLevel() async {
try {
final result = await platform.invokeMethod<int>('getBatteryLevel');
return result;
} catch (e) {
print('배터리 정보를 가져올 수 없습니다: $e');
return null;
}
}
}
// 사용법
void checkBattery() async {
final batteryLevel = await BatteryService.getBatteryLevel();
print('배터리 잔량: $batteryLevel%');
}
Android 네이티브 코드 (Kotlin)
// MainActivity.kt
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "배터리 정보를 사용할 수 없습니다", null)
}
}
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}
iOS 네이티브 코드 (Swift)
// AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(
name: "com.example.app/battery",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self.receiveBatteryLevel(result: result)
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == UIDevice.BatteryState.unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "배터리 정보를 사용할 수 없습니다",
details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
2. EventChannel 구현 (스트리밍 데이터)
Dart 코드
class SensorService {
static const eventChannel = EventChannel('com.example.app/sensor');
static Stream<double> get accelerometerStream {
return eventChannel.receiveBroadcastStream().map((event) => event as double);
}
}
// 사용법
void listenToSensor() {
SensorService.accelerometerStream.listen((acceleration) {
print('가속도: $acceleration m/s²');
});
}
Android 네이티브 코드
class MainActivity: FlutterActivity() {
private val EVENT_CHANNEL = "com.example.app/sensor"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
startSensorListener(events)
}
override fun onCancel(arguments: Any?) {
stopSensorListener()
}
})
}
}
3. Platform Channel 사용 패턴
단방향 통신 (Flutter → Native)
// Flutter에서 네이티브 기능 호출
await platform.invokeMethod('openSettings');
양방향 통신 (데이터 반환)
// 네이티브에서 데이터 받기
final result = await platform.invokeMethod('calculateHash', {'data': inputData});
스트림 통신 (실시간 데이터)
// 실시간 데이터 스트림
eventChannel.receiveBroadcastStream().listen((data) {
updateUI(data);
});
네이티브 개발 시 고려사항
1. 플랫폼별 권한 처리
Android - 권한 요청
// 런타임 권한 요청
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA)
}
iOS - Info.plist 설정
<!-- iOS 권한 설정 -->
<key>NSCameraUsageDescription</key>
<string>앱에서 사진 촬영을 위해 카메라 접근이 필요합니다</string>
2. 에러 처리 패턴
class NativeService {
static Future<String?> callNativeMethod() async {
try {
final result = await platform.invokeMethod<String>('nativeMethod');
return result;
} on PlatformException catch (e) {
print('플랫폼 에러: ${e.code} - ${e.message}');
return null;
} catch (e) {
print('알 수 없는 에러: $e');
return null;
}
}
}
3. 성능 최적화
대용량 데이터 전송 최적화
// ❌ 나쁜 예: 큰 데이터를 직접 전송
await platform.invokeMethod('processLargeData', {'data': hugeBinaryData});
// ✅ 좋은 예: 파일 경로만 전송
final tempFile = await writeToTempFile(hugeBinaryData);
await platform.invokeMethod('processFileData', {'filePath': tempFile.path});
언제 네이티브 개발을 해야 할까?
네이티브 개발이 필요한 신호들
- Flutter 패키지가 없는 기능
- pub.dev에서 찾을 수 없는 플랫폼 고유 기능
- 성능이 중요한 작업
- 실시간 이미지/비디오 처리
- 복잡한 수학 연산
- 대용량 데이터 처리
- 플랫폼 통합이 중요한 경우
- 위젯/확장앱
- 백그라운드 서비스
- 시스템 수준 통합
- 보안이 중요한 기능
- 생체 인증
- 암호화/복호화
- 키 관리
개발 우선순위
1순위: 기존 Flutter 패키지 사용
2순위: 간단한 Platform Channel 구현
3순위: 복잡한 네이티브 기능 개발
4순위: 플러그인 패키지 제작 및 배포
실무에서의 하이브리드 접근법
대부분의 실제 앱 구조
Flutter App (90%)
├── UI 및 비즈니스 로직
├── 네트워킹 및 상태 관리
├── 일반적인 디바이스 기능
└── Native Integration (10%)
├── 플랫폼별 고유 기능
├── 성능 중요 작업
└── 시스템 수준 통합
성공적인 하이브리드 앱 사례
Alibaba (알리바바)
- Flutter: 대부분의 UI와 비즈니스 로직
- Native: 결제 시스템, 보안 모듈
BMW (BMW)
- Flutter: 차량 제어 UI
- Native: 실시간 차량 데이터 처리
Google Pay (구글 페이)
- Flutter: 사용자 인터페이스
- Native: 결제 처리, 보안 인증
학습 로드맵
단계별 네이티브 개발 학습
1단계: Flutter 마스터 (2-3개월)
- 기본 위젯과 상태 관리
- 패키지 사용법 숙달
- 일반적인 앱 개발 패턴
2단계: Platform Channel 기초 (1개월)
- MethodChannel 구현
- 간단한 네이티브 기능 연동
- 에러 처리 패턴
3단계: 플랫폼별 기초 (각 2개월)
- Android: Kotlin, Android SDK
- iOS: Swift, iOS SDK
4단계: 고급 통합 (2-3개월)
- 복잡한 네이티브 기능 구현
- 성능 최적화 기법
- 플러그인 패키지 개발
정리
Flutter의 동작 원리와 네이티브 개발의 필요성을 정리하면
Flutter의 핵심 특징
- 직접 렌더링: Skia 엔진으로 픽셀 단위 제어
- 일관된 UI: 모든 플랫폼에서 동일한 모습
- 뛰어난 성능: 브리지 없는 직접 통신
네이티브 개발이 필요한 영역
- 플랫폼 고유 기능: Face ID, 홈 위젯 등
- 하드웨어 직접 제어: 센서, 카메라 고급 기능
- 백그라운드 작업: 백그라운드 서비스, 알림
- 보안 기능: 키체인, 암호화 모듈
Platform Channel 활용
- MethodChannel: 단방향/양방향 통신
- EventChannel: 실시간 스트리밍 데이터
- BasicMessageChannel: 커스텀 메시지 형식
Flutter와 네이티브 개발의 조합으로 최고의 사용자 경험을 만들어보세요!
반응형
'flutter' 카테고리의 다른 글
| Flutter iOS 실기기에서 하얀 화면 문제 해결하기 (0) | 2025.09.24 |
|---|---|
| flutter social login - google 소셜 로그인 (Android, iOS) (0) | 2025.09.23 |
| Flutter 시작하기: 크로스 플랫폼 앱 개발의 첫걸음 (0) | 2025.09.22 |
| Dart 기초 마스터하기 #4 List와 Map으로 데이터 다루기 (0) | 2025.09.21 |
| Dart 기초 마스터하기 #3 조건문과 반복문으로 프로그램 제어하기 (0) | 2025.09.21 |