.NET MAUI Blazor Hybrid 완벽 가이드: AI 통합, 네이티브 API, 성능 최적화 (2026)

.NET MAUI Blazor Hybrid의 아키텍처, Azure OpenAI 및 ONNX 온디바이스 AI 통합, 네이티브 디바이스 API 활용, AOT 컴파일 성능 최적화, bUnit/Appium 테스트, CI/CD 배포까지 실전 개발의 모든 것을 다룹니다.

왜 지금 .NET MAUI Blazor Hybrid인가?

솔직히 말하면, 크로스 플랫폼 개발 프레임워크를 선택하는 건 항상 고민스럽습니다. Flutter도 있고, React Native도 있고, 뭘 골라야 할지 매번 머리가 아프죠. 그런데 2026년 들어서 .NET MAUI Blazor Hybrid가 꽤 흥미로운 포지션을 잡고 있습니다.

핵심은 간단합니다. 웹 기술(HTML, CSS, Razor)로 UI를 만들면서, 동시에 네이티브 디바이스 API에 완전히 접근할 수 있다는 거예요. 웹뷰 안에서 돌아가는 하이브리드 앱이지만, BlazorWebView 컨트롤 덕분에 서버 왕복 없이 로컬에서 모든 게 처리됩니다.

그래서 이번 글에서는 Blazor Hybrid 앱의 아키텍처부터 AI 통합, 성능 최적화, 테스트, 배포까지 실무에서 바로 쓸 수 있는 내용을 정리해봤습니다.

Blazor Hybrid 아키텍처 이해하기

Blazor Hybrid의 동작 방식을 이해하려면 먼저 기존 Blazor 모델과의 차이를 알아야 합니다.

Blazor Server는 서버에서 렌더링하고 SignalR로 UI 업데이트를 보내고, Blazor WebAssembly는 브라우저에서 .NET 런타임을 통째로 돌립니다. Blazor Hybrid는 이 둘과 다릅니다. 네이티브 앱 안에 내장된 WebView에서 Razor 컴포넌트가 실행되고, .NET 런타임은 네이티브 프로세스에서 직접 돌아갑니다. 네트워크 요청이 아니라 프로세스 내부 통신이라 속도가 빠릅니다.

프로젝트 기본 구조

새 프로젝트를 만들면 핵심 구조는 이렇게 생겼습니다:

MyHybridApp/
├── Components/
│   ├── Layout/
│   │   ├── MainLayout.razor
│   │   └── NavMenu.razor
│   └── Pages/
│       ├── Home.razor
│       └── Counter.razor
├── Platforms/
│   ├── Android/
│   ├── iOS/
│   ├── MacCatalyst/
│   └── Windows/
├── Resources/
│   ├── Raw/
│   │   └── wwwroot/
│   │       ├── css/
│   │       └── index.html
│   └── ...
├── MauiProgram.cs
└── App.xaml

Components 폴더에 Razor 컴포넌트를 넣고, Platforms 폴더에 플랫폼별 코드가 들어갑니다. 웹 에셋은 Resources/Raw/wwwroot에 놓으면 되고요.

BlazorWebView 설정

XAML에서 BlazorWebView를 추가하는 건 정말 간단합니다:

<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"
             x:Class="MyHybridApp.MainPage">
    <Grid>
        <b:BlazorWebView HostPage="wwwroot/index.html">
            <b:BlazorWebView.RootComponents>
                <b:RootComponent Selector="#app"
                                 ComponentType="{x:Type local:Components.Routes}" />
            </b:BlazorWebView.RootComponents>
        </b:BlazorWebView>
    </Grid>
</ContentPage>

여기서 HostPage가 웹뷰의 진입점이고, RootComponent가 Blazor 라우팅의 시작점입니다. 한 화면에 여러 BlazorWebView를 배치해서 네이티브 XAML 컨트롤과 Blazor UI를 혼합할 수도 있는데, 실제로 이렇게 쓰는 프로젝트가 꽤 있더라고요.

AI 기능 통합하기

2026년에 모바일 앱을 만들면서 AI를 빼놓을 수는 없겠죠. Blazor Hybrid에서는 클라우드 AI와 온디바이스 AI를 모두 활용할 수 있습니다.

