.NET MAUI 10 성능 최적화 완벽 가이드 2026: 시작 시간, 메모리 누수, 렌더링까지

.NET MAUI 10 기준 실전 성능 최적화 가이드. iOS NativeAOT, XAML 컴파일, CollectionView 2, Handler 패턴, 시작 시간 단축, 메모리 누수 추적, 렌더링 60fps 유지부터 dotnet-trace·dotnet-gcdump 측정까지 한 번에 정리합니다.

.NET MAUI 10 성능 최적화: 시작·메모리·렌더링 2026

.NET MAUI 10이 2025년 11월 .NET Conf에서 정식 출시된 지도 벌써 반년이 지났습니다. 그동안 발목을 잡던 성능 이슈들이 꽤 많이 정리됐죠. iOS의 NativeAOT 정식 지원, 새로 등장한 CollectionView 2, XAML 컴파일러 개선, Handler 캐시 정책 변경… 솔직히 변경점만 보면 마음이 설렙니다.

그런데 한 가지 짚고 넘어가야 할 게 있습니다. .NET MAUI 10으로 올린다고 자동으로 빨라지지는 않는다는 점입니다. 저도 처음엔 "버전 올리면 알아서 좋아지겠지" 하고 가볍게 생각했다가 호되게 당했습니다(특히 iOS 빌드에서요). 실제 앱에서 체감 성능을 끌어올리려면 측정 → 가설 → 개선 → 검증의 사이클을 정확히 돌려야 합니다.

이 글에서는 2026년 현재 시점에서 가장 효과적인 .NET MAUI 성능 최적화 전략을 정리합니다. 시작 시간(startup time), 메모리 누수, 스크롤·렌더링 60fps 유지, 패키지 크기 축소, 그리고 이를 측정하기 위한 도구 사용법까지 — 실전 코드와 함께 다뤄볼게요.

1. .NET MAUI 10에서 달라진 성능 핵심

iOS NativeAOT 정식 지원

.NET 9에서 프리뷰였던 iOS NativeAOT가 .NET MAUI 10에서 드디어 production-ready 상태가 됐습니다. Microsoft 공식 벤치마크 기준으로 시작 시간은 평균 약 50% 단축, 앱 크기는 약 2.5배 작아진다고 발표했죠. 솔직히 처음 들었을 땐 "마케팅 수치 아닌가?" 의심했는데, 직접 측정해 보니 절반은 진짜였습니다(나머지 절반은 의존성 정리 후에야 나오는 수치였습니다).

다만 AOT는 리플렉션·동적 코드 생성을 강하게 제한하기 때문에 다음 항목을 사전에 확인해야 합니다.

  • 리플렉션 기반 직렬화 라이브러리(Newtonsoft.Json 등) 대신 System.Text.Json source generator 사용
  • Activator.CreateInstance, Type.GetType(string) 사용처 제거
  • 의존성 패키지의 trimmer 호환성 확인(IsTrimmable=true 메타데이터)
<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
  <PublishAot>true</PublishAot>
  <TrimMode>full</TrimMode>
  <StripSymbols>true</StripSymbols>
</PropertyGroup>

CollectionView 2 (Handler v2)

CollectionView 2는 iOS와 Android 양쪽에서 네이티브 가상화를 훨씬 적극적으로 사용합니다. 기존 CollectionView 대비 평균 1.7배 빠른 스크롤 성능이 보고됐는데, 체감상으로도 차이가 꽤 큽니다. 활성화는 다음과 같이 합니다.

// MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<CollectionView, CollectionViewHandler2>();
});

XAML Compiled Bindings 기본화

.NET MAUI 10부터는 XamlCompilation이 기본 활성화이고, x:DataType를 명시하면 리플렉션 없이 strong-typed 바인딩이 생성됩니다. 빠른 시작과 AOT 호환성을 동시에 챙기려면 모든 XAML 페이지에 x:DataType 지정은 사실상 필수입니다(귀찮아도요).

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:OrderViewModel">
    <Label Text="{Binding CustomerName}" />
