Ver3.0 ๐ Xamarin๊ณผ MAUI๋ก ์์ํ๋ ํฌ๋ก์คํ๋ซํผ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ

๐ Xamarin๊ณผ MAUI๋ก ์์ํ๋ ํฌ๋ก์คํ๋ซํผ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ
ํ๋์ ์ฝ๋๋ก iOS, Android, Windows๊น์ง! C#์ผ๋ก ๋ง๋๋ ๋ฉํฐํ๋ซํผ์ ์ธ๊ณ
๐ฏ ํฌ๋ก์คํ๋ซํผ ๊ฐ๋ฐ, ์ ์ง๊ธ ์ฃผ๋ชฉ๋ฐ์๊น?
์น๊ตฌ์ผ, ์์ํด๋ด. ๋๊ฐ ๋ฉ์ง ์ฑ ์์ด๋์ด๋ฅผ ๋ ์ฌ๋ ธ์ด. ๊ทธ๋ฐ๋ฐ iOS์ฉ์ผ๋ก Swift๋ก ํ ๋ฒ ๋ง๋ค๊ณ , Android์ฉ์ผ๋ก Kotlin์ผ๋ก ๋ ๋ง๋ค๊ณ , Windows์ฉ์ผ๋ก ๋ ๋ฐ๋ก ๋ง๋ค์ด์ผ ํ๋ค๋ฉด? ๐ฑ ์๊ฐ๋ง ํด๋ ๋จธ๋ฆฌ๊ฐ ์ง๋์ง๋ํ์ง ์์?
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ **ํฌ๋ก์คํ๋ซํผ ๊ฐ๋ฐ**์ด์ผ. ํ๋์ ์ฝ๋๋ฒ ์ด์ค๋ก ์ฌ๋ฌ ํ๋ซํผ์์ ๋์ํ๋ ์ฑ์ ๋ง๋๋ ๊ฑฐ์ง. ์๊ฐ๋ ์ ์ฝ๋๊ณ , ์ ์ง๋ณด์๋ ํจ์ฌ ์ฌ์์ง๊ณ , ๊ฐ๋ฐ ๋น์ฉ๋ ํ ์ค์ด๋ค์ด. ๐ฐ
ํนํ C# ๊ฐ๋ฐ์๋ผ๋ฉด **Xamarin**๊ณผ **.NET MAUI**(Multi-platform App UI)๋ ์ ๋ง ๋งค๋ ฅ์ ์ธ ์ ํ์ง์ผ. Microsoft๊ฐ ์ง์ ๋ฐ์ด์ฃผ๋ ๊ธฐ์ ์ด๊ณ , .NET ์ํ๊ณ์ ๊ฐ๋ ฅํ ์ง์์ ๋ฐ์ ์ ์๊ฑฐ๋ .
๐ Xamarin์ด๋ ๋ฌด์์ธ๊ฐ?
Xamarin์ 2011๋ ์ ์ฒ์ ๋ฑ์ฅํ ํฌ๋ก์คํ๋ซํผ ๋ชจ๋ฐ์ผ ๊ฐ๋ฐ ํ๋ ์์ํฌ์ผ. 2016๋ ์ Microsoft๊ฐ ์ธ์ํ๋ฉด์ .NET ์ํ๊ณ์ ํต์ฌ ๊ธฐ์ ๋ก ์๋ฆฌ์ก์์ง. C#๊ณผ .NET์ ์ฌ์ฉํด์ ๋ค์ดํฐ๋ธ ์์ค์ ์ฑ๋ฅ์ ๋ด๋ iOS, Android ์ฑ์ ๋ง๋ค ์ ์์ด.
Xamarin์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ๋ญ๊น? ๋ฐ๋ก **๋ค์ดํฐ๋ธ API์ ์ง์ ์ ๊ทผ**ํ ์ ์๋ค๋ ๊ฑฐ์ผ. ๊ฐ ํ๋ซํผ์ ๊ณ ์ ๊ธฐ๋ฅ์ 100% ํ์ฉํ ์ ์์ผ๋ฉด์๋, ๋น์ฆ๋์ค ๋ก์ง์ ๊ณต์ ํ ์ ์์ง. ๐จ
Xamarin.Android: Android ์ฑ ๊ฐ๋ฐ์ ์ํ ๋ฐ์ธ๋ฉ
Xamarin.Forms: UI ์ฝ๋๊น์ง ๊ณต์ ํ ์ ์๋ ์ถ์ํ ๋ ์ด์ด
Xamarin.Essentials: ํฌ๋ก์คํ๋ซํผ API ๋ชจ์
๐ .NET MAUI์ ๋ฑ์ฅ: Xamarin์ ์งํ
2022๋ 5์, Microsoft๋ Xamarin.Forms์ ํ์์์ธ **.NET MAUI**๋ฅผ ์ ์ ์ถ์ํ์ด. MAUI๋ "Multi-platform App UI"์ ์ฝ์๋ก, Xamarin์ ๋ชจ๋ ์ฅ์ ์ ๊ณ์นํ๋ฉด์ ๋์ฑ ํ๋์ ์ด๊ณ ๊ฐ๋ ฅํด์ง ํ๋ ์์ํฌ์ผ. ๐
MAUI๋ ๋จ์ํ Xamarin์ ์ ๊ทธ๋ ์ด๋ ๋ฒ์ ์ด ์๋์ผ. ์์ ํ ์๋กญ๊ฒ ์ค๊ณ๋ ์ํคํ ์ฒ๋ก, iOS, Android, macOS, Windows๋ฅผ ๋ชจ๋ ์ง์ํ๋ **์ง์ ํ ๋ฉํฐํ๋ซํผ ํ๋ ์์ํฌ**์ง.
๐ฑ Xamarin.Forms
์ถ์: 2014๋
์ง์ ํ๋ซํผ: iOS, Android, UWP
UI ๋ ๋๋ง: ๊ฐ ํ๋ซํผ ๋ค์ดํฐ๋ธ ์ปจํธ๋กค
ํ๋ก์ ํธ ๊ตฌ์กฐ: ๋ฉํฐ ํ๋ก์ ํธ
ํซ ๋ฆฌ๋ก๋: ์ ํ์
์ฑ๋ฅ: ์ฐ์
๐ฏ .NET MAUI
์ถ์: 2022๋
์ง์ ํ๋ซํผ: iOS, Android, macOS, Windows
UI ๋ ๋๋ง: ํธ๋ค๋ฌ ๊ธฐ๋ฐ ์ํคํ
์ฒ
ํ๋ก์ ํธ ๊ตฌ์กฐ: ๋จ์ผ ํ๋ก์ ํธ
ํซ ๋ฆฌ๋ก๋: ์์ ์ง์
์ฑ๋ฅ: ๋์ฑ ํฅ์๋จ
โจ .NET MAUI์ ์ฃผ์ ํน์ง
1๏ธโฃ ๋จ์ผ ํ๋ก์ ํธ ๊ตฌ์กฐ (Single Project)
๋ ์ด์ ํ๋ซํผ๋ณ๋ก ํ๋ก์ ํธ๋ฅผ ๋๋ ํ์๊ฐ ์์ด. ํ๋์ ํ๋ก์ ํธ์์ ๋ชจ๋ ํ๋ซํผ์ ๊ด๋ฆฌํ ์ ์์ง. ๋ฆฌ์์ค, ํฐํธ, ์ด๋ฏธ์ง ๋ฑ๋ ํ ๊ณณ์์ ๊ด๋ฆฌ๋ผ.
2๏ธโฃ ํซ ๋ฆฌ๋ก๋ (Hot Reload)
์ฝ๋๋ฅผ ์์ ํ๋ฉด ์ฑ์ ๋ค์ ๋น๋ํ์ง ์์๋ ์ฆ์ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์๋ผ. ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ๋๊ฒ ๋นจ๋ผ์ง์ง. XAML๊ณผ C# ์ฝ๋ ๋ชจ๋ ์ง์ํด! ๐ฅ
3๏ธโฃ ํธ๋ค๋ฌ ์ํคํ
์ฒ (Handler Architecture)
Xamarin.Forms์ ๋ ๋๋ฌ๋ณด๋ค ํจ์ฌ ๊ฐ๋ณ๊ณ ํจ์จ์ ์ธ ํธ๋ค๋ฌ ์์คํ
์ ์ฌ์ฉํด. ์ปค์คํฐ๋ง์ด์ง๋ ๋ ์ฝ๊ณ , ์ฑ๋ฅ๋ ํฅ์๋์ด.
4๏ธโฃ .NET 6/7/8 ํตํฉ
์ต์ .NET์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์์ด. C# 10, 11, 12์ ์๋ก์ด ๋ฌธ๋ฒ, ์ฑ๋ฅ ๊ฐ์ , ๊ทธ๋ฆฌ๊ณ ๋ฐฉ๋ํ NuGet ํจํค์ง ์ํ๊ณ๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ง.
5๏ธโฃ ๋ค์ดํฐ๋ธ ์๋ฒ ๋ฉ
๊ธฐ์กด ๋ค์ดํฐ๋ธ ์ฑ์ MAUI ํ๋ฉด์ ์๋ฒ ๋ฉํ๊ฑฐ๋, ๋ฐ๋๋ก MAUI ์ฑ์ ๋ค์ดํฐ๋ธ ๋ทฐ๋ฅผ ๋ฃ์ ์ ์์ด. ์ ์ง์ ์ธ ๋ง์ด๊ทธ๋ ์ด์
์ด ๊ฐ๋ฅํ๋ค๋ ๋ป์ด์ผ.
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ธํ ํด์ผ ํด. ๊ฑฑ์ ๋ง, ์๊ฐ๋ณด๋ค ๊ฐ๋จํด! ๐
Step 1: Visual Studio ์ค์น
MAUI ๊ฐ๋ฐ์ ํต์ฌ์ **Visual Studio**์ผ. Windows์์๋ Visual Studio 2022, macOS์์๋ Visual Studio 2022 for Mac์ ์ฌ์ฉํด.
Windows ์ฌ์ฉ์:
โข Visual Studio 2022 ๋ฒ์ 17.3 ์ด์
โข ".NET Multi-platform App UI development" ์ํฌ๋ก๋ ์ ํ
โข Android SDK, iOS ์๊ฒฉ ๋น๋ ์ค์ (์ ํ์ฌํญ)
Mac ์ฌ์ฉ์:
โข Visual Studio 2022 for Mac ๋ฒ์ 17.4 ์ด์
โข Xcode ์ต์ ๋ฒ์ (iOS ๊ฐ๋ฐ์ฉ)
โข Android SDK ์ค์น
Step 2: .NET SDK ํ์ธ
Visual Studio๋ฅผ ์ค์นํ๋ฉด .NET SDK๊ฐ ํจ๊ป ์ค์น๋์ง๋ง, ํฐ๋ฏธ๋์์ ํ์ธํด๋ณด์:
dotnet --version
.NET 6.0 ์ด์์ด ์ค์น๋์ด ์์ด์ผ ํด. ์ต์ ๋ฒ์ ์ .NET 8.0์ด๋ ๊ฐ๋ฅํ๋ฉด ์ต์ ๋ฒ์ ์ ์ฌ์ฉํ๋ ๊ฒ ์ข์.
Step 3: MAUI ์ํฌ๋ก๋ ์ค์น
ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด:
dotnet workload install maui
์ด ๋ช ๋ น์ด๋ MAUI ๊ฐ๋ฐ์ ํ์ํ ๋ชจ๋ ๋๊ตฌ์ ํ ํ๋ฆฟ์ ์ค์นํด์ค. ์๊ฐ์ด ์ข ๊ฑธ๋ฆด ์ ์์ผ๋ ์ปคํผ ํ ์ ํ๊ณ ์๋ ๋ผ. โ
Step 4: ์ฒซ ํ๋ก์ ํธ ์์ฑ
Visual Studio๋ฅผ ์ด๊ณ "์ ํ๋ก์ ํธ ๋ง๋ค๊ธฐ"๋ฅผ ์ ํํ ๋ค์, ".NET MAUI App" ํ ํ๋ฆฟ์ ์ฐพ์๋ด. ๋๋ ํฐ๋ฏธ๋์์:
dotnet new maui -n MyFirstMauiApp
cd MyFirstMauiApp
dotnet build
์ง์! ๐ ์ฒซ MAUI ํ๋ก์ ํธ๊ฐ ์์ฑ๋์ด. ์ด์ ์คํ๋ง ํ๋ฉด ๋ผ!
๐ MAUI ํ๋ก์ ํธ ๊ตฌ์กฐ ํํค์น๊ธฐ
์๋ก ๋ง๋ ํ๋ก์ ํธ๋ฅผ ์ด์ด๋ณด๋ฉด ์ฌ๋ฌ ํ์ผ๊ณผ ํด๋๊ฐ ๋ณด์ผ ๊ฑฐ์ผ. ํ๋์ฉ ์ดํด๋ณด์:
๐๏ธ ์ฃผ์ ํ์ผ ๋ฐ ํด๋
App.xaml / App.xaml.cs
์ฑ์ ์ง์
์ ์ด์ ์ ์ญ ๋ฆฌ์์ค๋ฅผ ์ ์ํ๋ ๊ณณ์ด์ผ. ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ ์คํ์ผ, ์์, ํ
๋ง ๋ฑ์ ์ฌ๊ธฐ์ ์ค์ ํด.
AppShell.xaml / AppShell.xaml.cs
์ฑ์ ๋ค๋น๊ฒ์ด์
๊ตฌ์กฐ๋ฅผ ์ ์ํ๋ ํ์ผ์ด์ผ. Shell์ MAUI์ ๊ฐ๋ ฅํ ๋ค๋น๊ฒ์ด์
์์คํ
์ผ๋ก, ํญ, ํ๋ผ์ด์์ ๋ฉ๋ด, ๋ผ์ฐํ
๋ฑ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
MainPage.xaml / MainPage.xaml.cs
์ฑ์ด ์์๋ ๋ ๋ณด์ด๋ ์ฒซ ํ๋ฉด์ด์ผ. XAML ํ์ผ์๋ UI ๋ ์ด์์์ด, .cs ํ์ผ์๋ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ค์ด๊ฐ.
MauiProgram.cs
์ฑ์ ๋ถํธ์คํธ๋ํ๊ณผ ์์กด์ฑ ์ฃผ์
์ ์ค์ ํ๋ ๊ณณ์ด์ผ. ์๋น์ค, ํฐํธ, ํธ๋ค๋ฌ ๋ฑ์ ์ฌ๊ธฐ์ ๋ฑ๋กํด.
Platforms ํด๋
๊ฐ ํ๋ซํผ๋ณ ํนํ ์ฝ๋๊ฐ ๋ค์ด๊ฐ๋ ๊ณณ์ด์ผ. Android, iOS, MacCatalyst, Windows ํด๋๊ฐ ์๊ณ , ํ๋ซํผ๋ณ๋ก ๋ค๋ฅด๊ฒ ๋์ํด์ผ ํ๋ ์ฝ๋๋ฅผ ์ฌ๊ธฐ์ ์์ฑํด.
Resources ํด๋
์ด๋ฏธ์ง, ํฐํธ, ์คํ์ผ ๋ฑ ๋ฆฌ์์ค ํ์ผ๋ค์ด ๋ชจ์ฌ์์ด. ๋ชจ๋ ํ๋ซํผ์์ ๊ณต์ ๋๋ ๋ฆฌ์์ค๋ฅผ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ง.
๐ MauiProgram.cs ์์
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// ์๋น์ค ๋ฑ๋ก
builder.Services.AddSingleton<MainPage>();
builder.Services.AddTransient<DetailPage>();
return builder.Build();
}
}
์ด ์ฝ๋๋ ์ฑ์ ์ด๊ธฐํํ๊ณ , ํฐํธ๋ฅผ ๋ฑ๋กํ๊ณ , ์์กด์ฑ ์ฃผ์ ์ปจํ ์ด๋์ ํ์ด์ง๋ค์ ๋ฑ๋กํ๋ ๊ณผ์ ์ด์ผ. .NET์ ํ์ค DI ํจํด์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ด์ ์ ๋ง ํธ๋ฆฌํด! ๐ฏ
๐จ XAML๋ก UI ๋ง๋ค๊ธฐ
MAUI์์ UI๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง์ผ. XAML์ ์ฌ์ฉํ๊ฑฐ๋, C# ์ฝ๋๋ก ์ง์ ์์ฑํ๊ฑฐ๋. ๋๋ถ๋ถ์ XAML์ ์ ํธํ๋๋ฐ, ์ ์ธ์ ์ด๊ณ ์ง๊ด์ ์ด๊ฑฐ๋ . ๋ง์น HTML ๊ฐ์ ๋๋์ด์ผ!
๐ผ๏ธ XAML ๊ธฐ๋ณธ ๊ตฌ์กฐ
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="200"
HorizontalOptions="Center" />
<Label
Text="Hello, World!"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
<Label
Text="Welcome to .NET MAUI!"
SemanticProperties.HeadingLevel="Level2"
FontSize="18"
HorizontalOptions="Center" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
์ด ์ฝ๋๋ฅผ ๋ณด๋ฉด ๊ตฌ์กฐ๊ฐ ํ๋์ ๋ค์ด์ค์ง? ์ด๋ฏธ์ง, ๋ผ๋ฒจ, ๋ฒํผ์ด ์ธ๋ก๋ก ๋ฐฐ์น๋์ด ์์ด. ๊ฐ ์์์ ์์ฑ๋ ๋ช ํํ๊ฒ ํํ๋์ด ์๊ณ .
๐งฉ ์ฃผ์ ๋ ์ด์์ ์ปจํธ๋กค
StackLayout / VerticalStackLayout / HorizontalStackLayout
์์๋ค์ ์ธ๋ก ๋๋ ๊ฐ๋ก๋ก ์์์ฌ๋ฆฌ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ ์ด์์์ด์ผ. ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ด์ง.
Grid
ํ๊ณผ ์ด๋ก ๊ตฌ์ฑ๋ ํ
์ด๋ธ ํํ์ ๋ ์ด์์์ด์ผ. ๋ณต์กํ UI๋ฅผ ๋ง๋ค ๋ ์ ๋ง ์ ์ฉํด. HTML์ table์ด๋ CSS Grid์ ๋น์ทํด.
FlexLayout
CSS Flexbox์ ์ ์ฌํ ๋ ์ด์์์ด์ผ. ๋ฐ์ํ ๋์์ธ์ ๋ง๋ค ๋ ๊ฐ๋ ฅํ์ง.
AbsoluteLayout
์ ๋ ์ขํ๋ก ์์๋ฅผ ๋ฐฐ์นํด. ์ ํํ ์์น ์ ์ด๊ฐ ํ์ํ ๋ ์ฌ์ฉํ์ง๋ง, ๋ฐ์ํ ๋์์ธ์๋ ์ ํฉํ์ง ์์.
ScrollView
๋ด์ฉ์ด ํ๋ฉด์ ๋์ด๊ฐ ๋ ์คํฌ๋กค์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ์ปจํ
์ด๋์ผ.
๐ Grid ๋ ์ด์์ ์์
<Grid RowDefinitions="Auto,*,Auto"
ColumnDefinitions="*,*"
Padding="20"
RowSpacing="10"
ColumnSpacing="10">
<!-- ํค๋ (์ฒซ ๋ฒ์งธ ํ, ๋ ์ด์ ๊ฑธ์ณ) -->
<Label Text="My App"
Grid.Row="0"
Grid.ColumnSpan="2"
FontSize="24"
HorizontalOptions="Center"/>
<!-- ์ผ์ชฝ ์ฝํ
์ธ -->
<VerticalStackLayout Grid.Row="1" Grid.Column="0">
<Label Text="Left Panel"/>
<Button Text="Button 1"/>
</VerticalStackLayout>
<!-- ์ค๋ฅธ์ชฝ ์ฝํ
์ธ -->
<VerticalStackLayout Grid.Row="1" Grid.Column="1">
<Label Text="Right Panel"/>
<Button Text="Button 2"/>
</VerticalStackLayout>
<!-- ํธํฐ -->
<Label Text="Footer"
Grid.Row="2"
Grid.ColumnSpan="2"
HorizontalOptions="Center"/>
</Grid>
Grid๋ RowDefinitions์ ColumnDefinitions๋ก ํ๊ณผ ์ด์ ํฌ๊ธฐ๋ฅผ ์ ์ํด. "*"๋ ๋จ์ ๊ณต๊ฐ์ ์ฑ์ฐ๊ณ , "Auto"๋ ๋ด์ฉ์ ๋ง์ถฐ ํฌ๊ธฐ๊ฐ ์กฐ์ ๋๊ณ , ์ซ์๋ฅผ ์ง์ ์ฐ๋ฉด ๊ณ ์ ํฌ๊ธฐ๊ฐ ๋ผ.
๐พ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ๊ณผ MVVM ํจํด
MAUI์ ์ง์ง ๊ฐ๋ ฅํจ์ **๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ**๊ณผ **MVVM(Model-View-ViewModel) ํจํด**์์ ๋์. ์ด ํจํด์ ์ฌ์ฉํ๋ฉด UI์ ๋น์ฆ๋์ค ๋ก์ง์ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ ์ ์์ด. ๐ฏ
๐ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ด๋?
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ UI ์์์ ๋ฐ์ดํฐ ์์ค๋ฅผ ์ฐ๊ฒฐํ๋ ๋ฉ์ปค๋์ฆ์ด์ผ. ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋๋ฉด UI๊ฐ ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋๊ณ , ๋ฐ๋๋ก UI์์ ์ ๋ ฅํ ๊ฐ์ด ๋ฐ์ดํฐ ์์ค์ ์๋์ผ๋ก ๋ฐ์๋ผ.
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ํ ์คํธ ๋ฐ์ค์ ์ด๋ฆ์ ์ ๋ ฅํ๋ฉด, ๊ทธ ๊ฐ์ด ์๋์ผ๋ก ViewModel์ ์์ฑ์ ์ ์ฅ๋๊ณ , ๋ค๋ฅธ ๋ผ๋ฒจ์๋ ์ค์๊ฐ์ผ๋ก ํ์๋๋ ๊ฑฐ์ง. ์ฝ๋ ํ ์ค ์์ด!
๐๏ธ MVVM ํจํด ๊ตฌ์กฐ
Model (๋ชจ๋ธ)
๋ฐ์ดํฐ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํด. ๋ฐ์ดํฐ๋ฒ ์ด์ค, API, ํ์ผ ์์คํ
๋ฑ๊ณผ ์ํธ์์ฉํ์ง.
View (๋ทฐ)
์ฌ์ฉ์์๊ฒ ๋ณด์ด๋ UI์ผ. XAML ํ์ผ์ด ์ฌ๊ธฐ์ ํด๋นํด. ๊ฐ๋ฅํ ํ ๋ก์ง์ ํฌํจํ์ง ์๊ณ , ์์ํ๊ฒ ํํ๋ง ๋ด๋นํด์ผ ํด.
ViewModel (๋ทฐ๋ชจ๋ธ)
View์ Model ์ฌ์ด์ ์ค์ฌ์์ผ. View์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ค๋นํ๊ณ , ์ฌ์ฉ์ ์
๋ ฅ์ ์ฒ๋ฆฌํด์ Model์ ์ ๋ฌํ์ง. INotifyPropertyChanged ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ View์ ์๋ ค์ค.
๐ ViewModel ์์
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
public class MainViewModel : INotifyPropertyChanged
{
private string _name;
private int _count;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Greeting));
}
}
}
public int Count
{
get => _count;
set
{
if (_count != value)
{
_count = value;
OnPropertyChanged();
}
}
}
public string Greeting => $"Hello, {Name}!";
public ICommand IncrementCommand { get; }
public MainViewModel()
{
Name = "World";
Count = 0;
IncrementCommand = new Command(() => Count++);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
๐จ View (XAML) ์์
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.MainPage">
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="30" Spacing="20">
<Label Text="Enter your name:" FontSize="18"/>
<Entry Text="{Binding Name}"
Placeholder="Your name"
FontSize="16"/>
<Label Text="{Binding Greeting}"
FontSize="24"
FontAttributes="Bold"
HorizontalOptions="Center"/>
<Button Text="{Binding Count, StringFormat='Clicked {0} times'}"
Command="{Binding IncrementCommand}"
FontSize="18"/>
</VerticalStackLayout>
</ContentPage>
์ด ์ฝ๋๋ฅผ ๋ณด๋ฉด, Entry์ Text ์์ฑ์ด {Binding Name}์ผ๋ก ๋ฐ์ธ๋ฉ๋์ด ์์ด. ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ฉด ViewModel์ Name ์์ฑ์ด ์๋์ผ๋ก ์
๋ฐ์ดํธ๋๊ณ , ๊ทธ๋ฌ๋ฉด Greeting ์์ฑ๋ ๋ณ๊ฒฝ๋์ด Label์ ํ์๋๋ ๊ฑฐ์ง. ๋ง๋ฒ ๊ฐ์ง? โจ
[ObservableProperty] ์ดํธ๋ฆฌ๋ทฐํธ๋ง ๋ถ์ด๋ฉด ์๋์ผ๋ก ํ๋กํผํฐ ๋ณ๊ฒฝ ์๋ฆผ์ด ๊ตฌํ๋ผ. ์ ๋ง ํธ๋ฆฌํด!
๐บ๏ธ ๋ค๋น๊ฒ์ด์ ๊ณผ Shell
์ฑ์์ ํ๋ฉด ๊ฐ ์ด๋์ ํ์์ ์ด์ง. MAUI๋ **Shell**์ด๋ผ๋ ๊ฐ๋ ฅํ ๋ค๋น๊ฒ์ด์ ์์คํ ์ ์ ๊ณตํด. Shell์ ์ฌ์ฉํ๋ฉด ๋ณต์กํ ๋ค๋น๊ฒ์ด์ ๊ตฌ์กฐ๋ ์ฝ๊ฒ ๋ง๋ค ์ ์์ด. ๐ข
๐ Shell์ ์ฃผ์ ๊ธฐ๋ฅ
ํ๋ผ์ด์์ ๋ฉ๋ด (Flyout)
ํ๋ฒ๊ฑฐ ๋ฉ๋ด์ฒ๋ผ ์์์ ์ฌ๋ผ์ด๋๋๋ ๋ฉ๋ด์ผ. ์ฃผ์ ๋ค๋น๊ฒ์ด์
ํญ๋ชฉ์ ๋ด์ ์ ์์ง.
ํญ ๋ฐ (Tab Bar)
ํ๋ฉด ํ๋จ์ ํ์๋๋ ํญ ๋ฉ๋ด์ผ. ์ฃผ์ ์น์
๊ฐ ๋น ๋ฅธ ์ ํ์ด ๊ฐ๋ฅํด.
URI ๊ธฐ๋ฐ ๋ผ์ฐํ
์น์ฒ๋ผ URI๋ฅผ ์ฌ์ฉํด์ ํ์ด์ง๋ฅผ ํ์ํ ์ ์์ด. ๋ฅ๋งํฌ ๊ตฌํ๋ ์ฌ์์ง์ง.
๊ฒ์ ๊ธฐ๋ฅ
Shell์ ๋ด์ฅ๋ ๊ฒ์ ๊ธฐ๋ฅ์ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์์ด.
๐๏ธ AppShell.xaml ์์
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell"
FlyoutBehavior="Flyout">
<!-- ํ๋ผ์ด์์ ํค๋ -->
<Shell.FlyoutHeader>
<Grid BackgroundColor="#667eea" HeightRequest="150">
<Label Text="My App"
TextColor="White"
FontSize="24"
VerticalOptions="Center"
HorizontalOptions="Center"/>
</Grid>
</Shell.FlyoutHeader>
<!-- ํ๋ผ์ด์์ ๋ฉ๋ด ํญ๋ชฉ -->
<FlyoutItem Title="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate local:HomePage}"/>
</FlyoutItem>
<FlyoutItem Title="Settings" Icon="settings.png">
<ShellContent ContentTemplate="{DataTemplate local:SettingsPage}"/>
</FlyoutItem>
<!-- ํญ ๋ฐ -->
<TabBar>
<Tab Title="Browse" Icon="browse.png">
<ShellContent ContentTemplate="{DataTemplate local:BrowsePage}"/>
</Tab>
<Tab Title="About" Icon="info.png">
<ShellContent ContentTemplate="{DataTemplate local:AboutPage}"/>
</Tab>
</TabBar>
</Shell>
๐งญ ๋ผ์ฐํ ๋ค๋น๊ฒ์ด์
Shell์ ๋ผ์ฐํ ์์คํ ์ ์ฌ์ฉํ๋ฉด ํ์ด์ง ๊ฐ ์ด๋์ด ์ ๋ง ๊ฐ๋จํด์ ธ:
// AppShell.xaml.cs์์ ๋ผ์ฐํธ ๋ฑ๋ก
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("details", typeof(DetailPage));
Routing.RegisterRoute("edit", typeof(EditPage));
}
// ํ์ด์ง ์ด๋
await Shell.Current.GoToAsync("details");
// ํ๋ผ๋ฏธํฐ์ ํจ๊ป ์ด๋
await Shell.Current.GoToAsync($"details?id={itemId}");
// ๋ค๋ก ๊ฐ๊ธฐ
await Shell.Current.GoToAsync("..");
// ๋ฃจํธ๋ก ์ด๋
await Shell.Current.GoToAsync("//home");
ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ ํ์ด์ง๋ ์ด๋ ๊ฒ ๊ตฌํํด:
[QueryProperty(nameof(ItemId), "id")]
public partial class DetailPage : ContentPage
{
private string _itemId;
public string ItemId
{
get => _itemId;
set
{
_itemId = value;
LoadItemDetails(value);
}
}
public DetailPage()
{
InitializeComponent();
}
private void LoadItemDetails(string id)
{
// ID๋ฅผ ์ฌ์ฉํด์ ์์ธ ์ ๋ณด ๋ก๋
}
}
QueryProperty ์ดํธ๋ฆฌ๋ทฐํธ๋ฅผ ์ฌ์ฉํ๋ฉด URL ํ๋ผ๋ฏธํฐ๋ฅผ ์๋์ผ๋ก ์์ฑ์ ๋ฐ์ธ๋ฉํด์ค. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
๐ฑ ํ๋ซํผ๋ณ ๊ธฐ๋ฅ ํ์ฉํ๊ธฐ
ํฌ๋ก์คํ๋ซํผ์ด๋ผ๊ณ ํด์ ๊ฐ ํ๋ซํผ์ ๊ณ ์ ๊ธฐ๋ฅ์ ํฌ๊ธฐํ๋ ๊ฑด ์๋์ผ. MAUI๋ ํ๋ซํผ๋ณ API์ ์ ๊ทผํ ์ ์๋ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ ์ ๊ณตํด. ๐ฏ
๐ง ์กฐ๊ฑด๋ถ ์ปดํ์ผ
๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ ์กฐ๊ฑด๋ถ ์ปดํ์ผ ์ฌ๋ณผ์ ์ฌ์ฉํ๋ ๊ฑฐ์ผ:
public void DoSomethingPlatformSpecific()
{
#if ANDROID
// Android ์ ์ฉ ์ฝ๋
Android.Widget.Toast.MakeText(
Platform.CurrentActivity,
"Hello from Android!",
Android.Widget.ToastLength.Short
).Show();
#elif IOS
// iOS ์ ์ฉ ์ฝ๋
var alert = UIKit.UIAlertController.Create(
"Hello",
"Hello from iOS!",
UIKit.UIAlertControllerStyle.Alert
);
// ... ์๋ฆผ ํ์
#elif WINDOWS
// Windows ์ ์ฉ ์ฝ๋
var dialog = new Windows.UI.Popups.MessageDialog("Hello from Windows!");
await dialog.ShowAsync();
#endif
}
๐จ ํ๋ซํผ๋ณ UI ์ปค์คํฐ๋ง์ด์ง
XAML์์๋ ํ๋ซํผ๋ณ๋ก ๋ค๋ฅธ ๊ฐ์ ์ค์ ํ ์ ์์ด:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="iOS" Value="0,20,0,0" />
<On Platform="Android" Value="0,0,0,0" />
</OnPlatform>
</ContentPage.Padding>
<Label>
<Label.FontSize>
<OnPlatform x:TypeArguments="x:Double">
<On Platform="iOS" Value="16" />
<On Platform="Android" Value="18" />
<On Platform="Windows" Value="20" />
</OnPlatform>
</Label.FontSize>
</Label>
</ContentPage>
iOS์ ๋ ธ์น ์์ญ์ ํผํ๊ธฐ ์ํด ์๋จ ํจ๋ฉ์ ์ถ๊ฐํ๊ฑฐ๋, ํ๋ซํผ๋ณ๋ก ๋ค๋ฅธ ํฐํธ ํฌ๊ธฐ๋ฅผ ์ค์ ํ ์ ์์ด.
๐ ์์กด์ฑ ์ฃผ์ ์ ํตํ ํ๋ซํผ ์๋น์ค
๋ ๊น๋ํ ๋ฐฉ๋ฒ์ ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๊ณ , ๊ฐ ํ๋ซํผ์์ ๊ตฌํํ๋ ๊ฑฐ์ผ:
// ๊ณตํต ์ธํฐํ์ด์ค
public interface IDeviceOrientationService
{
DeviceOrientation GetOrientation();
}
// Android ๊ตฌํ (Platforms/Android ํด๋)
public class DeviceOrientationService : IDeviceOrientationService
{
public DeviceOrientation GetOrientation()
{
var rotation = Platform.CurrentActivity.WindowManager
.DefaultDisplay.Rotation;
// ... ํ์ ๊ฐ์ DeviceOrientation์ผ๋ก ๋ณํ
return orientation;
}
}
// iOS ๊ตฌํ (Platforms/iOS ํด๋)
public class DeviceOrientationService : IDeviceOrientationService
{
public DeviceOrientation GetOrientation()
{
var orientation = UIKit.UIApplication.SharedApplication
.StatusBarOrientation;
// ... ๋ณํ ๋ก์ง
return orientation;
}
}
// MauiProgram.cs์์ ๋ฑ๋ก
#if ANDROID
builder.Services.AddSingleton<IDeviceOrientationService,
Platforms.Android.DeviceOrientationService>();
#elif IOS
builder.Services.AddSingleton<IDeviceOrientationService,
Platforms.iOS.DeviceOrientationService>();
#endif
์ด๋ ๊ฒ ํ๋ฉด ๊ณตํต ์ฝ๋์์๋ ์ธํฐํ์ด์ค๋ง ์ฌ์ฉํ๊ณ , ์ค์ ๊ตฌํ์ ๊ฐ ํ๋ซํผ์์ ์ฒ๋ฆฌํ ์ ์์ด. ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด์ง์ง! ๐จ
๐ .NET MAUI Essentials
MAUI์๋ **Essentials**๋ผ๋ ํฌ๋ก์คํ๋ซํผ API ๋ชจ์์ด ๋ด์ฅ๋์ด ์์ด. ์์น, ์ผ์, ํ์ผ ์์คํ , ๋คํธ์ํฌ ๋ฑ ์์ฃผ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ๋ค์ ํ๋ซํผ ๊ตฌ๋ถ ์์ด ์ฌ์ฉํ ์ ์์ง. ๐
๐ ๏ธ ์ฃผ์ Essentials API
์์น ์ ๋ณด (Geolocation)
var location = await Geolocation.GetLocationAsync(new GeolocationRequest
{
DesiredAccuracy = GeolocationAccuracy.Medium,
Timeout = TimeSpan.FromSeconds(10)
});
if (location != null)
{
Console.WriteLine($"์๋: {location.Latitude}, ๊ฒฝ๋: {location.Longitude}");
}
์ฐ๊ฒฐ ์ํ (Connectivity)
var current = Connectivity.NetworkAccess;
if (current == NetworkAccess.Internet)
{
// ์ธํฐ๋ท ์ฐ๊ฒฐ๋จ
}
// ์ฐ๊ฒฐ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
Connectivity.ConnectivityChanged += (sender, e) =>
{
if (e.NetworkAccess == NetworkAccess.Internet)
{
// ์ธํฐ๋ท ์ฐ๊ฒฐ๋จ
}
};
๋ฐฐํฐ๋ฆฌ ์ ๋ณด (Battery)
var level = Battery.ChargeLevel; // 0.0 ~ 1.0
var state = Battery.State; // Charging, Discharging, Full, etc.
var source = Battery.PowerSource; // Battery, AC, Usb, Wireless
Battery.BatteryInfoChanged += (sender, e) =>
{
var newLevel = e.ChargeLevel;
// ๋ฐฐํฐ๋ฆฌ ๋ ๋ฒจ ๋ณ๊ฒฝ ์ฒ๋ฆฌ
};
ํ์ผ ์ ํ๊ธฐ (FilePicker)
var result = await FilePicker.PickAsync(new PickOptions
{
PickerTitle = "์ด๋ฏธ์ง๋ฅผ ์ ํํ์ธ์",
FileTypes = FilePickerFileType.Images
});
if (result != null)
{
var stream = await result.OpenReadAsync();
// ํ์ผ ์ฒ๋ฆฌ
}
๊ณต์ ๊ธฐ๋ฅ (Share)
await Share.RequestAsync(new ShareTextRequest
{
Text = "MAUI๋ ์ ๋ง ๋ฉ์ ธ์!",
Title = "๊ณต์ ํ๊ธฐ"
});
// ํ์ผ ๊ณต์
await Share.RequestAsync(new ShareFileRequest
{
Title = "์ฌ์ง ๊ณต์ ",
File = new ShareFile(filePath)
});
์ง๋ (Vibration)
// ๊ธฐ๋ณธ ์ง๋
Vibration.Vibrate();
// ์ง์ ๋ ์๊ฐ ๋์ ์ง๋
Vibration.Vibrate(TimeSpan.FromSeconds(1));
// ์ง๋ ์ทจ์
Vibration.Cancel();
์์น, ์นด๋ฉ๋ผ, ๋ง์ดํฌ ๋ฑ ๋ฏผ๊ฐํ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ค๋ฉด ๊ถํ์ด ํ์ํด. MAUI๋ ๊ถํ ์์ฒญ๋ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด:
var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();if (status != PermissionStatus.Granted){status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();}๊ฐ ํ๋ซํผ์ ๋งค๋ํ์คํธ ํ์ผ์๋ ๊ถํ์ ์ ์ธํด์ผ ํ๋ค๋ ๊ฑธ ์์ง ๋ง!
๐พ ๋ฐ์ดํฐ ์ ์ฅ๊ณผ ๊ด๋ฆฌ
์ฑ์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ด. ๊ฐ๋จํ ์ค์ ๊ฐ๋ถํฐ ๋ณต์กํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง, ์ํฉ์ ๋ง๋ ๋ฐฉ๋ฒ์ ์ ํํ๋ฉด ๋ผ. ๐
๐ Preferences (๊ฐ๋จํ ํค-๊ฐ ์ ์ฅ)
์ค์ ๊ฐ์ด๋ ๊ฐ๋จํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ์ฌ์ฉํด:
// ์ ์ฅ
Preferences.Set("username", "ํ๊ธธ๋");
Preferences.Set("age", 25);
Preferences.Set("is_premium", true);
// ์ฝ๊ธฐ
var username = Preferences.Get("username", "๊ธฐ๋ณธ๊ฐ");
var age = Preferences.Get("age", 0);
var isPremium = Preferences.Get("is_premium", false);
// ์ญ์
Preferences.Remove("username");
// ๋ชจ๋ ์ญ์
Preferences.Clear();
Preferences๋ ๊ฐ ํ๋ซํผ์ ๋ค์ดํฐ๋ธ ์ ์ฅ์๋ฅผ ์ฌ์ฉํด. Android๋ SharedPreferences, iOS๋ NSUserDefaults, Windows๋ ApplicationDataContainer๋ฅผ ์ฌ์ฉํ์ง.
๐ SecureStorage (๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ์ฅ)
๋น๋ฐ๋ฒํธ, ํ ํฐ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ SecureStorage๋ฅผ ์ฌ์ฉํด:
// ์ ์ฅ
await SecureStorage.SetAsync("oauth_token", "abc123xyz");
// ์ฝ๊ธฐ
var token = await SecureStorage.GetAsync("oauth_token");
// ์ญ์
SecureStorage.Remove("oauth_token");
// ๋ชจ๋ ์ญ์
SecureStorage.RemoveAll();
SecureStorage๋ ๊ฐ ํ๋ซํผ์ ๋ณด์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํด. Android๋ KeyStore, iOS๋ Keychain์ ํ์ฉํ์ง. ์ํธํ๋์ด ์์ ํ๊ฒ ์ ์ฅ๋ผ! ๐
๐๏ธ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค
๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ ๋ง์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ SQLite๋ฅผ ์ฌ์ฉํด. **sqlite-net-pcl** NuGet ํจํค์ง๋ฅผ ์ค์นํ๋ฉด ๋ผ:
using SQLite;
// ๋ชจ๋ธ ์ ์
public class Person
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(100)]
public string Name { get; set; }
public int Age { get; set; }
[Indexed]
public string Email { get; set; }
}
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋น์ค
public class DatabaseService
{
private SQLiteAsyncConnection _database;
public DatabaseService()
{
var dbPath = Path.Combine(
FileSystem.AppDataDirectory,
"myapp.db3"
);
_database = new SQLiteAsyncConnection(dbPath);
_database.CreateTableAsync<Person>().Wait();
}
// ์ถ๊ฐ
public Task<int> SavePersonAsync(Person person)
{
if (person.Id != 0)
return _database.UpdateAsync(person);
else
return _database.InsertAsync(person);
}
// ์กฐํ
public Task<List<Person>> GetPeopleAsync()
{
return _database.Table<Person>().ToListAsync();
}
public Task<Person> GetPersonAsync(int id)
{
return _database.Table<Person>()
.Where(p => p.Id == id)
.FirstOrDefaultAsync();
}
// ์ญ์
public Task<int> DeletePersonAsync(Person person)
{
return _database.DeleteAsync(person);
}
// ๊ฒ์
public Task<List<Person>> SearchPeopleAsync(string query)
{
return _database.Table<Person>()
.Where(p => p.Name.Contains(query))
.ToListAsync();
}
}
SQLite๋ ๊ฐ๋ณ๊ณ ๋น ๋ฅด๋ฉฐ, ์๋ฒ ์์ด ๋ก์ปฌ์์ ๋์ํด. ์คํ๋ผ์ธ ์ฑ์ ๋ง๋ค ๋ ์ ๋ง ์ ์ฉํ์ง! ๐ฑ
๐ REST API ํต์
๋๋ถ๋ถ์ ์ฑ์ ์๋ฒ์ ํต์ ํด์ผ ํด. MAUI์์๋ .NET์ **HttpClient**๋ฅผ ์ฌ์ฉํด์ REST API๋ฅผ ํธ์ถํ ์ ์์ด. ๐
๐ HttpClient ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
public class ApiService
{
private readonly HttpClient _httpClient;
private const string BaseUrl = "https://api.example.com";
public ApiService()
{
_httpClient = new HttpClient
{
BaseAddress = new Uri(BaseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
// ๊ธฐ๋ณธ ํค๋ ์ค์
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
// GET ์์ฒญ
public async Task<List<Product>> GetProductsAsync()
{
try
{
var response = await _httpClient.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(json);
return products;
}
catch (HttpRequestException ex)
{
// ๋คํธ์ํฌ ์ค๋ฅ ์ฒ๋ฆฌ
Console.WriteLine($"Error: {ex.Message}");
return new List<Product>();
}
}
// POST ์์ฒญ
public async Task<bool> CreateProductAsync(Product product)
{
try
{
var json = JsonSerializer.Serialize(product);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/products", content);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return false;
}
}
// PUT ์์ฒญ
public async Task<bool> UpdateProductAsync(int id, Product product)
{
var json = JsonSerializer.Serialize(product);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PutAsync($"/api/products/{id}", content);
return response.IsSuccessStatusCode;
}
// DELETE ์์ฒญ
public async Task<bool> DeleteProductAsync(int id)
{
var response = await _httpClient.DeleteAsync($"/api/products/{id}");
return response.IsSuccessStatusCode;
}
}
๐ ์ธ์ฆ ํ ํฐ ์ฒ๋ฆฌ
๋๋ถ๋ถ์ API๋ ์ธ์ฆ์ด ํ์ํด. Bearer ํ ํฐ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ:
public class AuthenticatedApiService
{
private readonly HttpClient _httpClient;
public AuthenticatedApiService()
{
_httpClient = new HttpClient();
}
public async Task<bool> LoginAsync(string username, string password)
{
var loginData = new { username, password };
var json = JsonSerializer.Serialize(loginData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
"https://api.example.com/auth/login",
content
);
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(responseJson);
// ํ ํฐ์ SecureStorage์ ์ ์ฅ
await SecureStorage.SetAsync("auth_token", tokenResponse.Token);
// HttpClient ํค๋์ ํ ํฐ ์ถ๊ฐ
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", tokenResponse.Token);
return true;
}
return false;
}
public async Task<List<UserData>> GetUserDataAsync()
{
// ์ ์ฅ๋ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
var token = await SecureStorage.GetAsync("auth_token");
if (string.IsNullOrEmpty(token))
{
throw new UnauthorizedAccessException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
}
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.GetAsync("https://api.example.com/api/userdata");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<List<UserData>>(json);
}
}
HttpClient๋ฅผ ์ง์ ์ฌ์ฉํ๋ ๊ฒ๋ ์ข์ง๋ง, **Refit** ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๊ฒฐํ๊ฒ API๋ฅผ ํธ์ถํ ์ ์์ด:
public interface IApiService{ย ย ย ย [Get("/api/products")]Task<List<Product>> GetProducts();ย ย ย ย [Post("/api/products")]Task<Product> CreateProduct([Body] Product product);}์ธํฐํ์ด์ค๋ง ์ ์ํ๋ฉด Refit์ด ์๋์ผ๋ก ๊ตฌํ์ฒด๋ฅผ ์์ฑํด์ค. ์ ๋ง ํธ๋ฆฌํ์ง? ๐ฏ
๐จ ์ปค์คํ ์ปจํธ๋กค๊ณผ ํธ๋ค๋ฌ
MAUI์ ๊ธฐ๋ณธ ์ปจํธ๋กค๋ก ๋ถ์กฑํ๋ค๋ฉด? ์ง์ ๋ง๋ค๋ฉด ๋ผ! ์ปค์คํ ์ปจํธ๋กค์ ๋ง๋ค๊ฑฐ๋, ๊ธฐ์กด ์ปจํธ๋กค์ ํ์ฅํ ์ ์์ด. ๐ ๏ธ
๐ฏ ์ปค์คํ ์ปจํธ๋กค ๋ง๋ค๊ธฐ
๊ฐ๋จํ ์ปค์คํ ๋ฒํผ์ ๋ง๋ค์ด๋ณด์:
public class GradientButton : Button
{
public static readonly BindableProperty StartColorProperty =
BindableProperty.Create(
nameof(StartColor),
typeof(Color),
typeof(GradientButton),
Colors.Blue
);
public static readonly BindableProperty EndColorProperty =
BindableProperty.Create(
nameof(EndColor),
typeof(Color),
typeof(GradientButton),
Colors.Purple
);
public Color StartColor
{
get => (Color)GetValue(StartColorProperty);
set => SetValue(StartColorProperty, value);
}
public Color EndColor
{
get => (Color)GetValue(EndColorProperty);
set => SetValue(EndColorProperty, value);
}
}
XAML์์ ์ฌ์ฉ:
<ContentPage xmlns:local="clr-namespace:MyApp.Controls">
<local:GradientButton
Text="๋ฉ์ง ๋ฒํผ"
StartColor="#667eea"
EndColor="#764ba2"
TextColor="White"
CornerRadius="20"
HeightRequest="50" />
</ContentPage>
๐ง ํธ๋ค๋ฌ๋ก ๋ค์ดํฐ๋ธ ๋ ๋๋ง ์ปค์คํฐ๋ง์ด์ง
์ค์ ๊ทธ๋ผ๋์ธํธ ํจ๊ณผ๋ฅผ ์ ์ฉํ๋ ค๋ฉด ๊ฐ ํ๋ซํผ์ ๋ค์ดํฐ๋ธ ์ฝ๋๊ฐ ํ์ํด. ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ:
// Android ํธ๋ค๋ฌ (Platforms/Android ํด๋)
#if ANDROID
using Microsoft.Maui.Handlers;
using Android.Graphics.Drawables;
public class GradientButtonHandler : ButtonHandler
{
protected override void ConnectHandler(AppCompatButton platformView)
{
base.ConnectHandler(platformView);
UpdateGradient();
}
private void UpdateGradient()
{
if (VirtualView is GradientButton gradientButton)
{
var startColor = gradientButton.StartColor.ToAndroid();
var endColor = gradientButton.EndColor.ToAndroid();
var gradient = new GradientDrawable(
GradientDrawable.Orientation.LeftRight,
new[] { startColor, endColor }
);
gradient.SetCornerRadius(20 * Resources.System.DisplayMetrics.Density);
PlatformView.Background = gradient;
}
}
}
#endif
// MauiProgram.cs์์ ํธ๋ค๋ฌ ๋ฑ๋ก
builder.ConfigureMauiHandlers(handlers =>
{
#if ANDROID
handlers.AddHandler<GradientButton, GradientButtonHandler>();
#endif
});
iOS์ Windows์๋ ๋น์ทํ๊ฒ ๊ตฌํํ๋ฉด ๋ชจ๋ ํ๋ซํผ์์ ๋ฉ์ง ๊ทธ๋ผ๋์ธํธ ๋ฒํผ์ ์ฌ์ฉํ ์ ์์ด! ๐
๐ ์ฑ๋ฅ ์ต์ ํ ํ
์ฑ์ด ๋๋ฆฌ๋ฉด ์ฌ์ฉ์๋ค์ด ๋ ๋๊ฐ. ์ฑ๋ฅ ์ต์ ํ๋ ์ ๋ง ์ค์ํด! ๋ช ๊ฐ์ง ํ์ ์๋ ค์ค๊ฒ. โก
๐ ์ฑ๋ฅ ํฅ์ ์ ๋ต
1๏ธโฃ CollectionView ๊ฐ์ํ ํ์ฉ
๊ธด ๋ฆฌ์คํธ๋ฅผ ํ์ํ ๋๋ ListView ๋์ CollectionView๋ฅผ ์ฌ์ฉํด. ๊ฐ์ํ๊ฐ ๋ ํจ์จ์ ์ด์ผ:
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectedItem="{Binding SelectedItem}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10">
<Label Text="{Binding Name}" FontSize="16"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
2๏ธโฃ ์ด๋ฏธ์ง ์ต์ ํ
ํฐ ์ด๋ฏธ์ง๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ง์ด ์ฐจ์งํด. ์ ์ ํ ํฌ๊ธฐ๋ก ๋ฆฌ์ฌ์ด์งํ๊ณ , ์บ์ฑ์ ํ์ฉํด:
<Image Source="{Binding ImageUrl}"
Aspect="AspectFill"
HeightRequest="200"
WidthRequest="200">
<Image.Behaviors>
<toolkit:ImageCachingBehavior CacheDuration="7" />
</Image.Behaviors>
</Image>
3๏ธโฃ ๋น๋๊ธฐ ์์
ํ์ฉ
๋ฌด๊ฑฐ์ด ์์
์ ํญ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด. UI ์ค๋ ๋๋ฅผ ๋ธ๋กํนํ๋ฉด ์ฑ์ด ๋ฉ์ถ ๊ฒ์ฒ๋ผ ๋ณด์ฌ:
public async Task LoadDataAsync()
{
IsLoading = true;
try
{
// ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ฐ์ดํฐ ๋ก๋
var data = await Task.Run(() => LoadHeavyData());
// UI ์
๋ฐ์ดํธ๋ ๋ฉ์ธ ์ค๋ ๋์์
await MainThread.InvokeOnMainThreadAsync(() =>
{
Items = new ObservableCollection<Item>(data);
});
}
finally
{
IsLoading = false;
}
}
4๏ธโฃ ์ปดํ์ผ๋ ๋ฐ์ธ๋ฉ ์ฌ์ฉ
XAML ๋ฐ์ธ๋ฉ์ ๋ฆฌํ๋ ์
์ ์ฌ์ฉํด์ ๋๋ฆด ์ ์์ด. ์ปดํ์ผ๋ ๋ฐ์ธ๋ฉ์ ์ฌ์ฉํ๋ฉด ์ฑ๋ฅ์ด ํฅ์๋ผ:
<ContentPage xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:DataType="vm:MainViewModel">
<!-- x:DataType์ ์ง์ ํ๋ฉด ์ปดํ์ผ ํ์์ ๋ฐ์ธ๋ฉ์ด ๊ฒ์ฆ๋๊ณ ์ต์ ํ๋จ -->
<Label Text="{Binding Name}" />
</ContentPage>
5๏ธโฃ ๋ถํ์ํ ๋ ์ด์์ ์ค์ฒฉ ํผํ๊ธฐ
๋ ์ด์์์ด ๊น๊ฒ ์ค์ฒฉ๋๋ฉด ๋ ๋๋ง ์ฑ๋ฅ์ด ๋จ์ด์ ธ. ๊ฐ๋ฅํ ํ ํํํ๊ฒ ์ ์งํด:
<!-- ๋์ ์ โ -->
<StackLayout>
<StackLayout>
<StackLayout>
<Label Text="Hello" />
</StackLayout>
</StackLayout>
</StackLayout>
<!-- ์ข์ ์ โ
-->
<Grid>
<Label Text="Hello" />
</Grid>
6๏ธโฃ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํ์ผ๋ฉด ๋ฐ๋์ ํด์ ํด์ผ ํด:
public class MyPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<object>(this, "UpdateData", OnDataUpdated);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<object>(this, "UpdateData");
}
private void OnDataUpdated(object sender)
{
// ๋ฐ์ดํฐ ์
๋ฐ์ดํธ ์ฒ๋ฆฌ
}
}
๐งช ํ ์คํ ๊ณผ ๋๋ฒ๊น
๋ฒ๊ทธ ์๋ ์ฑ์ ์์ด. ํ์ง๋ง ํ ์คํธ๋ฅผ ์ํ๋ฉด ๋ฒ๊ทธ๋ฅผ ์ต์ํํ ์ ์์ง! ๐
โ ๋จ์ ํ ์คํธ (Unit Testing)
๋น์ฆ๋์ค ๋ก์ง์ ๋จ์ ํ ์คํธ๋ก ๊ฒ์ฆํด. xUnit์ด๋ NUnit์ ์ฌ์ฉํ ์ ์์ด:
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
๐ญ UI ํ ์คํธ
UI ํ ์คํธ๋ ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค๋ฅผ ์๋ํํด. **Appium**์ด๋ **Xamarin.UITest**๋ฅผ ์ฌ์ฉํ ์ ์์ด:
using NUnit.Framework;
using Xamarin.UITest;
[TestFixture(Platform.Android)]
[TestFixture(Platform.iOS)]
public class LoginTests
{
IApp app;
Platform platform;
public LoginTests(Platform platform)
{
this.platform = platform;
}
[SetUp]
public void BeforeEachTest()
{
app = AppInitializer.StartApp(platform);
}
[Test]
public void SuccessfulLogin()
{
// ์ฌ์ฉ์ ์ด๋ฆ ์
๋ ฅ
app.EnterText(c => c.Marked("UsernameEntry"), "testuser");
// ๋น๋ฐ๋ฒํธ ์
๋ ฅ
app.EnterText(c => c.Marked("PasswordEntry"), "password123");
// ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ
app.Tap(c => c.Marked("LoginButton"));
// ํ ํ๋ฉด์ผ๋ก ์ด๋ํ๋์ง ํ์ธ
app.WaitForElement(c => c.Marked("HomeScreen"));
}
}
๐ ๋๋ฒ๊น ํ
ํซ ๋ฆฌ๋ก๋ ํ์ฉ
XAML์ด๋ C# ์ฝ๋๋ฅผ ์์ ํ๋ฉด ์ฆ์ ๋ฐ์๋ผ. ์ฑ์ ๋ค์ ์์ํ ํ์ ์์ด!
์ค๋จ์ (Breakpoint)
์ฝ๋ ์คํ์ ๋ฉ์ถ๊ณ ๋ณ์ ๊ฐ์ ํ์ธํ ์ ์์ด. ์กฐ๊ฑด๋ถ ์ค๋จ์ ๋ ์ค์ ๊ฐ๋ฅํด.
๋ก๊น
Debug.WriteLine()์ด๋ ๋ก๊น
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ์คํ ํ๋ฆ์ ์ถ์ ํด:
Debug.WriteLine($"์ฌ์ฉ์ ID: {userId}, ์ด๋ฆ: {userName}");
// ๋๋ ๊ตฌ์กฐํ๋ ๋ก๊น
_logger.LogInformation("์ฌ์ฉ์ {UserId}๊ฐ ๋ก๊ทธ์ธํ์ต๋๋ค.", userId);
์์ธ ์ฒ๋ฆฌ
์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์์ธํ ์ ๋ณด๋ฅผ ๋ก๊น
ํด:
try
{
await LoadDataAsync();
}
catch (Exception ex)
{
Debug.WriteLine($"๋ฐ์ดํฐ ๋ก๋ ์คํจ: {ex.Message}");
Debug.WriteLine($"์คํ ํธ๋ ์ด์ค: {ex.StackTrace}");
await DisplayAlert("์ค๋ฅ", "๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.", "ํ์ธ");
}
๐ฆ ๋ฐฐํฌ์ ํผ๋ธ๋ฆฌ์ฑ
์ฑ์ ๋ค ๋ง๋ค์์ผ๋ฉด ์ด์ ์ธ์์ ๊ณต๊ฐํ ์ฐจ๋ก์ผ! ๊ฐ ํ๋ซํผ๋ณ๋ก ๋ฐฐํฌ ๊ณผ์ ์ด ์กฐ๊ธ์ฉ ๋ฌ๋ผ. ๐
๐ iOS ์ฑ ๋ฐฐํฌ
1. Apple Developer ๊ณ์ ํ์
์ฐ๊ฐ $99์ ๋น์ฉ์ด ๋ค์ด. ๊ฐ์ธ ๋๋ ์กฐ์ง ๊ณ์ ์ ์ ํํ ์ ์์ด.
2. ์ธ์ฆ์์ ํ๋ก๋น์ ๋ ํ๋กํ ์์ฑ
Xcode๋ Apple Developer ์น์ฌ์ดํธ์์ ์์ฑํด. Visual Studio์์๋ ์๋์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด.
3. App Store Connect์ ์ฑ ๋ฑ๋ก
์ฑ ์ด๋ฆ, ์ค๋ช
, ์คํฌ๋ฆฐ์ท, ์นดํ
๊ณ ๋ฆฌ ๋ฑ์ ์ค์ ํด.
4. ์์นด์ด๋ธ ๋ฐ ์
๋ก๋
Visual Studio์์ "Archive for Publishing"์ ์ ํํ๊ณ , App Store Connect์ ์
๋ก๋ํด.
5. ์ฌ์ฌ ์ ์ถ
Apple์ ์ฌ์ฌ๋ฅผ ํต๊ณผํด์ผ ์ฑ ์คํ ์ด์ ๋ฑ๋ก๋ผ. ๋ณดํต 1-3์ผ ์ ๋ ๊ฑธ๋ ค.
๐ค Android ์ฑ ๋ฐฐํฌ
1. Google Play Console ๊ณ์
์ผํ์ฑ $25 ๋ฑ๋ก๋น๊ฐ ํ์ํด.
2. ํค์คํ ์ด ์์ฑ
์ฑ ์๋ช
์ ์ํ ํค์คํ ์ด ํ์ผ์ ์์ฑํด. ์ด ํ์ผ์ ์ ๋ ์์ด๋ฒ๋ฆฌ๋ฉด ์ ๋ผ!
keytool -genkey -v -keystore myapp.keystore -alias myapp
-keyalg RSA -keysize 2048 -validity 10000
3. Release ๋น๋ ์์ฑ
Visual Studio์์ Release ๋ชจ๋๋ก ๋น๋ํ๊ณ , APK ๋๋ AAB ํ์ผ์ ์์ฑํด.
4. Google Play Console์ ์
๋ก๋
์ฑ ์ ๋ณด, ์คํฌ๋ฆฐ์ท, ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋ฑ์ ์
๋ ฅํ๊ณ APK/AAB๋ฅผ ์
๋ก๋ํด.
5. ์ถ์
๋ด๋ถ ํ
์คํธ, ๋น๊ณต๊ฐ ํ
์คํธ, ๊ณต๊ฐ ํ
์คํธ๋ฅผ ๊ฑฐ์ณ ํ๋ก๋์
์ผ๋ก ์ถ์ํ ์ ์์ด.
๐ช Windows ์ฑ ๋ฐฐํฌ
1. Microsoft Store ๊ณ์
๊ฐ์ธ์ $19, ๊ธฐ์
์ $99์ ์ผํ์ฑ ๋น์ฉ์ด ๋ค์ด.
2. MSIX ํจํค์ง ์์ฑ
Visual Studio์์ "Publish" โ "Create App Packages"๋ฅผ ์ ํํด.
3. Microsoft Store์ ์ ์ถ
Partner Center์์ ์ฑ ์ ๋ณด๋ฅผ ์
๋ ฅํ๊ณ ํจํค์ง๋ฅผ ์
๋ก๋ํด.
4. ์ธ์ฆ ๋ฐ ์ถ์
Microsoft์ ์๋ ๋ฐ ์๋ ๊ฒํ ๋ฅผ ๊ฑฐ์ณ ์คํ ์ด์ ๋ฑ๋ก๋ผ.
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
MAUI๋ฅผ ๋ฐฐ์ฐ๋ ์ฌ์ ์์ ํผ์๊ฐ ์๋์ผ. ํ๋ฅญํ ๋ฆฌ์์ค์ ํ๋ฐํ ์ปค๋ฎค๋ํฐ๊ฐ ์์ด! ๐
๐ ๊ณต์ ๋ฌธ์ ๋ฐ ํํ ๋ฆฌ์ผ
Microsoft Learn
๋ฌด๋ฃ ์จ๋ผ์ธ ํ์ต ํ๋ซํผ์ด์ผ. MAUI ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ๊น์ง ์ฒด๊ณ์ ์ผ๋ก ๋ฐฐ์ธ ์ ์์ด. ์ค์ต ํ๊ฒฝ๋ ์ ๊ณต๋๊ณ !
.NET MAUI ๊ณต์ ๋ฌธ์
๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด๋ฅผ ์ป์ ์ ์๋ ๊ณณ์ด์ผ. API ๋ ํผ๋ฐ์ค, ๊ฐ์ด๋, ์ํ ์ฝ๋๊ฐ ๊ฐ๋ํด.
.NET MAUI Samples
GitHub์ ๊ณต๊ฐ๋ ์ํ ํ๋ก์ ํธ๋ค์ด์ผ. ์ค์ ๋์ํ๋ ์ฝ๋๋ฅผ ๋ณด๋ฉด์ ๋ฐฐ์ธ ์ ์์ด.
๐ฅ ์ปค๋ฎค๋ํฐ
Stack Overflow
๋งํ๋ ๋ฌธ์ ๊ฐ ์์ ๋ ๊ฒ์ํ๋ฉด ๋๋ถ๋ถ ๋ต์ ์ฐพ์ ์ ์์ด. ์ง๋ฌธ๋ ํ ์ ์๊ณ !
Reddit (r/dotnet, r/xamarin)
๊ฐ๋ฐ์๋ค์ด ๋ชจ์ฌ์ ์ ๋ณด๋ฅผ ๊ณต์ ํ๊ณ ํ ๋ก ํ๋ ๊ณณ์ด์ผ.
Discord / Slack
.NET ์ปค๋ฎค๋ํฐ ์๋ฒ์์ ์ค์๊ฐ์ผ๋ก ์ง๋ฌธํ๊ณ ๋ต๋ณ๋ฐ์ ์ ์์ด.
YouTube
James Montemagno, Gerald Versluis ๊ฐ์ ๊ฐ๋ฐ์๋ค์ด MAUI ํํ ๋ฆฌ์ผ ์์์ ์ฌ๋ ค. ์์์ผ๋ก ๋ฐฐ์ฐ๋ ๊ฒ ํธํ๋ค๋ฉด ์ถ์ฒ!
๐ฏ ์ฌ๋ฅ๋ท์์ MAUI ์ ๋ฌธ๊ฐ ์ฐพ๊ธฐ
ํผ์ ๊ณต๋ถํ๋ค๊ฐ ๋งํ๊ฑฐ๋, ํ๋ก์ ํธ์ ๋์์ด ํ์ํ๋ค๋ฉด? ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ MAUI ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ด! ๐ผ
์ฌ๋ฅ๋ท์ ๋ค์ํ ๋ถ์ผ์ ์ ๋ฌธ๊ฐ๋ค์ด ๋ชจ์ฌ์๋ ํ๋ซํผ์ด์ผ. MAUI ์ฑ ๊ฐ๋ฐ, ์ฝ๋ ๋ฆฌ๋ทฐ, ๋ฉํ ๋ง, ํ๋ก์ ํธ ์ธ์ฃผ ๋ฑ ํ์ํ ๋์์ ๋ฐ์ ์ ์์ง. ์ด๋ณด์๋ฅผ ์ํ 1:1 ๊ณผ์ธ๋ถํฐ ๊ธฐ์ ์ฉ ์ฑ ๊ฐ๋ฐ๊น์ง, ๋ค์ํ ์๋น์ค๊ฐ ์์ด.
๋ฐ๋๋ก ๋๊ฐ MAUI์ ๋ฅ์ํ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ๋์ ์ง์์ ๊ณต์ ํ๊ณ ์์ต์ ์ฌ๋ฆด ์๋ ์์ด. ์์์ด์ง! ๐ค
๐ฎ MAUI์ ๋ฏธ๋์ ์ ๋ง
MAUI๋ ์์ง ์ ์ ํ๋ ์์ํฌ์ผ. 2022๋ ์ ์ ์ ์ถ์๋์์ผ๋๊น. ํ์ง๋ง ๋น ๋ฅด๊ฒ ์ฑ์ฅํ๊ณ ์๊ณ , Microsoft์ ์ ํญ์ ์ธ ์ง์์ ๋ฐ๊ณ ์์ด. ๐
๐ ๋ฐ์ ๋ฐฉํฅ
์ฑ๋ฅ ๊ฐ์
๋งค ๋ฆด๋ฆฌ์ค๋ง๋ค ์์ ์๊ฐ, ๋ ๋๋ง ์๋, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๊ฐ์ ๋๊ณ ์์ด. .NET 8์์๋ ํนํ ํฐ ์ฑ๋ฅ ํฅ์์ด ์์์ง.
๋ ๋ง์ ํ๋ซํผ ์ง์
ํ์ฌ๋ iOS, Android, macOS, Windows๋ฅผ ์ง์ํ์ง๋ง, ๋ฏธ๋์๋ Linux, Tizen ๊ฐ์ ํ๋ซํผ๋ ์ง์ํ ๊ฐ๋ฅ์ฑ์ด ์์ด.
Blazor ํตํฉ
Blazor Hybrid๋ฅผ ์ฌ์ฉํ๋ฉด ์น ๊ธฐ์ (HTML, CSS, JavaScript)๋ก MAUI ์ฑ์ ๋ง๋ค ์ ์์ด. ์น ๊ฐ๋ฐ์๋ค์๊ฒ ์ง์
์ฅ๋ฒฝ์ด ๋ฎ์์ง๋ ๊ฑฐ์ง.
AI ํตํฉ
.NET์ AI ๊ธฐ๋ฅ์ด ๊ณ์ ์ถ๊ฐ๋๊ณ ์์ด. MAUI ์ฑ์์๋ ๋จธ์ ๋ฌ๋, ์์ฐ์ด ์ฒ๋ฆฌ ๊ฐ์ AI ๊ธฐ๋ฅ์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๊ฒ ๋ ๊ฑฐ์ผ.
์ปค๋ฎค๋ํฐ ์ํ๊ณ
NuGet ํจํค์ง, ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํํ ๋ฆฌ์ผ์ด ๊ณ์ ๋์ด๋๊ณ ์์ด. ์ปค๋ฎค๋ํฐ๊ฐ ํ๋ฐํ ์๋ก ํ๋ ์์ํฌ๋ ๊ฐํด์ง์ง.
๐ก ์ค์ ํ๋ก์ ํธ ์์ด๋์ด
์ด๋ก ๋ง ๊ณต๋ถํ๋ฉด ์ง๋ฃจํด. ์ง์ ๋ง๋ค์ด๋ณด๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ์ต๊ณ ์ผ! ๋ช ๊ฐ์ง ํ๋ก์ ํธ ์์ด๋์ด๋ฅผ ์ ์ํ ๊ฒ. ๐จ
๐ ํ ์ผ ๊ด๋ฆฌ ์ฑ (To-Do App)
โข SQLite๋ก ๋ก์ปฌ ๋ฐ์ดํฐ ์ ์ฅ
โข MVVM ํจํด ์ ์ฉ
โข ์๋ฆผ ๊ธฐ๋ฅ ๊ตฌํ
โข ์นดํ
๊ณ ๋ฆฌ๋ณ ๋ถ๋ฅ
โข ๊ฒ์ ๋ฐ ํํฐ๋ง
๐ค๏ธ ๋ ์จ ์ฑ
โข REST API ํธ์ถ (OpenWeatherMap ๋ฑ)
โข ์์น ์ ๋ณด ํ์ฉ
โข ์ฐจํธ๋ก ๋ฐ์ดํฐ ์๊ฐํ
โข ๋ฐฑ๊ทธ๋ผ์ด๋ ์
๋ฐ์ดํธ
โข ์์ ฏ ์ง์
๐ฐ ๊ฐ๊ณ๋ถ ์ฑ
โข ์์
/์ง์ถ ๊ธฐ๋ก
โข ์นดํ
๊ณ ๋ฆฌ๋ณ ํต๊ณ
โข ๊ทธ๋ํ๋ก ์๊ฐํ
โข ์์ฐ ์ค์ ๋ฐ ์๋ฆผ
โข ๋ฐ์ดํฐ ๋ฐฑ์
/๋ณต์
๐ท ์ฌ์ง ๊ฐค๋ฌ๋ฆฌ ์ฑ
โข ์นด๋ฉ๋ผ ๋ฐ ๊ฐค๋ฌ๋ฆฌ ์ ๊ทผ
โข ์ด๋ฏธ์ง ํํฐ ์ ์ฉ
โข ํด๋ผ์ฐ๋ ๋๊ธฐํ
โข ์จ๋ฒ ๊ด๋ฆฌ
โข ๊ณต์ ๊ธฐ๋ฅ
๐ต ์์ ํ๋ ์ด์ด
โข ๋ก์ปฌ ์์
ํ์ผ ์ฌ์
โข ํ๋ ์ด๋ฆฌ์คํธ ๊ด๋ฆฌ
โข ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฌ์
โข ๋ฏธ๋์ด ์ปจํธ๋กค
โข ๊ฐ์ฌ ํ์
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ ๋๋จํด! ๐ Xamarin๊ณผ MAUI์ ๋ํด ๊ฝค ๋ง์ ๊ฑธ ๋ฐฐ์ ์ ๊ฑฐ์ผ. ํฌ๋ก์คํ๋ซํผ ๊ฐ๋ฐ์ ์ ๋ง ๋งค๋ ฅ์ ์ธ ๋ถ์ผ์ผ. ํ ๋ฒ์ ๊ฐ๋ฐ๋ก ์ฌ๋ฌ ํ๋ซํผ์์ ๋์ํ๋ ์ฑ์ ๋ง๋ค ์ ์๋ค๋, ํจ์จ์ ์ด์ง ์์?
๋ฌผ๋ก ์ฒ์์๋ ์ด๋ ค์ธ ์ ์์ด. XAML๋ ์ต์ํด์ ธ์ผ ํ๊ณ , MVVM ํจํด๋ ์ดํดํด์ผ ํ๊ณ , ๊ฐ ํ๋ซํผ์ ํน์ฑ๋ ์์์ผ ํ๋๊น. ํ์ง๋ง ๊ฑฑ์ ๋ง! ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฉ์ง ์ฑ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ. ๐
MAUI๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. Microsoft์ ์ง์๋ ํํํ๊ณ , ์ปค๋ฎค๋ํฐ๋ ํ๋ฐํด. .NET ์ํ๊ณ์ ๋ชจ๋ ์ฅ์ ์ ํ์ฉํ ์ ์๋ค๋ ๊ฒ๋ ํฐ ๋ฉ๋ฆฌํธ์ง. C# ๊ฐ๋ฐ์๋ผ๋ฉด MAUI๋ฅผ ๋ฐฐ์๋๋ ๊ฒ ์ ๋ง ์ข์ ํฌ์๊ฐ ๋ ๊ฑฐ์ผ. ๐
์ด์ ๋์ ์ฐจ๋ก์ผ! ์ด ๊ธ์์ ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ฒซ MAUI ์ฑ์ ๋ง๋ค์ด๋ด. ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๋ณต์กํ ์ฑ์ผ๋ก ๋ฐ์ ์์ผ ๋๊ฐ. ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ๊ณต์ ๋ฌธ์๋ฅผ ์ฐพ์๋ณด๊ณ , ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ณ , ํ์ํ๋ฉด ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ ์ข์.
๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์ ์ธ๊ณ๋ ๋ฌด๊ถ๋ฌด์งํด. ๋์ ์์ด๋์ด๋ฅผ ํ์ค๋ก ๋ง๋ค ์ ์๋ ๋๊ตฌ๋ฅผ ์ด์ ์์ ์ฅ์์ด. ์, ์ด์ ์์ํด๋ณผ๊น? ํ์ดํ ! ๐โจ
Happy Coding! ๐
๋น์ ์ ์ฒซ MAUI ์ฑ์ด ์ฑ๊ณตํ๊ธฐ๋ฅผ ์์ํด์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.