Azure OpenAI 서비스 연동

먼저 클라우드 기반 AI부터 봅시다. Azure OpenAI를 연동하는 서비스 클래스를 만들어보겠습니다:

using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;

public class AiChatService
{
    private readonly IChatClient _chatClient;

    public AiChatService(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }

    public async Task<string> GetResponseAsync(string userMessage)
    {
        var response = await _chatClient.GetResponseAsync(userMessage);
        return response.Text;
    }

    public async IAsyncEnumerable<string> StreamResponseAsync(
        string userMessage)
    {
        await foreach (var update in
            _chatClient.GetStreamingResponseAsync(userMessage))
        {
            if (!string.IsNullOrEmpty(update.Text))
                yield return update.Text;
        }
    }
}

DI 등록은 MauiProgram.cs에서 처리합니다:

builder.Services.AddSingleton<IChatClient>(sp =>
{
    var client = new AzureOpenAIClient(
        new Uri("https://my-resource.openai.azure.com/"),
        new Azure.AzureKeyCredential("your-api-key"));
    return client.GetChatClient("gpt-4o").AsIChatClient();
});

builder.Services.AddSingleton<AiChatService>();

(실제 프로덕션에서는 API 키를 코드에 하드코딩하면 안 됩니다. Azure Key Vault나 앱 설정을 쓰세요.)

온디바이스 AI: ONNX Runtime으로 오프라인 추론

여기서 진짜 재미있는 부분이 나옵니다. 네트워크 없이도 디바이스에서 직접 AI 모델을 돌릴 수 있거든요. ONNX Runtime을 사용하면 됩니다:

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

public class OnDeviceClassifier : IDisposable
{
    private readonly InferenceSession _session;

    public OnDeviceClassifier()
    {
        var modelPath = Path.Combine(
            FileSystem.AppDataDirectory, "models", "classifier.onnx");
        var options = new SessionOptions();
        options.GraphOptimizationLevel =
            GraphOptimizationLevel.ORT_ENABLE_ALL;
        _session = new InferenceSession(modelPath, options);
    }

    public float[] Predict(float[] inputData)
    {
        var inputTensor = new DenseTensor<float>(
            inputData, new[] { 1, inputData.Length });
        var inputs = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor("input", inputTensor)
        };

        using var results = _session.Run(inputs);
        return results.First().AsEnumerable<float>().ToArray();
    }

    public void Dispose() => _session?.Dispose();
}

이 방식의 장점은 확실합니다. 오프라인에서도 동작하고, 응답 속도가 빠르며, 사용자 데이터가 디바이스를 떠나지 않습니다. 개인정보 보호가 중요한 앱이라면 특히 유용하죠.

Razor 컴포넌트에서 AI 활용하기

실제 UI 컴포넌트에서 AI 챗을 구현하면 이런 식입니다:

@page "/ai-assistant"
@inject AiChatService ChatService

<div class="chat-container">
    @foreach (var msg in _messages)
    {
        <div class="chat-message @msg.Role">
            @msg.Content
        </div>
    }

    @if (_isLoading)
    {
        <div class="typing-indicator">AI가 답변을 생성하고 있습니다...</div>
    }
</div>

<div class="input-area">
    <input @bind="_userInput"
           @onkeypress="HandleKeyPress"
           placeholder="메시지를 입력하세요..." />
    <button @onclick="SendMessage" disabled="@_isLoading">전송</button>
</div>

@code {
    private List<ChatMessage> _messages = new();
    private string _userInput = "";
    private bool _isLoading;

    private async Task SendMessage()
    {
        if (string.IsNullOrWhiteSpace(_userInput)) return;

        var input = _userInput;
        _userInput = "";
        _messages.Add(new("user", input));
        _isLoading = true;

        try
        {
            var response = await ChatService.GetResponseAsync(input);
            _messages.Add(new("assistant", response));
        }
        catch (Exception ex)
        {
            _messages.Add(new("system",
                $"오류가 발생했습니다: {ex.Message}"));
        }
        finally
        {
            _isLoading = false;
        }
    }

    private async Task HandleKeyPress(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") await SendMessage();
    }

    record ChatMessage(string Role, string Content);
}

