Flutter 튜토리얼 70편: 다중 Flutter 인스턴스와 성능
요약
핵심 요지
- 문제 정의: 기존 네이티브 앱에 여러 Flutter 화면을 추가하면 각각의 FlutterEngine1이 메모리를 차지해 앱이 무거워진다.
- 핵심 주장:
FlutterEngineGroup2을 사용하면 여러 Flutter 인스턴스가 공통 리소스를 공유하여 메모리를 절약할 수 있다. - 주요 근거: 첫 번째 엔진만 전체 로딩 비용이 발생하고, 이후 엔진은 폰트와 GPU 컨텍스트 등을 재사용한다.
- 실무 기준: 각 FlutterEngine은 독립적인 Dart Isolate를 가지므로 화면 간 상태 공유는 플랫폼 채널로 처리해야 한다.
- 한계: 아직 실험적 기능이며, 웹 플랫폼에서는 지원되지 않는다.
문서가 설명하는 범위
- FlutterEngineGroup으로 다중 인스턴스 생성하기
- Android와 iOS에서의 구현 방법
- Flutter 엔진 로딩 시퀀스와 성능 최적화
- 메모리 오버헤드 관리 전략
읽는 시간: 22분 | 난이도: 고급
참고 자료
- Multiple Flutter screens or views - 다중 Flutter 화면 공식 문서
- Load sequence, performance, and memory - 성능 및 메모리 공식 문서
문제 상황
기존 네이티브 앱에 Flutter를 통합할 때, 하나의 화면만 추가하는 것이 아니라 여러 화면을 Flutter로 구현해야 하는 경우가 있습니다. 예를 들어 설정 화면, 프로필 화면, 결제 화면을 모두 Flutter로 만들고 싶을 수 있습니다.
다중 인스턴스의 메모리 문제
화면 A (FlutterEngine) → 메모리 ~180MB화면 B (FlutterEngine) → 메모리 ~180MB화면 C (FlutterEngine) → 메모리 ~180MB───────────────────────────────────총 메모리 사용량 → ~540MB문제는 다음과 같습니다.
- 각 FlutterEngine이 독립적으로 Dart VM, 폰트 로더, GPU 컨텍스트를 초기화합니다.
- 앱에 Flutter 화면이 많아질수록 메모리 사용량이 선형으로 증가합니다.
- 저사양 기기에서 메모리 부족으로 앱이 강제 종료될 수 있습니다.
해결 방법
Flutter 2.0부터 도입된 FlutterEngineGroup을 사용하면 여러 Flutter 인스턴스가 공통 리소스를 공유합니다.
첫 번째 엔진만 전체 초기화 비용이 발생하고, 이후 엔진은 기존 리소스를 재사용합니다.
챕터 1: FlutterEngineGroup 이해하기
Why
NOTE여러 Flutter 화면을 사용할 때 각각 독립적인 FlutterEngine을 생성하면 메모리 낭비가 심합니다. FlutterEngineGroup은 공유 가능한 리소스를 그룹 내 엔진들이 재사용하게 하여 메모리를 절약합니다.
대규모 앱에서 Flutter를 점진적으로 도입할 때 특히 중요한 기능입니다.
What
NOTEFlutterEngineGroup은 FlutterEngine을 생성하는 팩토리 역할을 합니다. 그룹에서 생성된 엔진들은 다음 리소스를 공유합니다.
공유되는 리소스 설명 GPU 컨텍스트 그래픽 렌더링에 필요한 OpenGL/Metal 컨텍스트 폰트 메트릭스 폰트 로딩 및 측정 데이터 이미지 디코딩 스냅샷 공유 이미지 캐시 Isolate 그룹 스냅샷 Dart Isolate 초기화 데이터 graph TD A[FlutterEngineGroup] --> B[Engine 1] A --> C[Engine 2] A --> D[Engine 3] E[공유 리소스] --> B E --> C E --> D E --> |GPU 컨텍스트| F[렌더링] E --> |폰트 캐시| G[텍스트] E --> |이미지 캐시| H[이미지]첫 번째 엔진 생성 시 약 180MB가 필요하지만, 추가 엔진은 약 20MB만 필요합니다.
How
TIPAndroid에서 FlutterEngineGroup 사용하기
// App.kt - Application 클래스class App : Application() {lateinit var engineGroup: FlutterEngineGroupoverride fun onCreate() {super.onCreate()// FlutterEngineGroup 초기화engineGroup = FlutterEngineGroup(this)}}// MainActivity.kt - 엔진 생성class MainActivity : FlutterActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val app = application as App// 첫 번째 엔진 생성 (전체 초기화 비용 발생)val engineOne = app.engineGroup.createAndRunDefaultEngine(this)// 두 번째 엔진 생성 (리소스 공유로 빠른 생성)val engineTwo = app.engineGroup.createAndRunDefaultEngine(this)// 특정 진입점으로 엔진 생성val options = DartEntrypoint(FlutterInjector.instance().flutterLoader().findAppBundlePath(),"secondScreenMain")val engineThree = app.engineGroup.createAndRunEngine(this, options)}}iOS에서 FlutterEngineGroup 사용하기
AppDelegate.swift import Flutterimport UIKit@mainclass AppDelegate: FlutterAppDelegate {let engineGroup = FlutterEngineGroup(name: "multiple-flutters", project: nil)override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {return super.application(application, didFinishLaunchingWithOptions: launchOptions)}}ViewController.swift class ViewController: UIViewController {var engineOne: FlutterEngine!var engineTwo: FlutterEngine!override func viewDidLoad() {super.viewDidLoad()let appDelegate = UIApplication.shared.delegate as! AppDelegate// 기본 진입점으로 엔진 생성engineOne = appDelegate.engineGroup.makeEngine(withEntrypoint: nil, libraryURI: nil)// 특정 진입점으로 엔진 생성engineTwo = appDelegate.engineGroup.makeEngine(withEntrypoint: "secondScreenMain",libraryURI: nil)}@IBAction func showFirstFlutterScreen(_ sender: Any) {let flutterVC = FlutterViewController(engine: engineOne, nibName: nil, bundle: nil)present(flutterVC, animated: true)}}Dart에서 다중 진입점 정의하기
lib/main.dart void main() => runApp(const MyApp(screen: 'home'));@pragma('vm:entry-point')void secondScreenMain() => runApp(const MyApp(screen: 'settings'));@pragma('vm:entry-point')void thirdScreenMain() => runApp(const MyApp(screen: 'profile'));class MyApp extends StatelessWidget {final String screen;const MyApp({super.key, required this.screen});@overrideWidget build(BuildContext context) {return MaterialApp(home: _buildScreen(),);}Widget _buildScreen() {switch (screen) {case 'settings':return const SettingsScreen();case 'profile':return const ProfileScreen();default:return const HomeScreen();}}}
@pragma('vm:entry-point')어노테이션은 해당 함수가 네이티브 코드에서 호출될 수 있음을 Dart 컴파일러에게 알립니다.
Watch out
WARNINGFlutterEngineGroup을 사용해도 각 엔진은 독립적인 Dart Isolate3를 가집니다.
화면 간 상태 공유가 필요하면 다음 방법을 사용하세요:
- 플랫폼 채널: 네이티브 코드를 통해 데이터 전달
- 공유 저장소: SQLite, SharedPreferences 등
- 네이티브 상태 관리: 네이티브 레이어에서 상태 유지
// 플랫폼 채널을 통한 데이터 공유 예시class DataChannel {static const platform = MethodChannel('com.example/data');static Future<void> setSharedData(String key, String value) async {await platform.invokeMethod('setData', {'key': key, 'value': value});}static Future<String?> getSharedData(String key) async {return await platform.invokeMethod('getData', {'key': key});}}또한 이 기능은 아직 실험적이며 웹 플랫폼에서는 지원되지 않습니다.
결론: FlutterEngineGroup으로 다중 Flutter 인스턴스의 메모리 오버헤드를 크게 줄일 수 있습니다.
챕터 2: Flutter 엔진 로딩 시퀀스 이해하기
Why
NOTEFlutter 화면이 표시되기까지 여러 단계의 초기화 과정이 있습니다. 이 시퀀스를 이해하면 어디에서 병목이 발생하는지 파악하고 최적화할 수 있습니다.
특히 앱 시작 시 첫 Flutter 화면이 느리게 표시되는 문제를 해결하는 데 도움이 됩니다.
What
NOTEFlutter 엔진 로딩은 크게 네 단계로 나뉩니다.
sequenceDiagram participant App as 앱 시작 participant Engine as FlutterEngine participant Dart as Dart VM participant UI as Flutter UI App->>Engine: 1. 엔진 초기화 Engine->>Dart: 2. Dart 코드 로딩 Dart->>Dart: 3. main() 실행 Dart->>UI: 4. 첫 프레임 렌더링
단계 동작 일반적 소요 시간 FlutterEngine 초기화 네이티브 라이브러리 로딩, GPU 설정 ~200ms Dart 코드 로딩 AOT 바이너리 또는 JIT 스냅샷 로딩 ~300ms main() 실행 Dart 코드 실행, 위젯 트리 구성 ~100ms 첫 프레임 렌더링 GPU에 픽셀 렌더링 ~16ms 총 약 600ms 이상이 소요될 수 있으며, 디바이스 성능에 따라 달라집니다.
How
TIP사전 워밍으로 로딩 시간 단축하기
앱 시작 시점에 Flutter 엔진을 미리 초기화하면 사용자가 Flutter 화면에 진입할 때 즉시 표시할 수 있습니다.
Android 사전 워밍
App.kt class App : Application() {lateinit var flutterEngine: FlutterEngineoverride fun onCreate() {super.onCreate()// 앱 시작 시 엔진 미리 초기화flutterEngine = FlutterEngine(this).apply {// Dart 코드 미리 실행dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())}// 엔진 캐시에 등록FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)}}// 나중에 Flutter 화면 표시class MainActivity : AppCompatActivity() {fun showFlutterScreen() {startActivity(FlutterActivity.withCachedEngine("my_engine_id").build(this))}}iOS 사전 워밍
AppDelegate.swift class AppDelegate: FlutterAppDelegate {lazy var flutterEngine = FlutterEngine(name: "my_engine")override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {// 엔진 사전 워밍flutterEngine.run()return super.application(application, didFinishLaunchingWithOptions: launchOptions)}}// 나중에 Flutter 화면 표시@IBAction func showFlutterScreen(_ sender: Any) {let appDelegate = UIApplication.shared.delegate as! AppDelegatelet flutterVC = FlutterViewController(engine: appDelegate.flutterEngine,nibName: nil,bundle: nil)present(flutterVC, animated: true)}초기 라우트 지정하기
// AndroidflutterEngine.navigationChannel.setInitialRoute("/settings")// iOSflutterEngine.navigationChannel.setInitialRoute("/settings")Dart에서 초기 라우트 처리
void main() {runApp(MaterialApp(initialRoute: window.defaultRouteName,routes: {'/': (context) => const HomeScreen(),'/settings': (context) => const SettingsScreen(),},),);}
Watch out
WARNING사전 워밍은 메모리와 CPU를 미리 사용합니다.
주의 사항:
- 메모리 트레이드오프: 사전 워밍된 엔진은 앱이 종료될 때까지 메모리를 차지합니다.
- 배터리 영향: 백그라운드에서 Dart 코드가 실행되면 배터리를 소모합니다.
- 적절한 타이밍: 사용자가 Flutter 화면을 높은 확률로 방문할 때만 사전 워밍하세요.
// 조건부 사전 워밍 예시class App : Application() {override fun onCreate() {super.onCreate()// 사용자 패턴에 따라 조건부 워밍if (userFrequentlyVisitsFlutterScreen()) {prewarmFlutterEngine()}}}
결론: 로딩 시퀀스를 이해하고 사전 워밍을 적절히 활용하면 Flutter 화면 진입 시간을 단축할 수 있습니다.
챕터 3: 메모리 관리 전략
Why
NOTE모바일 앱에서 메모리 관리는 사용자 경험에 직접적인 영향을 미칩니다. 메모리가 부족하면 시스템이 앱을 강제 종료하거나 UI가 버벅거립니다.
특히 add-to-app 환경에서는 네이티브 코드와 Flutter가 메모리를 공유하므로 더욱 주의가 필요합니다.
What
NOTEFlutter 엔진의 메모리 구성 요소는 다음과 같습니다.
구성 요소 메모리 사용량 설명 Dart VM ~40MB Dart 런타임 및 GC GPU 리소스 ~50MB 텍스처, 셰이더 캐시 Skia ~30MB 2D 그래픽 엔진 폰트 캐시 ~10MB 로드된 폰트 데이터 이미지 캐시 가변 디코딩된 이미지 위젯 트리 가변 UI 구조 데이터 pie title FlutterEngine 메모리 분포 (대략) "Dart VM" : 40 "GPU 리소스" : 50 "Skia" : 30 "폰트 캐시" : 10 "기타" : 20기본 엔진만으로 약 150~180MB를 사용하며, 앱 복잡도에 따라 증가합니다.
How
TIP메모리 최적화 전략
1. 이미지 캐시 관리
// 이미지 캐시 크기 제한class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {// 이미지 캐시를 100MB로 제한PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;return MaterialApp(home: const HomeScreen(),);}}// 화면 전환 시 캐시 정리@overridevoid dispose() {// 이 화면에서 사용한 이미지 캐시 정리imageCache.clear();super.dispose();}2. 대용량 리소스 해제
class HeavyScreen extends StatefulWidget {@override_HeavyScreenState createState() => _HeavyScreenState();}class _HeavyScreenState extends State<HeavyScreen> {ui.Image? _heavyImage;@overridevoid dispose() {// 대용량 이미지 명시적 해제_heavyImage?.dispose();_heavyImage = null;super.dispose();}}3. 메모리 압박 대응
class MyApp extends StatefulWidget {@override_MyAppState createState() => _MyAppState();}class _MyAppState extends State<MyApp> with WidgetsBindingObserver {@overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);}@overridevoid didHaveMemoryPressure() {// 시스템이 메모리 부족 신호를 보냄imageCache.clear();imageCache.clearLiveImages();// 불필요한 데이터 정리_clearCachedData();}@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);super.dispose();}}4. FlutterEngineGroup 활용
// 사용하지 않는 엔진 해제class MultiEngineManager(private val engineGroup: FlutterEngineGroup) {private val engines = mutableMapOf<String, FlutterEngine>()fun getEngine(id: String): FlutterEngine {return engines.getOrPut(id) {engineGroup.createAndRunDefaultEngine(context)}}fun releaseEngine(id: String) {engines.remove(id)?.destroy()}fun releaseAll() {engines.values.forEach { it.destroy() }engines.clear()}}
Watch out
WARNING메모리 최적화 시 다음 사항에 주의하세요.
과도한 캐시 정리 금지
// 잘못된 예: 매 프레임마다 캐시 정리@overrideWidget build(BuildContext context) {imageCache.clear(); // 성능 저하 유발return ...;}// 올바른 예: 적절한 시점에만 정리@overridevoid didHaveMemoryPressure() {imageCache.clear();}엔진 재사용 vs 생성 트레이드오프
전략 장점 단점 엔진 재사용 빠른 화면 전환 지속적인 메모리 사용 매번 생성 사용 시에만 메모리 사용 느린 화면 전환 하이브리드 균형 잡힌 성능 구현 복잡도 증가 사용 패턴에 따라 적절한 전략을 선택하세요.
결론: 적절한 메모리 관리 전략으로 add-to-app 환경에서도 안정적인 사용자 경험을 제공할 수 있습니다.
한계
다중 Flutter 인스턴스와 add-to-app 성능 최적화는 강력하지만 다음과 같은 제한이 있습니다.
- 실험적 기능: FlutterEngineGroup은 아직 안정화되지 않았으며 API가 변경될 수 있다.
- 플랫폼 제한: 웹 플랫폼에서는 다중 인스턴스를 지원하지 않는다.
- 상태 격리: 각 엔진의 Dart Isolate가 독립적이므로 직접적인 메모리 공유가 불가능하다.
- 복잡한 디버깅: 여러 엔진이 동시에 실행되면 디버깅이 어려워질 수 있다.
- 네이티브 통합 필요: 화면 간 통신을 위해 플랫폼 채널 등 네이티브 코드 작성이 필요하다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!