.NET MAUI Blazor Hybrid 완벽 가이드 2026: BlazorWebView부터 네이티브 연동, 성능 최적화까지

BlazorWebView로 Razor 컴포넌트를 네이티브 앱에 내장해 iOS/Android/Windows/macOS에서 동일한 UI를 돌리는 .NET 10 하이브리드 가이드. 인터롭, 성능 병목, 상태 관리, 웹과의 코드 공유까지 실전 패턴 정리.

.NET MAUI Blazor Hybrid 가이드 2026

업데이트: 2026년 6월 6일

.NET MAUI Blazor Hybrid는 BlazorWebView 컨트롤로 Razor 컴포넌트를 네이티브 앱 안에 내장해 iOS, Android, Windows, macOS에서 동일한 UI를 실행하는 하이브리드 앱 모델입니다. WebAssembly 없이 컴포넌트가 .NET 런타임 위에서 직접 실행되고, 임베디드 WebView가 렌더링만 담당합니다. 결과적으로 웹 자산 다운로드가 사라지고 네이티브 API에 풀 액세스가 가능합니다. 다만 단순한 구조 뒤에는 JS bridge, 인터롭 채널, 상태 관리, AOT 같은 결정적인 성능 변수가 숨어 있어 설계가 곧 성능을 좌우합니다.

  • 실행 위치: Razor 컴포넌트는 브라우저가 아니라 디바이스의 .NET 런타임에서 실행되고, WebView는 DOM 렌더링만 담당합니다. WebAssembly가 아닙니다.
  • 네이티브 연동: BlazorWebView.TryDispatchAsync와 의존성 주입을 통해 Razor 컴포넌트에서 카메라, 지오로케이션, 보안 저장소 같은 MAUI Essentials API에 접근합니다.
  • 성능 병목: 대부분의 병목은 MAUI 레이어와 Blazor UI 사이의 비효율적인 인터롭에서 발생합니다. 동기 JS 호출을 줄이는 것이 첫 번째 최적화입니다.
  • .NET 10 변경: XAML 소스 생성기, NativeAOT, iOS/Mac Catalyst 기본 핸들러 교체로 시작 시간이 .NET 8 대비 평균 30% 이상 단축됐습니다.
  • UI 선택: 긴 리스트는 HTML 테이블이 아니라 네이티브 CollectionView를, 폼·대시보드·CMS 화면은 Razor 컴포넌트를 쓰는 하이브리드 분기가 정석입니다.
  • 코드 재사용: Razor Class Library(RCL)로 Blazor Web App과 MAUI Blazor Hybrid가 동일한 컴포넌트를 공유합니다. 진정한 의미의 1 코드베이스 → 5 플랫폼이 가능합니다.

.NET MAUI Blazor Hybrid란 무엇인가

Blazor Hybrid는 Razor 컴포넌트를 네이티브 앱 안에 내장된 WebView에서 렌더링하지만, 컴포넌트의 C# 코드는 디바이스의 .NET 런타임에서 직접 실행하는 패턴입니다. 즉, 브라우저 샌드박스나 WebAssembly가 끼어들지 않습니다. WebView는 결과 DOM만 받아 표시하고, 모든 이벤트와 상태 변화는 로컬 인터롭 채널로 즉시 전달됩니다.

이 구조의 가장 큰 가치는 단순합니다. Razor로 만든 동일한 UI를 안드로이드, iOS, Windows, macOS에서 그대로 돌리면서, 동시에 SecureStorage, Geolocation, Camera 같은 MAUI Essentials API에 풀 액세스할 수 있다는 점입니다. PWA가 풀 수 없는 권한 모델 문제도 사라집니다.

저는 지난 18개월 동안 사내 영업 도구, B2B 키오스크 앱, 그리고 SaaS 어드민 앱 세 개를 Blazor Hybrid로 출시했습니다. 공통점이 하나 있는데, 모두 웹 버전이 먼저 존재했고 모바일이 그 다음에 필요해진 케이스였습니다. Blazor Hybrid는 이런 시나리오에서 압도적으로 강력합니다. 반대로 처음부터 모바일 네이티브 UX가 핵심인 앱(소셜, 게임, 카메라 중심 앱)에서는 XAML 기반 .NET MAUI가 여전히 정답입니다.

BlazorWebView의 동작 원리