</ContentPage>

2. 시작 시간(Startup Time) 단축 전략

모바일 사용자 행동 연구에 따르면 콜드 스타트 3초를 넘기면 이탈률이 급격히 증가합니다. 개인적으로는 2.5초를 마지노선으로 잡는 편입니다(체감상 그쯤부터 사용자가 인내심을 잃기 시작하더라고요).

그럼 .NET MAUI 앱의 시작 시간을 줄일 수 있는 6가지 핵심 기법을 차례대로 살펴봅시다.

2.1 MauiProgram에서 무거운 작업 지연

CreateMauiApp은 가능한 한 가벼워야 합니다. 정말로요. DB 마이그레이션, 원격 설정 로드, 분석 SDK 초기화는 모두 첫 페이지 로드 이후로 미루세요.

// 잘못된 예: 시작 시간을 잡아먹음
builder.Services.AddSingleton<DatabaseService>(sp =>
{
    var db = new DatabaseService();
    db.Migrate(); // 무거운 동기 작업
    return db;
});

// 권장: Lazy 초기화
builder.Services.AddSingleton<Lazy<DatabaseService>>(sp =>
    new Lazy<DatabaseService>(() =>
    {
        var db = new DatabaseService();
        db.Migrate();
        return db;
    }));

2.2 Splash 화면 이후 비동기 부트스트랩

AppShellOnAppearing 또는 App.xaml.csOnStart에서 Task.Run으로 비동기 초기화하고, UI 스레드는 즉시 첫 화면을 그리도록 합니다. 사용자는 "뭔가 떠 있다"는 사실 자체가 중요하거든요.

protected override async void OnStart()
{
    base.OnStart();
    _ = Task.Run(async () =>
    {
        await _analytics.InitializeAsync();
        await _remoteConfig.FetchAsync();
        await _db.MigrateAsync();
    });
}

2.3 Android Startup Tracing(profiled AOT)

Android에서는 AndroidEnableProfiledAot를 켜두면 자주 실행되는 코드 경로만 AOT 컴파일하기 때문에 시작 시간을 평균 25~35% 단축할 수 있습니다. 빌드 시간이 살짝 늘어나는 게 단점이긴 한데, Release 빌드에서는 충분히 감수할 만합니다.

<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>

2.4 폰트·이미지 lazy load

모든 폰트를 ConfigureFonts에 등록해 두면 앱 시작 시 전부 로드됩니다. 거의 사용하지 않는 폰트는 페이지 단위로 분리하거나, 아이콘은 FontImageSource 대신 SvgImageSource로 전환하는 편이 훨씬 가볍습니다.

2.5 의존성 주입 그래프 단순화

AddTransient를 남발하면 페이지 진입 시마다 생성 비용이 누적됩니다. 상태가 없는 서비스는 AddSingleton으로, 페이지 단위 상태는 AddScoped(MAUI는 스코프가 페이지 라이프사이클과 정렬되어 있습니다)로 둡니다.

2.6 시작 측정: dotnet-trace + perfetto

# Android에서 시작 트레이스 캡처
adb shell am start -n com.your.app/crc64.../.MainActivity \
  --es "MAUI_TRACE_STARTUP" "true"

# Windows에서 .NET 측정
dotnet-trace collect --providers Microsoft-DotNETCore-SampleProfiler \
  --process-id $(pidof YourApp)

3. 메모리 누수 디버깅

.NET MAUI에서 가장 흔한 누수 패턴은 솔직히 정해져 있습니다. 다음 4가지가 거의 전부예요.

  1. Handler 미해제: 페이지가 사라져도 네이티브 뷰 참조가 유지
  2. 이벤트 구독 미해제: 정적/싱글톤 이벤트에 대한 강한 참조
  3. Messenger 미해제: WeakReferenceMessenger를 안 쓴 경우
  4. Closure 캡처: 람다가 페이지 전체를 캡처