스트리밍 응답을 쓰면 ChatGPT처럼 글자가 하나씩 나오는 효과도 줄 수 있는데, 사용자 경험 측면에서 확실히 좋습니다.

네이티브 디바이스 API 활용

Blazor Hybrid의 가장 큰 장점 중 하나가 바로 이겁니다. 웹 기술로 UI를 만들면서도 카메라, GPS, 센서 같은 네이티브 기능을 자유롭게 쓸 수 있다는 것.

카메라와 위치 정보

네이티브 API를 Razor 컴포넌트에서 쓰려면 서비스 레이어를 만들어서 DI로 주입하는 게 좋습니다:

public class DeviceService
{
    public async Task<FileResult?> TakePhotoAsync()
    {
        if (!MediaPicker.Default.IsCaptureSupported)
            return null;

        var photo = await MediaPicker.Default.CapturePhotoAsync(
            new MediaPickerOptions
            {
                Title = "사진 촬영"
            });

        return photo;
    }

    public async Task<Location?> GetCurrentLocationAsync()
    {
        var request = new GeolocationRequest(
            GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
        return await Geolocation.Default.GetLocationAsync(request);
    }

    public async Task<bool> ShareFileAsync(
        string filePath, string title)
    {
        await Share.Default.RequestAsync(new ShareFileRequest
        {
            Title = title,
            File = new ShareFile(filePath)
        });
        return true;
    }
}

Razor 컴포넌트에서는 이렇게 쓰면 됩니다:

@inject DeviceService Device

<button @onclick="CapturePhoto">📷 사진 촬영</button>

@if (_photoPath is not null)
{
    <img src="@_photoPath" alt="촬영된 사진" />
}

@code {
    private string? _photoPath;

    private async Task CapturePhoto()
    {
        var result = await Device.TakePhotoAsync();
        if (result is not null)
        {
            var stream = await result.OpenReadAsync();
            var path = Path.Combine(
                FileSystem.CacheDirectory, result.FileName);
            using var fileStream = File.OpenWrite(path);
            await stream.CopyToAsync(fileStream);
            _photoPath = path;
        }
    }
}

플랫폼별 코드 처리

가끔은 플랫폼별로 다른 동작이 필요할 때가 있습니다. 그럴 때는 partial class 패턴을 쓰면 깔끔합니다:

// Services/NativeFeatureService.cs
public partial class NativeFeatureService
{
    public partial Task<bool> RequestNotificationPermissionAsync();
    public partial Task ShowNativeAlertAsync(string title, string message);
}

// Platforms/Android/Services/NativeFeatureService.cs
public partial class NativeFeatureService
{
    public partial async Task<bool> RequestNotificationPermissionAsync()
    {
        if (OperatingSystem.IsAndroidVersionAtLeast(33))
        {
            var status = await Permissions
                .RequestAsync<Permissions.PostNotifications>();
            return status == PermissionStatus.Granted;
        }
        return true;
    }