핵심 컨트롤은 Microsoft.AspNetCore.Components.WebView.Maui 네임스페이스의 BlazorWebView입니다. 내부적으로 각 플랫폼의 시스템 WebView를 감싸는데, Windows는 WebView2(Edge Chromium), Android는 WebView, iOS/macOS는 WKWebView를 사용합니다. 동일한 Razor 코드가 세 가지 다른 렌더링 엔진 위에서 실행된다는 뜻이고, 이것은 곧 호환성 테스트의 진실이기도 합니다.

렌더링 파이프라인은 이렇게 구성됩니다. 첫째, .NET 런타임이 Razor 컴포넌트를 실행해 DOM 변경 사항을 계산합니다. 둘째, 변경 사항이 직렬화돼 로컬 인터롭 채널을 통해 WebView에 전달됩니다. 셋째, JavaScript 측 Blazor 런타임이 실제 DOM에 패치를 적용합니다. 이 마지막 단계가 바로 JS bridge이며, 거의 모든 성능 문제가 여기서 시작됩니다.

주의해야 할 이벤트 두 개를 알아두면 좋습니다. BlazorWebViewInitializing은 WebView가 만들어지기 전에 발생해 플랫폼별 설정(예: WKWebView의 WKWebViewConfiguration)을 주입할 수 있고, BlazorWebViewInitialized는 생성된 후 호출돼 WebView 인스턴스에 직접 접근할 수 있게 해줍니다. 안드로이드에서 mixed content를 허용하거나, iOS에서 inspectable WebView를 켜는 작업은 모두 여기서 처리합니다. 자세한 옵션은 Microsoft Learn의 BlazorWebView 문서에 정리돼 있습니다.

.NET 10 프로젝트 생성과 구조

비주얼 스튜디오 17.13 이상 또는 dotnet CLI 10.0.100+에서 다음 명령으로 시작합니다. 워크로드는 미리 설치돼 있어야 합니다.

dotnet workload install maui
dotnet new maui-blazor -n ContosoApp -f net10.0
cd ContosoApp
dotnet build -t:Run -f net10.0-android

생성된 프로젝트의 핵심 진입점은 MauiProgram.cs입니다. 여기서 AddMauiBlazorWebView()를 호출해 Blazor 서비스를 DI 컨테이너에 등록합니다. 그리고 MainPage.xaml에는 <BlazorWebView>가 단 한 줄로 들어가는데, 이게 사실상 앱의 진짜 루트입니다.

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureFonts(f => f.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"));

    builder.Services.AddMauiBlazorWebView();

#if DEBUG
    builder.Services.AddBlazorWebViewDeveloperTools();
    builder.Logging.AddDebug();
#endif

    // 앱 전반에 걸쳐 사용할 서비스 등록
    builder.Services.AddSingleton<IDeviceInfoService, DeviceInfoService>();
    builder.Services.AddScoped<ICartState, CartState>();

    return builder.Build();
}

XAML 쪽은 더 단순합니다. HostPage가 시작 HTML이고, RootComponent가 마운트할 Razor 컴포넌트입니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
             xmlns:local="clr-namespace:ContosoApp"
             x:Class="ContosoApp.MainPage">
    <b:BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
        <b:BlazorWebView.RootComponents>
            <b:RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
        </b:BlazorWebView.RootComponents>
    </b:BlazorWebView>
</ContentPage>

이 구조의 함의는 분명합니다. 라우팅, 레이아웃, 페이지 컴포넌트 모두 Razor 쪽에서 처리되고, MAUI는 호스트 셸 역할만 합니다. 이것이 다중 페이지 XAML 구조를 가진 일반 .NET MAUI 앱과 가장 다른 점입니다. .NET MAUI 10 성능 최적화 가이드에서 다룬 시작 시간 튜닝 기법은 Blazor Hybrid에도 그대로 적용되지만, WebView 초기화 비용이 추가된다는 점을 항상 염두에 두어야 합니다.

Razor 컴포넌트에서 네이티브 기능 호출하기

Blazor Hybrid의 진짜 매력은 Razor 컴포넌트가 풀 .NET 런타임에서 실행된다는 점입니다. 즉, JS 인터롭 없이 C# 코드만으로 카메라, GPS, 생체 인증, 파일 시스템에 접근할 수 있습니다. MAUI Essentials를 그냥 import해서 쓰면 끝입니다.

@page "/scan"
@inject IPhotoService Photo
@inject IGeolocation Geo

<button class="btn btn-primary" @onclick="CapturePhotoAsync">
    사진 찍기
</button>

@if (capturedImage is not null)
{
    <img src="@capturedImage" alt="captured" />
    <p>위치: @lat, @lng</p>
}