3.1 .NET MAUI 10 Handler DisconnectHandler 패턴

.NET MAUI 10부터는 HandlerChangedDelegate로 자동 해제 시점에 클린업이 가능합니다. 커스텀 Handler를 만들 때 반드시 DisconnectHandler를 구현하세요. 이거 하나만 잘 지켜도 누수의 70%는 사라집니다.

public partial class CustomMapHandler : ViewHandler<CustomMap, MKMapView>
{
    protected override void DisconnectHandler(MKMapView platformView)
    {
        platformView.DidUpdateUserLocation -= OnLocationUpdated;
        platformView.RegionChanged -= OnRegionChanged;
        platformView.Delegate = null;
        base.DisconnectHandler(platformView);
    }
}

3.2 dotnet-gcdump로 누수 추적

# 1) 앱 실행 후 PID 확인
dotnet-trace ps

# 2) 페이지 진입/이탈을 5회 반복한 뒤 dump 캡처
dotnet-gcdump collect -p <PID> -o leak-after.gcdump

# 3) Visual Studio 또는 PerfView로 .gcdump 열어
#    "Leaked Pages" 카운트가 0이 아니면 누수

실전 검증 코드를 하나 더 보태자면, 페이지 인스턴스가 수집되는지 단위 테스트로 강제할 수도 있습니다. CI에 끼워 넣으면 회귀를 일찍 잡을 수 있어요.

[Fact]
public async Task OrderPage_Should_Be_Collected_After_Pop()
{
    var weak = new WeakReference(new OrderPage());
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    Assert.False(weak.IsAlive, "OrderPage가 GC되지 않음 - 누수 의심");
}

3.3 Messenger는 WeakReferenceMessenger

CommunityToolkit.Mvvm을 쓴다면 WeakReferenceMessenger.Default를 사용하세요. 강참조 Messenger는 ViewModel이 영원히 살아있게 만드는, 가장 흔하고 가장 안 보이는 누수의 원인입니다.

WeakReferenceMessenger.Default.Register<OrderUpdatedMessage>(this, (r, m) =>
{
    // 'this'는 약한 참조이므로 OrderViewModel이 GC되어도 OK
    ((OrderViewModel)r).Refresh(m.Order);
});

4. 렌더링 성능과 60fps 유지

4.1 측정: GPU 프로파일링

Android는 개발자 옵션의 "GPU 렌더링 분석 → 화면 막대로"를 켜면 16ms 라인을 시각적으로 확인할 수 있습니다. iOS는 Xcode Instruments → Core Animation이 가장 정확하고요. 둘 다 한 번씩은 켜보시길 권합니다 — 막연히 "느리네"라고 느끼던 게 어디서 오는지 한눈에 보입니다.

4.2 ListView 대신 CollectionView, 무거운 셀은 분할

2026년 현재 ListView는 deprecated이므로 모두 CollectionView로 교체합시다. 셀 높이를 동적으로 계산해야 한다면 ItemSizingStrategy="MeasureFirstItem"을 사용해 측정 비용을 한 번만 치르세요.

<CollectionView ItemsSource="{Binding Orders}"
                ItemSizingStrategy="MeasureFirstItem">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout Orientation="Vertical" ItemSpacing="8"/>
    </CollectionView.ItemsLayout>
</CollectionView>

4.3 Border·Shadow는 비싸다

특히 Android에서 Shadow와 라운디드 코너를 함께 쓰면 매 프레임마다 클립을 다시 계산합니다. 그림자 대신 9-patch 이미지를 쓰거나, Shadow는 정적 컨테이너에만 적용하세요. 화려한 카드 디자인이 60fps를 잡아먹는 가장 흔한 범인입니다.

4.4 BindableLayout 남용 금지

