본문 바로가기

flutter

Flutter는 어떻게 작동할까? 아키텍처와 네이티브 개발 완전 분석

반응형

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가 실제로 하는 일:

  1. 100x100 픽셀 사각형 그리기
  2. 파란색으로 채우기
  3. 모서리 10픽셀 둥글게 처리
  4. 그림자 효과 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});

 

언제 네이티브 개발을 해야 할까?

네이티브 개발이 필요한 신호들

  1. Flutter 패키지가 없는 기능
    • pub.dev에서 찾을 수 없는 플랫폼 고유 기능
  2. 성능이 중요한 작업
    • 실시간 이미지/비디오 처리
    • 복잡한 수학 연산
    • 대용량 데이터 처리
  3. 플랫폼 통합이 중요한 경우
    • 위젯/확장앱
    • 백그라운드 서비스
    • 시스템 수준 통합
  4. 보안이 중요한 기능
    • 생체 인증
    • 암호화/복호화
    • 키 관리

 

개발 우선순위

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와 네이티브 개발의 조합으로 최고의 사용자 경험을 만들어보세요!

반응형