@code {
    private string? capturedImage;
    private double lat, lng;

    private async Task CapturePhotoAsync()
    {
        // MAUI Essentials API를 직접 호출
        var photo = await MediaPicker.Default.CapturePhotoAsync();
        if (photo is null) return;

        // 캐시 폴더로 복사
        var path = Path.Combine(FileSystem.CacheDirectory, photo.FileName);
        using (var src = await photo.OpenReadAsync())
        using (var dst = File.OpenWrite(path))
            await src.CopyToAsync(dst);

        capturedImage = $"file://{path}";

        // 위치 함께 캡처
        var location = await Geo.GetLastKnownLocationAsync();
        if (location is not null)
        {
            lat = location.Latitude;
            lng = location.Longitude;
        }
    }
}

역방향, 즉 MAUI 코드(예: 메뉴 핸들러)에서 Razor의 scoped 서비스를 호출해야 할 때는 TryDispatchAsync를 씁니다. 이게 없으면 카트 상태를 업데이트하거나 Razor 측 NavigationManager를 사용할 수 없습니다.

// 어디서든: MAUI 페이지의 코드 비하인드 또는 메뉴 액션
await blazorWebView.TryDispatchAsync(services =>
{
    var nav = services.GetRequiredService<NavigationManager>();
    nav.NavigateTo("/checkout", forceLoad: false);
});

성능 병목과 해결 패턴

대부분의 Blazor Hybrid 성능 문제는 다음 다섯 가지 패턴 중 하나에 해당합니다. 저는 프로파일링한 모든 프로젝트에서 적어도 두 개 이상이 동시에 일어나는 것을 봤습니다.

1) 과도한 리렌더링

Razor의 기본 변경 감지는 부모가 변하면 자식 전부를 다시 평가합니다. 큰 폼이나 대시보드에서는 이게 곧 수백 번의 diff 계산을 의미합니다. 해결책은 두 가지입니다. 첫째, 변하지 않는 영역을 ChildContent를 사용하지 않는 별도 컴포넌트로 분리합니다. 둘째, ShouldRender()를 오버라이드해 명시적으로 렌더링을 건너뜁니다.

protected override bool ShouldRender()
{
    // 가격이 바뀔 때만 다시 그린다
    return _lastPrice != Item.Price;
}

2) 동기 JS 인터롭

IJSInProcessRuntime가 같은 프로세스에서 빠르긴 하지만, 한 프레임 안에 수십 번 호출되면 메인 스레드를 막습니다. 가능하면 IJSRuntime.InvokeVoidAsync로 배치하고, 결과가 필요 없는 작업은 fire-and-forget으로 보냅니다.

3) 큰 리스트를 HTML로 그리기

1,000개 이상의 행을 <table>이나 @foreach로 그리면 WebView가 무릎을 꿇습니다. 이 경우 정공법은 그 화면만 네이티브 CollectionView로 빠지는 것입니다. BlazorWebView를 모달 위에 띄우고, 긴 리스트 페이지는 일반 ContentPage로 보여주는 하이브리드 라우팅이 정답입니다.

4) AOT와 트리밍을 끄고 출시하기

릴리스 빌드에서 <RunAOTCompilation><PublishTrimmed>를 켜지 않으면 시작 시간이 두 배 이상으로 늘어납니다. iOS는 NativeAOT, Android는 ProfiledAOT를 권장합니다.

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <RunAOTCompilation>true</RunAOTCompilation>
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>link</TrimMode>
</PropertyGroup>

5) 라우트 컴포넌트를 전부 한 번에 로드

Razor 어셈블리는 시작 시 모두 로드됩니다. 페이지 50개짜리 앱이라면 사용하지도 않을 컴포넌트까지 메모리에 올라간다는 뜻입니다. @page를 별도 RCL로 분리하고 지연 어셈블리 로드를 적용하면 초기 풋프린트를 절반 이하로 줄일 수 있습니다.

상태 관리: 의존성 주입과 Fluxor

Blazor Hybrid의 상태 관리는 일반 Blazor와 거의 같지만 한 가지 큰 차이가 있습니다. 앱이 백그라운드로 갔다가 돌아와도 프로세스가 살아 있을 가능성이 높다는 점입니다. 즉, 메모리 내 상태가 웹보다 훨씬 오래 유지됩니다. 이것은 축복이자 함정입니다. 메모리 누수가 곧 데이터 오염으로 이어지기 때문입니다.

저는 작은 앱에는 scoped 서비스 + INotifyPropertyChanged로 충분하다고 봅니다. 중·대형 앱은 Fluxor 같은 단일 store 패턴을 도입하는 편이 디버깅과 시간 여행 테스트 측면에서 압도적으로 유리합니다.