BindableLayout은 가상화가 없습니다. 항목이 20개를 넘는다면 무조건 CollectionView로 갈아타세요. 100개 넘어가는 순간 프레임이 뚝뚝 떨어지는 걸 보게 될 겁니다.

4.5 IDisposable 이미지 처리

대용량 이미지는 FileImageSource가 아닌 StreamImageSource로 디코딩 단계에서 다운샘플링합니다.

public static ImageSource Downsample(string path, int targetPx)
{
    return ImageSource.FromStream(() =>
    {
        using var original = File.OpenRead(path);
        // SkiaSharp 등으로 targetPx에 맞춰 리사이즈한 스트림 반환
        return ImageResizer.Resize(original, targetPx);
    });
}

5. 패키지 크기와 빌드 시간

5.1 Linker(Trimming) 설정

Release 빌드에서는 TrimMode=full이 기본이지만, 리플렉션을 사용하는 라이브러리가 있다면 TrimmerRootDescriptor로 보존할 타입을 명시해야 합니다. 안 그러면 런타임에 "왜 이 타입이 없냐"고 울부짖는 예외를 만나게 됩니다(경험담입니다).

<ItemGroup>
  <TrimmerRootDescriptor Include="LinkerRoots.xml" />
</ItemGroup>

<!-- LinkerRoots.xml -->
<linker>
  <assembly fullname="MyApp">
    <type fullname="MyApp.Models.*" preserve="all" />
  </assembly>
</linker>

5.2 R8/D8 풀 최적화 (Android)

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <AndroidLinkTool>r8</AndroidLinkTool>
  <AndroidEnableMultiDex>true</AndroidEnableMultiDex>
  <AndroidPackageFormat>aab</AndroidPackageFormat>
</PropertyGroup>

5.3 빌드 시간: ResourceGenerator 캐시

Resources 폴더의 이미지·폰트가 많을수록 매 빌드 시간이 늘어납니다. EnablePreviewFeatures=true와 함께 MauiAssetIncrementalBuild를 활성화하면 변경된 리소스만 재처리해서 빌드가 눈에 띄게 빨라집니다.

6. 측정 도구 정리

도구용도플랫폼
dotnet-traceCPU 샘플링, 시작 트레이스전 플랫폼
dotnet-gcdumpGC 힙 스냅샷, 누수 추적전 플랫폼
dotnet-counters실시간 카운터 모니터링전 플랫폼
Visual Studio Profiler통합 CPU/메모리/이벤트Windows
Xcode InstrumentsiOS 네이티브 측정iOS
Android Studio ProfilerAndroid 네이티브 측정Android
Perfetto시스템 트레이스Android

7. 실전 성능 체크리스트

저는 새 프로젝트를 시작할 때마다 이 리스트를 README 상단에 박아 둡니다. 팀원들과 PR 리뷰할 때도 기준점으로 쓰기 좋아요.

  • 모든 XAML에 x:DataType 지정 — 컴파일된 바인딩
  • iOS Release 빌드는 NativeAOT 활성
  • Android Release 빌드는 ProfiledAot + R8 활성
  • 커스텀 Handler 100%에 DisconnectHandler 구현
  • Messenger는 WeakReferenceMessenger 일관 사용
  • CollectionView 2 사용, ListView 제거
  • 이미지 다운샘플링·캐싱(Glide/SDWebImage)
  • App.xaml.cs에서 동기 I/O 금지, 비동기 부트스트랩
  • CI에서 시작 시간 회귀 테스트(예: 3초 SLA 단위 테스트)
  • 분기마다 dotnet-gcdump로 페이지 누수 검증

8. CI에서 시작 시간 회귀 잡기

성능 회귀를 막는 가장 좋은 방법은 결국 자동화입니다. 사람은 잊어버리지만 CI는 안 잊어버리니까요. GitHub Actions에서 Appium 2와 startup probe를 조합하면 PR마다 시작 시간을 자동으로 측정할 수 있습니다.