    public partial async Task ShowNativeAlertAsync(
        string title, string message)
    {
        var activity = Platform.CurrentActivity;
        if (activity is null) return;

        await MainThread.InvokeOnMainThreadAsync(() =>
        {
            new AndroidX.AppCompat.App.AlertDialog.Builder(activity)
                .SetTitle(title)
                .SetMessage(message)
                .SetPositiveButton("확인", (s, e) => { })
                .Show();
        });
    }
}

이렇게 하면 공통 인터페이스는 하나인데 플랫폼마다 구현이 달라지는 구조를 만들 수 있습니다. 개인적으로 이 패턴이 예전의 DependencyService보다 훨씬 깔끔하다고 생각합니다.

성능 최적화: 진짜 빠른 앱 만들기

하이브리드 앱이라고 해서 느려야 할 이유는 없습니다. 제대로 최적화하면 네이티브에 가까운 성능을 낼 수 있어요.

AOT 컴파일 활성화

가장 큰 성능 향상을 가져오는 건 역시 AOT 컴파일입니다. .csproj 파일에 다음을 추가하세요:

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

AOT를 켜면 앱 시작 시간이 확 줄어듭니다. 단, 빌드 시간은 좀 늘어나니까 릴리스 빌드에만 적용하는 게 좋습니다.

렌더링 최적화

Blazor 컴포넌트의 불필요한 재렌더링을 줄이는 것도 중요합니다:

@implements IHandleEvent

@code {
    // ShouldRender를 오버라이드해서 필요할 때만 렌더링
    private bool _shouldRender = true;

    protected override bool ShouldRender() => _shouldRender;

    // IHandleEvent를 구현하면 이벤트 처리 후
    // 자동 렌더링을 제어할 수 있음
    Task IHandleEvent.HandleEventAsync(
        EventCallbackWorkItem callback, object? arg)
    {
        _shouldRender = false;
        var task = callback.InvokeAsync(arg);
        _shouldRender = true;
        return task;
    }
}

리스트가 긴 경우에는 가상화를 반드시 사용하세요:

<Virtualize Items="_items" Context="item"
            ItemSize="60" OverscanCount="5">
    <div class="list-item">
        <span>@item.Name</span>
        <span>@item.Description</span>
    </div>
</Virtualize>

수천 개의 아이템이 있어도 화면에 보이는 것만 렌더링하니까 성능 차이가 엄청납니다. 저도 한번 가상화 없이 5000개 아이템 리스트를 띄운 적이 있는데... 그날 이후로 Virtualize 컴포넌트의 소중함을 깨달았습니다.

이미지 및 리소스 최적화

이미지 처리도 성능에 큰 영향을 줍니다:

public static class ImageOptimizer
{
    public static async Task<string> OptimizeForDisplayAsync(
        string imagePath, int maxWidth = 800)
    {
        using var stream = File.OpenRead(imagePath);
        using var image = PlatformImage.FromStream(stream);

        if (image.Width <= maxWidth)
            return imagePath;

        var ratio = (float)maxWidth / image.Width;
        var newHeight = (int)(image.Height * ratio);
        var resized = image.Resize(maxWidth, newHeight,
            ResizeMode.AspectFit);

        var outputPath = Path.Combine(
            FileSystem.CacheDirectory,
            $"opt_{Path.GetFileName(imagePath)}");
        using var output = File.OpenWrite(outputPath);
        await resized.SaveAsync(output, ImageFormat.Jpeg, 0.8f);

        return outputPath;
    }
}

원본 이미지를 그대로 표시하면 메모리를 불필요하게 많이 차지합니다. 표시할 크기에 맞게 리사이징하는 게 기본입니다.

반응형 UI 패턴

크로스 플랫폼 앱이니까 당연히 다양한 화면 크기를 지원해야 합니다. 핸드폰, 태블릿, 데스크톱까지.

CSS로 반응형 레이아웃

Blazor Hybrid에서는 일반 웹 CSS를 그대로 쓸 수 있으니까 미디어 쿼리를 활용하면 됩니다:

/* 모바일 우선 접근 */
.content-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
    padding: 1rem;
}

/* 태블릿 (768px 이상) */
@media (min-width: 768px) {
    .content-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .sidebar {
        display: block;
        width: 280px;
    }
}

/* 데스크톱 (1024px 이상) */
@media (min-width: 1024px) {
    .content-grid {
        grid-template-columns: repeat(3, 1fr);
        max-width: 1200px;
        margin: 0 auto;
    }
}

플랫폼 감지 컴포넌트

CSS만으로는 부족할 때도 있습니다. 플랫폼에 따라 완전히 다른 UI를 보여줘야 할 때가 있거든요:

public class DeviceInfo
{
    public bool IsPhone =>
        Microsoft.Maui.Devices.DeviceInfo.Idiom == DeviceIdiom.Phone;

    public bool IsTablet =>
        Microsoft.Maui.Devices.DeviceInfo.Idiom == DeviceIdiom.Tablet;

    public bool IsDesktop =>
        Microsoft.Maui.Devices.DeviceInfo.Idiom == DeviceIdiom.Desktop;

    public string Platform =>
        Microsoft.Maui.Devices.DeviceInfo.Platform.ToString();
}
@inject DeviceInfo Device

@if (Device.IsPhone)
{
    <MobileNavigation />
}
else
{
    <DesktopSidebar />
}

이 방식으로 폰에서는 하단 탭 바를, 태블릿이나 데스크톱에서는 사이드바를 보여주는 식의 분기가 가능합니다.

테스트 전략

