Xamarin WebView 예제를 .NET MAUI로 다시 만들기
이 글은 2022년에 작성한 Xamarin.Forms WebView 예제를 2026년 기준으로 전면 개정한 글입니다. Xamarin 지원은 2024년 5월 1일 종료되었으므로, 기존 URL과 검색 의도는 유지하면서 현재 지원되는 .NET MAUI로 모든 예제를 다시 작성했습니다.
Xamarin WebView 예제를 .NET MAUI로 다시 만들기

모바일 앱 안에서 웹 페이지를 표시하려면 WebView를 사용할 수 있습니다. .NET MAUI의 WebView는 다음 세 가지 콘텐츠를 모두 처리합니다.
| 목적 | 사용할 소스 | 대표적인 용도 |
|---|---|---|
| 인터넷 페이지 표시 | UrlWebViewSource |
도움말, 공지, 웹 서비스 |
| 문자열로 만든 HTML 표시 | HtmlWebViewSource |
동적 안내문, 간단한 리포트 |
| 앱에 포함한 HTML 표시 | 로컬 파일 | 오프라인 도움말, 약관 |

이 글에서는 새 프로젝트를 만든 뒤 위 세 가지 방법을 구현하고, 로딩 상태·오류·뒤로 가기·외부 링크 제한까지 연결합니다.
Xamarin.Forms 대신 .NET MAUI를 사용하는 이유
Microsoft의 Xamarin 지원은 2024년 5월 1일 종료되었습니다. Xamarin.Android와 Xamarin.iOS는 .NET for Android 및 .NET for iOS에 통합되었고, Xamarin.Forms의 후속 프레임워크는 .NET MAUI입니다.
| 기존 기술 | 현재 선택 |
|---|---|
| Xamarin.Forms | .NET MAUI |
| 여러 플랫폼 프로젝트 | 단일 프로젝트 구조 |
| 플랫폼별 Effect/Renderer | Handler 기반 사용자 지정 |
| Xamarin.Forms WebView | .NET MAUI WebView |
기존 Xamarin.Forms 프로젝트를 유지보수하는 경우에도 보안 업데이트와 최신 Android·iOS 도구 지원을 위해 .NET MAUI 이전 계획을 세우는 편이 좋습니다.
예제 프로젝트 만들기
.NET SDK와 MAUI 워크로드가 준비된 환경에서 다음 명령을 실행합니다.
dotnet workload install maui
dotnet new maui -n MauiWebViewSample
cd MauiWebViewSample
Visual Studio를 사용한다면 .NET MAUI 앱 템플릿으로 MauiWebViewSample 프로젝트를 만들어도 됩니다.
이 예제는 MainPage.xaml에 주소 표시줄, 이전·새로 고침 버튼, 상태 표시와 WebView를 배치합니다.
원격 웹 페이지 표시하기
MainPage.xaml을 다음과 같이 구성합니다.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MauiWebViewSample.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="WebView 예제">
<Grid
Padding="16"
RowDefinitions="Auto,Auto,*,Auto"
RowSpacing="12">
<Border
Grid.Row="0"
Padding="12,8"
Stroke="#D7DEE8"
StrokeShape="RoundRectangle 12">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
<Entry
x:Name="AddressEntry"
ClearButtonVisibility="WhileEditing"
Keyboard="Url"
Placeholder="https:// 주소를 입력하세요"
ReturnType="Go"
Text="https://learn.microsoft.com/dotnet/maui/"
Completed="OnAddressCompleted" />
<Button
Grid.Column="1"
Clicked="OnGoClicked"
Text="이동" />
</Grid>
</Border>
<HorizontalStackLayout Grid.Row="1" Spacing="8">
<Button Clicked="OnBackClicked" Text="← 이전" />
<Button Clicked="OnRefreshClicked" Text="새로 고침" />
<ActivityIndicator
x:Name="LoadingIndicator"
VerticalOptions="Center" />
</HorizontalStackLayout>
<Border
Grid.Row="2"
Stroke="#D7DEE8"
StrokeShape="RoundRectangle 16">
<WebView
x:Name="Browser"
Navigated="OnNavigated"
Navigating="OnNavigating" />
</Border>
<Label
x:Name="StatusLabel"
Grid.Row="3"
FontSize="12"
Text="준비됨"
TextColor="#5D6673" />
</Grid>
</ContentPage>
코드 비하인드에서는 URL을 검증한 뒤 WebView에 전달합니다.
namespace MauiWebViewSample;
public partial class MainPage : ContentPage
{
private static readonly HashSet<string> AllowedHosts =
new(StringComparer.OrdinalIgnoreCase)
{
"learn.microsoft.com",
"dotnet.microsoft.com"
};
public MainPage()
{
InitializeComponent();
NavigateTo(AddressEntry.Text);
}
private void OnGoClicked(object sender, EventArgs e) =>
NavigateTo(AddressEntry.Text);
private void OnAddressCompleted(object sender, EventArgs e) =>
NavigateTo(AddressEntry.Text);
private async void NavigateTo(string? input)
{
if (!Uri.TryCreate(input, UriKind.Absolute, out var uri) ||
uri.Scheme != Uri.UriSchemeHttps)
{
await DisplayAlert("주소 확인", "https://로 시작하는 주소를 입력하세요.", "확인");
return;
}
if (!AllowedHosts.Contains(uri.Host))
{
await DisplayAlert("이동 차단", "허용된 사이트만 앱 안에서 열 수 있습니다.", "확인");
return;
}
Browser.Source = new UrlWebViewSource { Url = uri.ToString() };
}
private void OnNavigating(object sender, WebNavigatingEventArgs e)
{
LoadingIndicator.IsRunning = true;
StatusLabel.Text = "불러오는 중…";
if (!Uri.TryCreate(e.Url, UriKind.Absolute, out var uri) ||
uri.Scheme != Uri.UriSchemeHttps ||
!AllowedHosts.Contains(uri.Host))
{
e.Cancel = true;
LoadingIndicator.IsRunning = false;
StatusLabel.Text = "허용되지 않은 이동을 차단했습니다.";
}
}
private void OnNavigated(object sender, WebNavigatedEventArgs e)
{
LoadingIndicator.IsRunning = false;
StatusLabel.Text = e.Result == WebNavigationResult.Success
? "페이지를 불러왔습니다."
: $"페이지 로드 실패: {e.Result}";
}
private async void OnBackClicked(object sender, EventArgs e)
{
if (Browser.CanGoBack)
{
Browser.GoBack();
return;
}
await DisplayAlert("이전 페이지", "WebView 방문 기록이 없습니다.", "확인");
}
private void OnRefreshClicked(object sender, EventArgs e) =>
Browser.Reload();
}
Navigating은 페이지 이동 전에 발생하므로 이동을 취소할 수 있고, Navigated는 완료 결과를 알려줍니다. 사용자가 입력한 주소를 그대로 열기보다 허용할 스킴과 호스트를 명시적으로 제한하는 것이 안전합니다.
HTML 문자열 직접 표시하기
서버에서 받은 안내문이나 앱에서 만든 리포트를 표시할 때는 HtmlWebViewSource를 사용할 수 있습니다.
var html = """
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 24px;
color: #172033;
background: #f5f7fb;
font-family: system-ui, sans-serif;
}
.card {
max-width: 680px;
margin: auto;
padding: 24px;
border-radius: 18px;
background: white;
box-shadow: 0 10px 30px rgba(31, 44, 70, .10);
}
h1 { margin-top: 0; color: #3457d5; }
</style>
</head>
<body>
<article class="card">
<h1>오프라인 안내</h1>
<p>이 화면은 HTML 문자열로 만들어 WebView에 표시했습니다.</p>
</article>
</body>
</html>
""";
Browser.Source = new HtmlWebViewSource { Html = html };
HTML 문자열에 신뢰할 수 없는 사용자 입력을 넣어야 한다면 반드시 HTML 인코딩을 적용해야 합니다. 인증 토큰이나 개인정보를 HTML 또는 JavaScript 문자열에 직접 삽입하지 마세요.
앱에 포함한 로컬 HTML 열기
오프라인 도움말처럼 HTML·CSS·이미지를 함께 배포하려면 Resources/Raw 아래에 파일을 둡니다.
Resources/
└─ Raw/
└─ help/
├─ index.html
└─ styles.css
Resources/Raw/help/index.html:
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<title>앱 도움말</title>
</head>
<body>
<main class="card">
<p class="eyebrow">MAUI WEBVIEW</p>
<h1>오프라인 도움말</h1>
<p>이 문서는 앱 패키지 안에 포함되어 네트워크가 없어도 열립니다.</p>
</main>
</body>
</html>
단순한 로컬 문서는 파일 내용을 읽어 HtmlWebViewSource로 표시할 수 있습니다.
private async Task ShowLocalHelpAsync()
{
await using var stream =
await FileSystem.OpenAppPackageFileAsync("help/index.html");
using var reader = new StreamReader(stream);
var html = await reader.ReadToEndAsync();
Browser.Source = new HtmlWebViewSource
{
Html = html
};
}
HTML이 여러 JavaScript·CSS·이미지 파일과 상호 작용하는 웹 앱이라면 일반 WebView보다 HybridWebView가 편리합니다. HybridWebView의 기본 웹 루트는 Resources/Raw/wwwroot이고, 기본 시작 파일은 index.html입니다.
| 요구 사항 | 권장 컨트롤 |
|---|---|
| 원격 페이지 표시 | WebView |
| 짧은 HTML 문자열 표시 | WebView |
| 단순 오프라인 문서 | WebView |
| 여러 정적 파일로 구성된 웹 앱 | HybridWebView |
| JavaScript와 C# 양방향 호출 | HybridWebView |
Android 뒤로 가기 처리
사용자가 시스템 뒤로 가기를 눌렀을 때 WebView 방문 기록이 있으면 이전 웹 페이지로 이동하도록 처리할 수 있습니다.
protected override bool OnBackButtonPressed()
{
if (Browser.CanGoBack)
{
Browser.GoBack();
return true;
}
return base.OnBackButtonPressed();
}
이 처리가 없으면 WebView 내부 페이지가 아니라 앱의 이전 화면으로 빠져나갈 수 있습니다. 다만 플랫폼별 뒤로 가기 동작과 셸 탐색을 함께 사용하는 앱에서는 실제 장치에서 충돌 여부를 확인해야 합니다.
WebView를 사용할 때 반드시 확인할 보안 항목
WebView는 앱 안에 브라우저를 넣는 기능이므로 입력 URL과 표시 콘텐츠를 일반 UI 문자열처럼 취급하면 안 됩니다.
| 점검 항목 | 권장 처리 |
|---|---|
| 통신 방식 | HTTPS만 허용 |
| 이동 가능한 사이트 | 호스트 허용 목록 사용 |
| 외부 링크 | 시스템 브라우저로 분리하거나 차단 |
| 사용자 HTML | 인코딩 또는 검증 후 삽입 |
| 인증 정보 | URL·HTML·JavaScript에 직접 포함하지 않음 |
| 파일 업로드·카메라 | 실제 필요한 권한만 요청 |
| 플랫폼 차이 | Android·iOS·Windows에서 각각 테스트 |
Android는 Chromium 기반 시스템 WebView를, iOS와 Mac Catalyst는 WKWebView를, Windows는 WebView2를 사용합니다. 같은 HTML이라도 엔진과 OS 버전에 따라 표시나 지원 API가 달라질 수 있습니다.
자주 발생하는 문제
빈 화면만 표시되는 경우
먼저 URL이 HTTPS인지, 장치가 인터넷에 연결되어 있는지, Navigated의 Result가 무엇인지 확인합니다. 사내 인증서나 리디렉션이 포함된 사이트는 일반 브라우저와 다르게 동작할 수 있습니다.
링크를 누르면 허용 목록에서 차단되는 경우
로그인이나 문서 사이트가 다른 하위 도메인으로 이동할 수 있습니다. 실제 이동 URL을 확인한 뒤 꼭 필요한 호스트만 AllowedHosts에 추가합니다. 모든 호스트를 허용하는 방식으로 문제를 덮지 않는 것이 좋습니다.
로컬 CSS와 이미지가 보이지 않는 경우
HTML 문자열만 읽으면 상대 경로의 CSS와 이미지가 자동으로 해결되지 않을 수 있습니다. 여러 파일이 필요한 콘텐츠는 Resources/Raw/wwwroot와 HybridWebView 구조를 사용하는 편이 명확합니다.
JavaScript와 C#이 서로 데이터를 주고받아야 하는 경우
단순 WebView에 플랫폼별 브리지를 직접 구현할 수도 있지만 코드와 보안 검토 범위가 커집니다. 현재 .NET MAUI에서는 이 용도로 제공되는 HybridWebView를 먼저 검토하세요.
정리
Xamarin.Forms WebView 예제의 핵심은 .NET MAUI에서도 이어지지만, 새 프로젝트를 Xamarin으로 시작해서는 안 됩니다. 원격 페이지나 간단한 HTML은 WebView, 여러 로컬 파일과 C#·JavaScript 연동이 필요한 웹 앱은 HybridWebView로 구분하면 구현이 단순해집니다.
무엇보다 WebView는 표시 기능만 완성하면 끝나는 컨트롤이 아닙니다. HTTPS 제한, 허용 호스트, 탐색 취소, 오류 표시, 뒤로 가기와 플랫폼별 테스트를 함께 구현해야 실제 앱에서 안전하게 사용할 수 있습니다.
댓글
댓글 쓰기