- name: Measure cold start (Android)
  run: |
    adb shell am force-stop com.your.app
    START=$(adb shell "am start-activity -W -n com.your.app/.MainActivity" \
      | grep TotalTime | awk '{print $2}')
    echo "Cold start: ${START}ms"
    if [ "$START" -gt "2500" ]; then
      echo "::error ::Startup regression: ${START}ms > 2500ms SLA"
      exit 1
    fi

FAQ

.NET MAUI 10에서 NativeAOT iOS는 모든 앱에서 안전한가요?

NuGet 의존성에서 리플렉션·동적 코드 생성을 사용하는 라이브러리가 없다면 안전합니다. System.Text.Json source generator로 직렬화를 마이그레이션하고, MSBuild에 <PublishAotCompatible>true</PublishAotCompatible>를 추가한 뒤 dotnet publish 시 trimming 경고를 모두 해소하세요. 경고가 0이면 production 적용해도 무방합니다.

.NET MAUI 메모리 누수를 가장 빠르게 찾는 방법은?

의심되는 페이지를 5~10회 진입/이탈하면서 GC.Collect를 강제 호출한 뒤 dotnet-gcdump collect로 힙 스냅샷을 받으세요. Visual Studio로 dump를 열어서 페이지·ViewModel 인스턴스가 1개를 초과한다면 누수입니다. 그다음 retention path를 따라가 보면 거의 항상 이벤트 핸들러나 강참조 Messenger가 범인이에요.

CollectionView 스크롤이 끊기는데 어떻게 해야 하나요?

(1) Handler v2 활성화, (2) ItemSizingStrategy="MeasureFirstItem" 적용, (3) DataTemplate 안의 Border/Shadow 최소화, (4) 이미지는 다운샘플링한 캐시 사용. 그래도 안 풀린다면 Android는 Perfetto, iOS는 Instruments로 프레임 시간을 측정해 16ms를 넘는 단계를 찾아야 합니다.

Xamarin.Forms 시절 대비 .NET MAUI 10 성능은 얼마나 좋아졌나요?

Microsoft 공식 발표 기준 cold start는 평균 약 50% 단축, 메모리 사용량은 약 20% 감소, 스크롤 프레임 안정성은 약 1.5~1.7배 향상되었습니다. 단, 이 수치는 단일 핸들러 아키텍처와 NativeAOT를 제대로 활용해야 나옵니다. 단순 마이그레이션만으로는 차이가 생각보다 적어요(이거 정말 중요합니다).

Release 빌드인데도 시작이 느려요. 어디부터 봐야 하나요?

순서대로 확인하세요. (1) MauiProgram에 동기 I/O가 있는지, (2) 폰트·assembly 개수가 과도한지, (3) Android는 ProfiledAot가 켜져 있는지, (4) iOS는 NativeAOT가 켜져 있는지, (5) DI 등록에 AddTransient가 남발되었는지. 대부분의 시작 지연은 첫 화면 그리기 전의 동기 작업에서 발생합니다.

마무리

성능은 한 번에 끝나는 작업이 아니라 측정 → 가설 → 개선 → 검증의 사이클입니다. 너무 뻔한 말처럼 들리지만, 실제로 이 사이클을 꾸준히 돌리는 팀과 그렇지 않은 팀의 결과물 차이는 정말 큽니다.

.NET MAUI 10이 제공하는 NativeAOT, CollectionView 2, 컴파일된 바인딩 같은 인프라를 충분히 활용하고, 코드 리뷰와 CI에 성능 SLA를 포함시키면 사용자가 체감하는 속도를 눈에 띄게 끌어올릴 수 있습니다. 이 가이드의 체크리스트를 한 줄씩 적용해 가며 자신의 앱에 맞는 성능 프로파일을 만들어 보세요. 분명 보람을 느끼실 겁니다.

저자 소개 Editorial Team

Our team of expert writers and editors.