테스트 얘기를 안 할 수 없죠. Blazor Hybrid 앱은 컴포넌트 테스트와 통합 테스트를 조합해서 커버리지를 확보해야 합니다.

bUnit으로 컴포넌트 테스트

Razor 컴포넌트의 단위 테스트에는 bUnit이 사실상 표준입니다:

using Bunit;
using Xunit;

public class CounterTests : TestContext
{
    [Fact]
    public void Counter_InitialValue_IsZero()
    {
        var cut = RenderComponent<Counter>();
        cut.Find(".count-value").MarkupMatches("<span class=\"count-value\">0</span>");
    }

    [Fact]
    public void Counter_ClickButton_IncrementsValue()
    {
        var cut = RenderComponent<Counter>();
        cut.Find("button.increment").Click();
        cut.Find(".count-value").MarkupMatches("<span class=\"count-value\">1</span>");
    }

    [Fact]
    public void AiChat_SendMessage_ShowsResponse()
    {
        // AI 서비스 모킹
        var mockChat = new Mock<AiChatService>();
        mockChat
            .Setup(s => s.GetResponseAsync(It.IsAny<string>()))
            .ReturnsAsync("테스트 응답입니다.");

        Services.AddSingleton(mockChat.Object);

        var cut = RenderComponent<AiAssistant>();
        cut.Find("input").Change("안녕하세요");
        cut.Find("button").Click();

        cut.WaitForState(() =>
            cut.FindAll(".chat-message").Count == 2);

        Assert.Contains("테스트 응답입니다.",
            cut.Find(".chat-message.assistant").TextContent);
    }
}

Appium으로 E2E 테스트

전체 앱의 통합 테스트는 Appium을 쓰면 됩니다. 에뮬레이터나 실제 디바이스에서 앱을 띄우고 자동화된 테스트를 실행할 수 있습니다:

using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;

public class AppTests
{
    private AndroidDriver _driver;

    [SetUp]
    public void Setup()
    {
        var options = new AppiumOptions();
        options.PlatformName = "Android";
        options.AddAdditionalAppiumOption("app",
            "/path/to/com.myapp-Signed.apk");
        options.AddAdditionalAppiumOption("automationName",
            "UiAutomator2");

        _driver = new AndroidDriver(
            new Uri("http://localhost:4723"), options);
        _driver.Manage().Timeouts().ImplicitWait =
            TimeSpan.FromSeconds(10);
    }

    [Test]
    public void Navigation_OpenSettings_ShowsSettingsPage()
    {
        var settingsButton = _driver.FindElement(
            MobileBy.AccessibilityId("SettingsButton"));
        settingsButton.Click();

        var settingsTitle = _driver.FindElement(
            MobileBy.AccessibilityId("SettingsTitle"));
        Assert.That(settingsTitle.Text, Is.EqualTo("설정"));
    }

    [TearDown]
    public void TearDown() => _driver?.Dispose();
}

솔직히 Appium 세팅이 처음에는 좀 번거롭습니다. 하지만 한번 구축해놓으면 CI에서 자동으로 돌릴 수 있으니 장기적으로는 확실히 투자할 가치가 있어요.

스토어 배포와 CI/CD

앱을 만들었으면 배포해야죠. GitHub Actions를 사용한 CI/CD 파이프라인 예시를 보겠습니다.

GitHub Actions 워크플로우

name: Build and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install MAUI Workload
        run: dotnet workload install maui-android

      - name: Build Android
        run: |
          dotnet publish -f net10.0-android \
            -c Release \
            -p:AndroidSigningKeyStore=${{ secrets.KEYSTORE_PATH }} \
            -p:AndroidSigningKeyAlias=${{ secrets.KEY_ALIAS }} \
            -p:AndroidSigningKeyPass=${{ secrets.KEY_PASSWORD }}

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: android-build
          path: '**/*.aab'

  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install MAUI Workload
        run: dotnet workload install maui-ios

      - name: Build iOS
        run: |
          dotnet publish -f net10.0-ios \
            -c Release \
            -p:CodesignKey="${{ secrets.IOS_SIGNING_KEY }}" \
            -p:CodesignProvision="${{ secrets.IOS_PROVISION }}"