// 간단한 카트 상태 서비스
public class CartState : ICartState
{
    private readonly List<CartItem> _items = new();
    public event Action? OnChange;

    public IReadOnlyList<CartItem> Items => _items;

    public void Add(CartItem item)
    {
        _items.Add(item);
        NotifyStateChanged();
    }

    public void Remove(string sku)
    {
        _items.RemoveAll(i => i.Sku == sku);
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

// 컴포넌트에서 구독
@inject ICartState Cart
@implements IDisposable

@code {
    protected override void OnInitialized() => Cart.OnChange += StateHasChanged;
    public void Dispose() => Cart.OnChange -= StateHasChanged;
}

Blazor Web App과 UI 공유하기

.NET 10의 MAUI Blazor Hybrid + Web App 솔루션 템플릿은 Razor Class Library(RCL)를 중심에 두고 모바일/데스크톱/웹 프로젝트가 모두 같은 컴포넌트를 참조하는 구조를 만듭니다. 진짜 의미의 코드 재사용이 가능해진 셈입니다.

dotnet new maui-blazor-web -n Contoso -f net10.0
# 생성되는 프로젝트:
# - Contoso.Shared (RCL: 공유 Razor 컴포넌트)
# - Contoso.Web    (Blazor Web App)
# - Contoso.MAUI   (Blazor Hybrid)

실전 팁 하나: 공유 RCL에는 플랫폼 서비스 인터페이스만 두고, 구현은 각 호스트에서 등록하세요. IPhotoService를 RCL에 정의하고, Web 측은 <input type="file"> 구현, MAUI 측은 MediaPicker 구현을 등록하면 같은 컴포넌트가 양쪽에서 동작합니다. 이 패턴은 CommunityToolkit.Mvvm DI 등록과도 결이 같습니다. CommunityToolkit.Mvvm 가이드에서 다룬 ObservableProperty 패턴도 Razor 컴포넌트 내부에서 그대로 쓸 수 있습니다.

측면.NET MAUI (XAML).NET MAUI Blazor Hybrid
UI 마크업XAMLRazor (HTML + C#)
웹 코드 재사용불가능RCL로 100% 공유 가능
네이티브 룩 앤 필플랫폼 네이티브 그대로WebView 기반 (CSS로 흉내)
긴 리스트 성능최상 (CollectionView)중간 (가상화 필요)
스타일링XAML 스타일/리소스CSS, SCSS, Tailwind
적합한 앱소비자 모바일, 게임 UIB2B, 어드민, 폼/대시보드 중심
초기 학습 곡선높음 (XAML 별도 학습)낮음 (웹 개발자에게 친숙)

언제 Blazor Hybrid를 선택해야 하는가

이 질문은 거의 항상 비용 구조의 질문입니다. Blazor Hybrid가 명확하게 유리한 시나리오는 다음과 같습니다.

  • 웹 앱이 먼저 있는 경우: 기존 Razor 컴포넌트를 그대로 모바일에 이식할 수 있습니다. 6개월짜리 모바일 재작업이 6주짜리 포장 작업으로 줄어듭니다.
  • 내부 도구·B2B 앱: 앱스토어 차트가 아닌 ROI가 중요한 앱. 픽셀 단위 네이티브 UX보다 빠른 출시와 코드 공유가 가치 있습니다.
  • 웹 개발자가 다수인 팀: XAML을 새로 가르치는 비용 대비 즉시 생산성을 낼 수 있습니다.
  • CMS·대시보드 화면이 핵심: 폼, 표, 차트, 필터가 메인인 화면은 HTML/CSS가 XAML보다 빠르게 만들어집니다.

반대로 다음 경우엔 XAML 기반 .NET MAUI나 네이티브 SDK가 낫습니다. 소셜·미디어 앱처럼 부드러운 60fps 스크롤이 핵심인 경우, 카메라/AR/지도 같은 그래픽 집약 화면이 메인인 경우, 그리고 플랫폼 디자인 가이드(머티리얼 You, iOS Liquid Glass) 준수가 비즈니스 요구사항인 경우입니다.

저는 한 프로젝트에서 두 방식을 섞었습니다. 로그인·온보딩·결제는 Blazor Hybrid, 메인 피드 스크롤만 네이티브 CollectionView로 빼는 식입니다. Shell이 두 종류 페이지를 동시에 다룰 수 있기 때문에 이런 분기는 생각보다 쉽습니다.

디버깅과 진단 로깅

Blazor Hybrid 디버깅은 일반 Blazor와 다릅니다. WebView 자체의 콘솔과 .NET 런타임의 콘솔이 따로 존재하기 때문입니다. .NET 8부터 추가된 AddBlazorWebViewDeveloperTools()를 켜면 WebView 내부에서 우클릭 → "Inspect"로 DevTools를 열 수 있습니다. iOS 시뮬레이터는 Safari의 "개발" 메뉴에서, Android 에뮬레이터는 Chrome의 chrome://inspect에서 같은 작업이 가능합니다.

프로덕션 진단을 위해서는 BlazorWebView의 빌트인 로깅을 활성화하는 것이 좋습니다. ILogger를 통해 JS 인터롭 호출, 렌더링 횟수, 인터롭 에러를 모두 추적할 수 있습니다.

builder.Logging.AddFilter(
    "Microsoft.AspNetCore.Components.WebView",
    LogLevel.Debug);

builder.Services.Configure<LoggerFilterOptions>(options =>
{
    options.AddFilter("Microsoft.AspNetCore.Components.WebView.WebView2",
                      LogLevel.Trace);
});

릴리스 빌드에서 트레이스 레벨을 켜면 성능에 치명적이므로, 빌드 구성에 따라 분기하세요. 또한 시작 시간 측정에는 dotnet-trace를, 메모리 스냅샷에는 dotnet-gcdump를 권장합니다. iOS 디바이스에서 직접 트레이스를 뜨려면 Xcode Instruments의 "Time Profiler" 템플릿이 가장 정확합니다. Xamarin에서 넘어온 분이라면 Xamarin.Forms → .NET MAUI 10 마이그레이션 가이드의 빌드 파이프라인 설정이 그대로 도움이 될 것입니다.

자주 묻는 질문

.NET MAUI Blazor Hybrid는 WebAssembly로 실행되나요?

아닙니다. Razor 컴포넌트의 C# 코드는 디바이스의 .NET 런타임에서 직접 실행되고, WebView는 결과 DOM만 렌더링합니다. WebAssembly나 브라우저 샌드박스가 끼어들지 않기 때문에 시작 속도가 빠르고, MAUI Essentials API에 풀 액세스할 수 있습니다.

Blazor Hybrid와 Blazor WebAssembly의 차이는 무엇인가요?

Blazor WebAssembly는 .wasm 번들을 브라우저에 다운로드해 샌드박스 안에서 실행합니다. Blazor Hybrid는 .NET 런타임이 네이티브 프로세스에서 실행되고 WebView에는 렌더링된 HTML만 전달됩니다. 결과적으로 Hybrid는 네이티브 API 접근이 가능하고 초기 다운로드 비용이 없습니다.

기존 Blazor 웹 앱을 모바일에 그대로 옮길 수 있나요?

대부분의 컴포넌트는 그대로 동작하지만 두 가지 작업이 필요합니다. 첫째, HttpClient 호출은 모바일 네트워크 정책(예: ATS, cleartext)에 맞춰 조정해야 합니다. 둘째, 인증·저장소·파일 처리는 MAUI Essentials API로 추상화된 서비스 인터페이스를 통해 분기해야 합니다. RCL 패턴을 쓰면 코드 90% 이상을 공유할 수 있습니다.

CollectionView 같은 네이티브 컨트롤도 같이 쓸 수 있나요?

네. BlazorWebView는 일반 MAUI 컨트롤이므로 같은 페이지나 다른 페이지에서 CollectionView, Shell, 카메라 뷰 등과 자유롭게 혼용할 수 있습니다. 긴 리스트나 그래픽 집약 화면만 네이티브로 빼고 나머지를 Blazor로 만드는 하이브리드 라우팅이 일반적인 패턴입니다.

Hot Reload는 잘 동작하나요?

.NET 10에서는 Razor 컴포넌트의 .razor, .css, .cs 파일 모두 Hot Reload가 지원됩니다. 라우트나 DI 등록을 변경한 경우만 재시작이 필요합니다. WebView 내부의 정적 자산은 별도 캐시가 없어 즉시 반영됩니다.

앱스토어 심사에 문제는 없나요?

Blazor Hybrid 앱은 일반 네이티브 앱과 동일하게 심사받습니다. 다만 Apple은 WebView로만 구성된 앱(소위 "wrapper app")을 거부할 수 있으므로, 적어도 한두 개의 네이티브 기능(푸시, 카메라, 위치 등)을 실제로 사용하고 있어야 안전합니다. 저는 세 번 출시하면서 거부된 적은 없습니다.

Marcus Chen
저자 소개 Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.