Flutter 튜토리얼 70편: 다중 Flutter 인스턴스와 성능

요약#

핵심 요지#

  • 문제 정의: 기존 네이티브 앱에 여러 Flutter 화면을 추가하면 각각의 FlutterEngine1이 메모리를 차지해 앱이 무거워진다.
  • 핵심 주장: FlutterEngineGroup2을 사용하면 여러 Flutter 인스턴스가 공통 리소스를 공유하여 메모리를 절약할 수 있다.
  • 주요 근거: 첫 번째 엔진만 전체 로딩 비용이 발생하고, 이후 엔진은 폰트와 GPU 컨텍스트 등을 재사용한다.
  • 실무 기준: 각 FlutterEngine은 독립적인 Dart Isolate를 가지므로 화면 간 상태 공유는 플랫폼 채널로 처리해야 한다.
  • 한계: 아직 실험적 기능이며, 웹 플랫폼에서는 지원되지 않는다.

문서가 설명하는 범위#

  • FlutterEngineGroup으로 다중 인스턴스 생성하기
  • Android와 iOS에서의 구현 방법
  • Flutter 엔진 로딩 시퀀스와 성능 최적화
  • 메모리 오버헤드 관리 전략

읽는 시간: 22분 | 난이도: 고급


참고 자료#


문제 상황#

기존 네이티브 앱에 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#

NOTE

FlutterEngineGroup은 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#

TIP

Android에서 FlutterEngineGroup 사용하기

// App.kt - Application 클래스
class App : Application() {
lateinit var engineGroup: FlutterEngineGroup
override 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 Flutter
import UIKit
@main
class 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});
@override
Widget 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#

WARNING

FlutterEngineGroup을 사용해도 각 엔진은 독립적인 Dart Isolate3를 가집니다.

화면 간 상태 공유가 필요하면 다음 방법을 사용하세요:

  1. 플랫폼 채널: 네이티브 코드를 통해 데이터 전달
  2. 공유 저장소: SQLite, SharedPreferences 등
  3. 네이티브 상태 관리: 네이티브 레이어에서 상태 유지
// 플랫폼 채널을 통한 데이터 공유 예시
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#

NOTE

Flutter 화면이 표시되기까지 여러 단계의 초기화 과정이 있습니다. 이 시퀀스를 이해하면 어디에서 병목이 발생하는지 파악하고 최적화할 수 있습니다.

특히 앱 시작 시 첫 Flutter 화면이 느리게 표시되는 문제를 해결하는 데 도움이 됩니다.

What#

NOTE

Flutter 엔진 로딩은 크게 네 단계로 나뉩니다.

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: FlutterEngine
override 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! AppDelegate
let flutterVC = FlutterViewController(
engine: appDelegate.flutterEngine,
nibName: nil,
bundle: nil
)
present(flutterVC, animated: true)
}

초기 라우트 지정하기

// Android
flutterEngine.navigationChannel.setInitialRoute("/settings")
// iOS
flutterEngine.navigationChannel.setInitialRoute("/settings")

Dart에서 초기 라우트 처리

void main() {
runApp(
MaterialApp(
initialRoute: window.defaultRouteName,
routes: {
'/': (context) => const HomeScreen(),
'/settings': (context) => const SettingsScreen(),
},
),
);
}

Watch out#

WARNING

사전 워밍은 메모리와 CPU를 미리 사용합니다.

주의 사항:

  1. 메모리 트레이드오프: 사전 워밍된 엔진은 앱이 종료될 때까지 메모리를 차지합니다.
  2. 배터리 영향: 백그라운드에서 Dart 코드가 실행되면 배터리를 소모합니다.
  3. 적절한 타이밍: 사용자가 Flutter 화면을 높은 확률로 방문할 때만 사전 워밍하세요.
// 조건부 사전 워밍 예시
class App : Application() {
override fun onCreate() {
super.onCreate()
// 사용자 패턴에 따라 조건부 워밍
if (userFrequentlyVisitsFlutterScreen()) {
prewarmFlutterEngine()
}
}
}

결론: 로딩 시퀀스를 이해하고 사전 워밍을 적절히 활용하면 Flutter 화면 진입 시간을 단축할 수 있습니다.


챕터 3: 메모리 관리 전략#

Why#

NOTE

모바일 앱에서 메모리 관리는 사용자 경험에 직접적인 영향을 미칩니다. 메모리가 부족하면 시스템이 앱을 강제 종료하거나 UI가 버벅거립니다.