Android와 iOS 빌드를 병렬로 돌리면 시간을 많이 절약할 수 있습니다.

앱 스토어 제출 체크리스트

배포 전에 꼭 확인해야 할 것들이 있습니다:

  • Google Play: AAB 형식으로 빌드, 타겟 API 레벨 확인 (2026년 기준 최소 API 35), 64비트 지원 필수
  • Apple App Store: 프로비저닝 프로파일과 인증서 확인, App Transport Security 설정, 권한 사용 설명 추가 (카메라, 위치 등)
  • 공통: 앱 아이콘 모든 사이즈 준비, 스크린샷 준비, 개인정보 처리방침 URL 필요

특히 iOS는 심사 과정에서 WebView 사용 앱에 대해 까다로운 편이니, 앱이 단순한 웹사이트 래퍼가 아니라 실질적인 네이티브 기능을 활용한다는 걸 잘 어필해야 합니다.

보안 베스트 프랙티스

마지막으로 보안 얘기를 빼먹으면 안 되겠죠.

데이터 보호

민감한 데이터는 반드시 안전하게 저장해야 합니다:

public class SecureStorageService
{
    public async Task SaveTokenAsync(string token)
    {
        await SecureStorage.Default.SetAsync("auth_token", token);
    }

    public async Task<string?> GetTokenAsync()
    {
        return await SecureStorage.Default.GetAsync("auth_token");
    }

    public void ClearAll()
    {
        SecureStorage.Default.RemoveAll();
    }
}

SecureStorage는 플랫폼별로 안전한 저장소를 사용합니다. Android에서는 Keystore, iOS에서는 Keychain을 씁니다. 직접 구현하려고 하면 보안 구멍이 생기기 쉬우니까 프레임워크가 제공하는 걸 쓰는 게 현명합니다.

네트워크 보안

API 통신에는 항상 HTTPS를 사용하고, 인증서 피닝까지 적용하면 더 좋습니다:

builder.Services.AddHttpClient("SecureApi", client =>
{
    client.BaseAddress = new Uri("https://api.myapp.com");
    client.DefaultRequestHeaders.Add("X-App-Version",
        AppInfo.Current.VersionString);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ServerCertificateCustomValidationCallback =
        (message, cert, chain, errors) =>
        {
            if (cert is null) return false;
            var expectedThumbprint = "YOUR_CERT_THUMBPRINT";
            return cert.GetCertHashString()
                == expectedThumbprint;
        };
    return handler;
});

WebView 보안 설정

Blazor Hybrid에서 WebView 보안도 신경 써야 합니다. 외부 콘텐츠 로딩을 제한하고, JavaScript 인터페이스를 통한 공격을 방지해야 합니다. 다행히 BlazorWebView는 기본적으로 로컬 콘텐츠만 로드하도록 설정되어 있어서 일반적인 WebView 보안 이슈의 상당 부분은 자동으로 처리됩니다.

하지만 외부 URL을 로드해야 하는 경우에는 화이트리스트 방식으로 허용 도메인을 관리하세요.

마무리하며

.NET MAUI Blazor Hybrid는 2026년 현재 꽤 성숙한 프레임워크가 되었습니다. 웹 개발 경험이 있는 .NET 개발자라면 러닝 커브가 상대적으로 낮고, 하나의 코드베이스로 iOS, Android, Windows, macOS를 모두 커버할 수 있다는 건 확실한 장점입니다.

물론 단점도 있습니다. WebView 기반이라 네이티브 앱 대비 미세한 성능 차이가 있을 수 있고, 복잡한 네이티브 UI 패턴을 구현할 때는 한계가 느껴질 때도 있습니다. 하지만 대부분의 비즈니스 앱에서는 그 차이가 문제가 되지 않는다는 게 제 경험입니다.

AI 통합, AOT 컴파일, 향상된 네이티브 API 접근성 등 최근 추가된 기능들을 보면 Microsoft가 이 프레임워크에 꽤 진지하게 투자하고 있다는 걸 알 수 있습니다. 새 프로젝트를 시작하거나 기존 웹 앱을 모바일로 확장하려는 계획이 있다면, Blazor Hybrid를 한번 진지하게 검토해보시길 추천합니다.

저자 소개 Editorial Team

Our team of expert writers and editors.