특히 add-to-app 환경에서는 네이티브 코드와 Flutter가 메모리를 공유하므로 더욱 주의가 필요합니다.

What#

NOTE

Flutter 엔진의 메모리 구성 요소는 다음과 같습니다.

구성 요소메모리 사용량설명
Dart VM~40MBDart 런타임 및 GC
GPU 리소스~50MB텍스처, 셰이더 캐시
Skia~30MB2D 그래픽 엔진
폰트 캐시~10MB로드된 폰트 데이터
이미지 캐시가변디코딩된 이미지
위젯 트리가변UI 구조 데이터
pie title FlutterEngine 메모리 분포 (대략) "Dart VM" : 40 "GPU 리소스" : 50 "Skia" : 30 "폰트 캐시" : 10 "기타" : 20

기본 엔진만으로 약 150~180MB를 사용하며, 앱 복잡도에 따라 증가합니다.

How#

TIP

메모리 최적화 전략

1. 이미지 캐시 관리

// 이미지 캐시 크기 제한
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 이미지 캐시를 100MB로 제한
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
return MaterialApp(
home: const HomeScreen(),
);
}
}
// 화면 전환 시 캐시 정리
@override
void dispose() {
// 이 화면에서 사용한 이미지 캐시 정리
imageCache.clear();
super.dispose();
}

2. 대용량 리소스 해제

class HeavyScreen extends StatefulWidget {
@override
_HeavyScreenState createState() => _HeavyScreenState();
}
class _HeavyScreenState extends State<HeavyScreen> {
ui.Image? _heavyImage;
@override
void dispose() {
// 대용량 이미지 명시적 해제
_heavyImage?.dispose();
_heavyImage = null;
super.dispose();
}
}

3. 메모리 압박 대응

class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didHaveMemoryPressure() {
// 시스템이 메모리 부족 신호를 보냄
imageCache.clear();
imageCache.clearLiveImages();
// 불필요한 데이터 정리
_clearCachedData();
}
@override
void 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

메모리 최적화 시 다음 사항에 주의하세요.

과도한 캐시 정리 금지

// 잘못된 예: 매 프레임마다 캐시 정리
@override
Widget build(BuildContext context) {
imageCache.clear(); // 성능 저하 유발
return ...;
}
// 올바른 예: 적절한 시점에만 정리
@override
void didHaveMemoryPressure() {
imageCache.clear();
}

엔진 재사용 vs 생성 트레이드오프

전략장점단점
엔진 재사용빠른 화면 전환지속적인 메모리 사용
매번 생성사용 시에만 메모리 사용느린 화면 전환
하이브리드균형 잡힌 성능구현 복잡도 증가

사용 패턴에 따라 적절한 전략을 선택하세요.

결론: 적절한 메모리 관리 전략으로 add-to-app 환경에서도 안정적인 사용자 경험을 제공할 수 있습니다.


한계#

다중 Flutter 인스턴스와 add-to-app 성능 최적화는 강력하지만 다음과 같은 제한이 있습니다.

  • 실험적 기능: FlutterEngineGroup은 아직 안정화되지 않았으며 API가 변경될 수 있다.
  • 플랫폼 제한: 웹 플랫폼에서는 다중 인스턴스를 지원하지 않는다.
  • 상태 격리: 각 엔진의 Dart Isolate가 독립적이므로 직접적인 메모리 공유가 불가능하다.
  • 복잡한 디버깅: 여러 엔진이 동시에 실행되면 디버깅이 어려워질 수 있다.
  • 네이티브 통합 필요: 화면 간 통신을 위해 플랫폼 채널 등 네이티브 코드 작성이 필요하다.

Footnotes#

  1. FlutterEngine(플러터 엔진): Flutter 앱을 실행하는 핵심 런타임이다. Dart VM, 그래픽 렌더링, 플랫폼 채널 등을 포함한다.

  2. FlutterEngineGroup(플러터 엔진 그룹): 여러 FlutterEngine이 공통 리소스를 공유할 수 있게 해주는 팩토리 클래스다. 메모리 효율성을 높인다.

  3. Dart Isolate(다트 아이솔레이트): Dart의 동시성 단위로, 각각 독립적인 메모리 힙을 가진다. 멀티스레딩과 유사하지만 메모리를 공유하지 않는다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 70편: 다중 Flutter 인스턴스와 성능
https://moodturnpost.net/posts/flutter/flutter-add-to-app-advanced/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차