Ver3.0 ๐ .NET MAUI๋ก ํ ๋ฒ์ ๋๋ด๋ ๋ฉํฐํ๋ซํผ ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋

๐ .NET MAUI๋ก ํ ๋ฒ์ ๋๋ด๋ ๋ฉํฐํ๋ซํผ ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋
iOS, Android, Windows, macOS๋ฅผ ํ๋์ ์ฝ๋๋ก ์ ๋ณตํ๋ ๋ง๋ฒ ๊ฐ์ ์ฌ์ โจ
๐ก ์ฌ๋ฌ๋ถ, ์ด๋ฐ ๊ณ ๋ฏผ ํด๋ณด์ ์ ์๋์?
"์ฑ์ ๋ง๋ค๊ณ ์ถ์๋ฐ... iOS๋ Swift๋ก, Android๋ Kotlin์ผ๋ก, Windows๋ ๋ ๋ฐ๋ก? ์ด๊ฑฐ ์ธ์ ๋ค ๋ฐฐ์ฐ๊ณ ๊ฐ๋ฐํ์ง?" ๐ฑ
๋ฐ๋ก ์ด๋ฐ ๊ณ ๋ฏผ์ ํด๊ฒฐํด์ฃผ๋ ๊ฒ .NET MAUI์ ๋๋ค! ํ ๋ฒ์ ์ฝ๋ ์์ฑ์ผ๋ก ์ฌ๋ฌ ํ๋ซํผ์์ ๋์ํ๋ ์ฑ์ ๋ง๋ค ์ ์๋ค๋, ์ ๋ง ๊ฟ๋ง ๊ฐ์ง ์๋์? ์ค๋์ ์ด ๋๋ผ์ด ๊ธฐ์ ์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ค๋ช ํด๋๋ฆด๊ฒ์! ๐
๐ฏ .NET MAUI๊ฐ ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์, ๋จผ์ .NET MAUI๊ฐ ์ ํํ ๋ญ์ง๋ถํฐ ์์๋ณผ๊น์? MAUI๋ Multi-platform App UI์ ์ฝ์์์. ๋ง์ดํฌ๋ก์ํํธ๊ฐ 2022๋ 5์์ ์ ์์ผ๋ก ์ถ์ํ ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ์ฃ . ๐
์ฝ๊ฒ ๋งํ๋ฉด, C#์ด๋ผ๋ ํ๋์ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ก ์ฝ๋๋ฅผ ์์ฑํ๋ฉด, ๊ทธ ์ฝ๋๊ฐ iOS, Android, Windows, macOS์์ ๋ชจ๋ ๋์๊ฐ๋ ์ฑ์ผ๋ก ๋ณ์ ํ๋ค๋ ๊ฑฐ์์! ๋ง์น ๋ง๋ฒ์ฌ๊ฐ ์งํก์ด ํ ๋ฒ ํ๋๋ฅด๋ฉด ์ฌ๋ฌ ๊ฐ์ ํ ๋ผ๊ฐ ๋์ค๋ ๊ฒ์ฒ๋ผ์. ๐ฐ๐ฐ๐ฐ
๐ .NET MAUI์ ์ ์ , Xamarin์ ์์๋์?
์ฌ์ค .NET MAUI๋ ์์ ํ ์๋ก์ด ๊ธฐ์ ์ ์๋์์. Xamarin.Forms๋ผ๋ ์ ๋ฐฐ ๊ธฐ์ ์ ์งํ ๋ฒ์ ์ด๋ผ๊ณ ํ ์ ์์ฃ . Xamarin์ด ์ข๊ธด ํ์ง๋ง ๋ช ๊ฐ์ง ํ๊ณ๊ฐ ์์์ด์. ์ฑ๋ฅ ์ด์๋ ์์๊ณ , ๊ฐ๋ฐ ๊ฒฝํ๋ ์กฐ๊ธ ๋ณต์กํ๊ฑฐ๋ ์. ๊ทธ๋์ ๋ง์ดํฌ๋ก์ํํธ๊ฐ "์ด๊ฑฐ ์์ ํ ์๋ก ๋ง๋ค์!"๋ผ๊ณ ๊ฒฐ์ฌํ๊ณ ํ์ํ ๊ฒ ๋ฐ๋ก MAUI์ ๋๋ค. ๋ ๋น ๋ฅด๊ณ , ๋ ๊ฐ๋จํ๊ณ , ๋ ํ๋์ ์ผ๋ก ๋ง์ด์ฃ ! ๐ช
๐ ์ ๊ฐ๋ฐ์๋ค์ด .NET MAUI์ ์ด๊ดํ ๊น?
์์งํ ๋งํด์, ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ๋ .NET MAUI๋ง ์๋ ๊ฒ ์๋์์. React Native๋ ์๊ณ , Flutter๋ ์์ฃ . ๊ทธ๋ฐ๋ฐ ์ .NET MAUI๋ฅผ ์ ํํด์ผ ํ ๊น์? ๐ค
1๏ธโฃ ์ง์ง ๋ค์ดํฐ๋ธ ์ฑ๋ฅ
.NET MAUI๋ ๊ฐ ํ๋ซํผ์ ๋ค์ดํฐ๋ธ ์ปจํธ๋กค์ ์ง์ ์ฌ์ฉํด์. ์ฆ, iOS์์๋ ์ง์ง iOS ๋ฒํผ์ด ๋์ค๊ณ , Android์์๋ ์ง์ง Android ๋ฒํผ์ด ๋์จ๋ค๋ ๊ฑฐ์ฃ . ์น๋ทฐ๋ ์บ๋ฒ์ค์ ๊ทธ๋ฆผ ๊ทธ๋ฆฌ๋ ๋ฐฉ์์ด ์๋๋ผ, ์ง์ง๋ฐฐ๊ธฐ ๋ค์ดํฐ๋ธ ์ฑ์ด์์! ๊ทธ๋์ ์ฑ๋ฅ๋ ์ข๊ณ , ์ฌ์ฉ์ ๊ฒฝํ๋ ๊ฐ ํ๋ซํผ์ ์ต์ ํ๋์ด ์๋ต๋๋ค. ๐
2๏ธโฃ C#์ ๊ฐ๋ ฅํจ
C#์ ์ ๋ง ๋ฉ์ง ์ธ์ด์์. ํ์ ์์ ์ฑ๋ ๋ฐ์ด๋๊ณ , LINQ ๊ฐ์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ ์๊ณ , async/await๋ก ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ๋ ์ฝ๊ฒ ํ ์ ์์ฃ . JavaScript๋ Dart๋ฅผ ์๋ก ๋ฐฐ์ธ ํ์ ์์ด, ์ด๋ฏธ C#์ ์๊ณ ์๋ค๋ฉด ๋ฐ๋ก ์์ํ ์ ์์ด์! ๐
3๏ธโฃ .NET ์ํ๊ณ์ ํํ
.NET MAUI๋ ๊ฑฐ๋ํ .NET ์ํ๊ณ์ ์ผ๋ถ์์. NuGet์ ์๋ ์๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๊ณ , Entity Framework๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์์ฃ . ๋ฐฑ์๋๊ฐ ASP.NET Core๋ก ๋์ด ์๋ค๋ฉด? ์๋ฒฝํ ๊ถํฉ์ด์์! ๐ฏ
4๏ธโฃ Hot Reload๋ก ๋น ๋ฅธ ๊ฐ๋ฐ
์ฝ๋๋ฅผ ์์ ํ๋ฉด ์ฑ์ ๋ค์ ๋น๋ํ์ง ์์๋ ๋ฐ๋ก๋ฐ๋ก ํ๋ฉด์ ๋ฐ์๋ผ์. ๋ฒํผ ์๊น ๋ฐ๊พธ๊ณ ์ถ์ด์? ์ฝ๋ ์์ ํ๋ฉด 1์ด ๋ง์ ํ๋ฉด์ ๋ํ๋์. ์ด๊ฒ ์ผ๋ง๋ ๊ฐ๋ฐ ์๊ฐ์ ๋จ์ถ์์ผ์ฃผ๋์ง ๋ชฐ๋ผ์! โก
๐ ๏ธ .NET MAUI ๊ฐ๋ฐ ํ๊ฒฝ ์ ์ ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๊ฐ๋ฐ์ ์์ํด๋ณผ๊น์? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ค๋นํด์ผ๊ฒ ์ฃ ? ๊ฑฑ์ ๋ง์ธ์, ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์์! ๐
Step 1: Visual Studio ์ค์น
.NET MAUI ๊ฐ๋ฐ์ ๊ฐ์ฅ ์ข์ ์น๊ตฌ๋ ๋ฐ๋ก Visual Studio 2022์์. (Visual Studio Code๊ฐ ์๋๋ผ Visual Studio์ ๋๋ค!) Windows์์๋ Community ์๋์ ์ ๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์๊ณ , Mac์์๋ Visual Studio for Mac์ ์ฌ์ฉํ ์ ์์ด์.
Step 2: ์ํฌ๋ก๋ ์ ํ
Visual Studio๋ฅผ ์ค์นํ ๋ ".NET Multi-platform App UI development" ์ํฌ๋ก๋๋ฅผ ๊ผญ ์ ํํด์ผ ํด์. ์ด๊ฒ ์ ํ๋์ด์ผ MAUI ๊ฐ๋ฐ์ ํ์ํ ๋ชจ๋ ๋๊ตฌ๋ค์ด ํจ๊ป ์ค์น๋ฉ๋๋ค. Android SDK, iOS ์๋ฎฌ๋ ์ดํฐ ์ฐ๊ฒฐ ๋๊ตฌ ๋ฑ๋ฑ์ด ๋ค ํฌํจ๋์ด ์์ด์! ๐ฆ
Step 3: ์ถ๊ฐ ์ค์
iOS ์ฑ์ ๊ฐ๋ฐํ๋ ค๋ฉด Mac์ด ํ์ํด์. (์ด๊ฑด ์ ํ์ ์ ์ฑ ์ด๋ผ ์ด์ฉ ์ ์์ด์ ๐ ) Windows์์ ๊ฐ๋ฐํ๋๋ผ๋ ๋คํธ์ํฌ๋ก Mac์ ์ฐ๊ฒฐํด์ ๋น๋ํ ์ ์๋ต๋๋ค. Android๋ Windows์์๋ Mac์์๋ ์์ ๋กญ๊ฒ ๊ฐ๋ฐ ๊ฐ๋ฅํ๊ณ ์!
๐ก ๊ฟํ!
์ฒ์ ์์ํ๋ค๋ฉด Android ๊ฐ๋ฐ๋ถํฐ ํด๋ณด๋ ๊ฑธ ์ถ์ฒํด์. ์๋ฎฌ๋ ์ดํฐ๋ ๋น ๋ฅด๊ณ , ์ค์ ๋ ์๋์ ์ผ๋ก ๊ฐ๋จํ๊ฑฐ๋ ์. iOS๋ ์กฐ๊ธ ์ต์ํด์ง ๋ค์์ ๋์ ํด๋ ๋ฆ์ง ์์์!
๐จ ์ฒซ ๋ฒ์งธ MAUI ํ๋ก์ ํธ ๋ง๋ค๊ธฐ
์, ์ด์ ์ง์ง ์ฝ๋ฉ์ ์์ํด๋ณผ๊น์? Visual Studio๋ฅผ ์ด๊ณ ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ด ์๋ค! ๐
// ํ๋ก์ ํธ ์์ฑ ํ ๊ธฐ๋ณธ ๊ตฌ์กฐ
MyMauiApp/
โโโ Platforms/
โ โโโ Android/
โ โโโ iOS/
โ โโโ MacCatalyst/
โ โโโ Windows/
โโโ Resources/
โ โโโ Images/
โ โโโ Fonts/
โ โโโ Styles/
โโโ App.xaml
โโโ AppShell.xaml
โโโ MainPage.xaml
ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ฉด ์ด๋ฐ ๊ตฌ์กฐ๊ฐ ๋ํ๋์. ์ ๊ธฐํ์ฃ ? Platforms ํด๋์๋ ๊ฐ ํ๋ซํผ๋ณ ํนํ ์ฝ๋๊ฐ ๋ค์ด๊ฐ๊ณ , ๋๋จธ์ง๋ ๋ชจ๋ ํ๋ซํผ์์ ๊ณต์ ํ๋ ์ฝ๋์์. ๋ณดํต์ ๊ณต์ ์ฝ๋๋ง์ผ๋ก๋ 90% ์ด์์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ต๋๋ค! ๐ฏ
MainPage.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="MyMauiApp.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Label
Text="์๋
ํ์ธ์! MAUI ์ธ๊ณ์ ์ค์ ๊ฑธ ํ์ํด์! ๐"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
<Button
x:Name="CounterBtn"
Text="ํด๋ฆญํด๋ณด์ธ์!"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
XAML์ด๋ผ๋ ๋งํฌ์ ์ธ์ด๋ก UI๋ฅผ ์ ์ํด์. HTML๊ณผ ๋น์ทํ๊ฒ ์๊ฒผ์ฃ ? ๊ทผ๋ฐ ํจ์ฌ ๋ ๊ฐ๋ ฅํด์! ์ด ์ฝ๋๋ ๋ผ๋ฒจ ํ๋์ ๋ฒํผ ํ๋๋ฅผ ์ธ๋ก๋ก ๋ฐฐ์นํ๋ ๊ฐ๋จํ ํ๋ฉด์ด์์. ๐
์ด์ ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ์ฝ๋๋ฅผ ๋ณผ๊น์? MainPage.xaml.cs ํ์ผ์ ์ด์ด๋ณด์ธ์.
namespace MyMauiApp;
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"1๋ฒ ํด๋ฆญํ์ด์!";
else
CounterBtn.Text = $"{count}๋ฒ ํด๋ฆญํ์ด์!";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}
๋ณด์ธ์! ์์ ์ผ๋ฐ์ ์ธ C# ์ฝ๋์ฃ ? ๋ฒํผ์ ํด๋ฆญํ ๋๋ง๋ค ์นด์ดํธ๊ฐ ์ฌ๋ผ๊ฐ๊ณ , ๋ฒํผ์ ํ ์คํธ๊ฐ ๋ฐ๋์ด์. ์ด ์ฝ๋ ํ๋๋ก iOS, Android, Windows, macOS ๋ชจ๋์์ ๋๊ฐ์ด ๋์ํ๋ต๋๋ค! ๐
๐จ XAML๊ณผ MVVM ํจํด ๋ง์คํฐํ๊ธฐ
.NET MAUI์์ UI๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง์์. XAML์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๊ณผ C# ์ฝ๋๋ก ์ง์ ๋ง๋๋ ๋ฐฉ๋ฒ์ด์ฃ . ๊ทผ๋ฐ ๋๋ถ๋ถ์ ๊ฐ๋ฐ์๋ค์ XAML์ ์ ํธํด์. ์๋๊ณ ์? ํจ์ฌ ์ง๊ด์ ์ด๊ณ ๋์์ด๋์ ํ์ ํ๊ธฐ๋ ์ข๊ฑฐ๋ ์! ๐จ
๐ XAML ๋ ์ด์์์ ๊ธฐ๋ณธ
MAUI๋ ๋ค์ํ ๋ ์ด์์ ์ปจํ ์ด๋๋ฅผ ์ ๊ณตํด์. ๊ฐ๊ฐ์ ํน์ง์ ์์๋ณผ๊น์?
StackLayout (VerticalStackLayout / HorizontalStackLayout)
์์๋ค์ ์ธ๋ก๋ ๊ฐ๋ก๋ก ์์์ฌ๋ ค์. ๊ฐ์ฅ ๊ฐ๋จํ๊ณ ์์ฃผ ์ฌ์ฉํ๋ ๋ ์ด์์์ด์์. ๋ง์น ๋ธ๋ก์ ์๋ฏ์ด UI๋ฅผ ๋ง๋ค ์ ์์ฃ ! ๐งฑ
<VerticalStackLayout Spacing="10">
<Label Text="์ฒซ ๋ฒ์งธ ํญ๋ชฉ" />
<Label Text="๋ ๋ฒ์งธ ํญ๋ชฉ" />
<Label Text="์ธ ๋ฒ์งธ ํญ๋ชฉ" />
</VerticalStackLayout>
Grid
ํ๊ณผ ์ด๋ก ๊ตฌ์ฑ๋ ํ ํํ์ ๋ ์ด์์์ด์์. ๋ณต์กํ UI๋ฅผ ๋ง๋ค ๋ ์ ๋ง ์ ์ฉํด์! ์น์ CSS Grid์ ๋น์ทํ๋ค๊ณ ์๊ฐํ๋ฉด ๋ผ์. ๐
<Grid RowDefinitions="Auto,*,Auto"
ColumnDefinitions="*,*">
<Label Text="ํค๋" Grid.Row="0" Grid.ColumnSpan="2" />
<Label Text="์ผ์ชฝ ์ปจํ
์ธ " Grid.Row="1" Grid.Column="0" />
<Label Text="์ค๋ฅธ์ชฝ ์ปจํ
์ธ " Grid.Row="1" Grid.Column="1" />
<Button Text="ํธํฐ ๋ฒํผ" Grid.Row="2" Grid.ColumnSpan="2" />
</Grid>
FlexLayout
CSS Flexbox์ ๊ฑฐ์ ๋๊ฐ์์! ๋ฐ์ํ UI๋ฅผ ๋ง๋ค ๋ ์ ๋ง ์ข์ฃ . ํ๋ฉด ํฌ๊ธฐ์ ๋ฐ๋ผ ์๋์ผ๋ก ๋ฐฐ์น๊ฐ ๋ฐ๋์ด์. ๐ฑ๐ป
AbsoluteLayout
์ ๋ ์ขํ๋ก ์์๋ฅผ ๋ฐฐ์นํด์. ๊ฒ์์ด๋ ํน์ํ UI๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํ์ง๋ง, ์ผ๋ฐ์ ์ผ๋ก๋ ์ ์ ์จ์. ๋ฐ์ํ์ด ์ ๋๊ฑฐ๋ ์! ๐
๐๏ธ MVVM ํจํด, ์ด๋ ต์ง ์์์!
MVVM์ Model-View-ViewModel์ ์ฝ์์์. ์ฒ์ ๋ค์ผ๋ฉด ๋ณต์กํด ๋ณด์ด์ง๋ง, ์ฌ์ค์ ์ฝ๋๋ฅผ ๊น๋ํ๊ฒ ์ ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ผ ๋ฟ์ด์์. ๐งน
๐ญ Model (๋ชจ๋ธ)
๋ฐ์ดํฐ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํด์. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ User ํด๋์ค ๊ฐ์ ๊ฑฐ์ฃ .
๐๏ธ View (๋ทฐ)
์ฌ์ฉ์๊ฐ ๋ณด๋ ํ๋ฉด์ด์์. XAML ํ์ผ์ด ๋ฐ๋ก View์์!
๐ ViewModel (๋ทฐ๋ชจ๋ธ)
View์ Model ์ฌ์ด์ ์ค๊ฐ ๋ค๋ฆฌ์์. View์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ค๋นํ๊ณ , ์ฌ์ฉ์์ ์ก์ ์ ์ฒ๋ฆฌํด์.
์ค์ ์ฝ๋๋ก ๋ณด๋ฉด ์ดํด๊ฐ ์ฌ์์. ๊ฐ๋จํ ํ ์ผ ๋ชฉ๋ก ์ฑ์ ๋ง๋ค์ด๋ณผ๊น์? ๐
// Model/TodoItem.cs
public class TodoItem
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
}
// ViewModel/TodoViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
public class TodoViewModel : INotifyPropertyChanged
{
public ObservableCollection<TodoItem> TodoItems { get; set; }
private string newTodoText;
public string NewTodoText
{
get => newTodoText;
set
{
newTodoText = value;
OnPropertyChanged(nameof(NewTodoText));
}
}
public ICommand AddTodoCommand { get; }
public TodoViewModel()
{
TodoItems = new ObservableCollection<TodoItem>();
AddTodoCommand = new Command(AddTodo);
}
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(NewTodoText))
{
TodoItems.Add(new TodoItem
{
Title = NewTodoText,
IsCompleted = false,
CreatedAt = DateTime.Now
});
NewTodoText = string.Empty;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
}
ViewModel์์ INotifyPropertyChanged๋ฅผ ๊ตฌํํ๋ ๊ฒ ๋ณด์ด์๋์? ์ด๊ฒ MVVM์ ํต์ฌ์ด์์! ๋ฐ์ดํฐ๊ฐ ๋ฐ๋๋ฉด ์๋์ผ๋ก UI๊ฐ ์ ๋ฐ์ดํธ๋๋ ๋ง๋ฒ ๊ฐ์ ์ผ์ด ์ผ์ด๋์ฃ . โจ
์ด์ View์์ ์ด ViewModel์ ์ฌ์ฉํด๋ณผ๊น์?
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
x:Class="TodoApp.Views.TodoPage">
<ContentPage.BindingContext>
<viewmodels:TodoViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="20">
<HorizontalStackLayout Spacing="10">
<Entry Text="{Binding NewTodoText}"
Placeholder="์๋ก์ด ํ ์ผ์ ์
๋ ฅํ์ธ์"
HorizontalOptions="FillAndExpand" />
<Button Text="์ถ๊ฐ"
Command="{Binding AddTodoCommand}" />
</HorizontalStackLayout>
<CollectionView ItemsSource="{Binding TodoItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<HorizontalStackLayout Padding="10">
<CheckBox IsChecked="{Binding IsCompleted}" />
<Label Text="{Binding Title}"
VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
๋ณด์ธ์! {Binding ...} ์ด๋ฐ ๋ฌธ๋ฒ์ด ๋ณด์ด์ฃ ? ์ด๊ฒ ๋ฐ๋ก ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ด์์. Entry์ ํ
์คํธ๋ฅผ ์
๋ ฅํ๋ฉด ์๋์ผ๋ก ViewModel์ NewTodoText ์์ฑ์ด ์
๋ฐ์ดํธ๋๊ณ , ๋ฒํผ์ ํด๋ฆญํ๋ฉด AddTodoCommand๊ฐ ์คํ๋ผ์. ์ฝ๋ ๋นํ์ธ๋์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์์ฑํ ํ์๊ฐ ์์ด์! ๐
๐ก MVVM ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ์ฒ!
INotifyPropertyChanged๋ฅผ ๋งค๋ฒ ๊ตฌํํ๋ ๊ฒ ๊ท์ฐฎ๋ค๊ณ ์? CommunityToolkit.Mvvm NuGet ํจํค์ง๋ฅผ ์ฌ์ฉํด๋ณด์ธ์! [ObservableProperty] ๊ฐ์ ์์ฑ๋ง ๋ถ์ด๋ฉด ์๋์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํด์ค์. ์ ๋ง ํธ๋ฆฌํ๋ต๋๋ค! ๐
๐ ๋ค๋น๊ฒ์ด์ ๊ณผ Shell ํ์ฉํ๊ธฐ
์ฑ์์ ํ๋ฉด ์ ํ์ ์ ๋ง ์ค์ํ์ฃ ? .NET MAUI๋ Shell์ด๋ผ๋ ๊ฐ๋ ฅํ ๋ค๋น๊ฒ์ด์ ์์คํ ์ ์ ๊ณตํด์. ์ด๊ฒ ์ ๋ง ํธ๋ฆฌํด์! ๐บ๏ธ
๐ Shell์ด ๋ญ์์?
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">
<!-- ํญ ๋ฐ ๊ตฌ์กฐ -->
<TabBar>
<ShellContent Title="ํ"
Icon="home.png"
ContentTemplate="{DataTemplate local:HomePage}" />
<ShellContent Title="๊ฒ์"
Icon="search.png"
ContentTemplate="{DataTemplate local:SearchPage}" />
<ShellContent Title="ํ๋กํ"
Icon="profile.png"
ContentTemplate="{DataTemplate local:ProfilePage}" />
</TabBar>
</Shell>
์ด๋ ๊ฒ ํ๋ฉด ํ๋จ์ 3๊ฐ์ ํญ์ด ์๋ ์ฑ์ด ๋ง๋ค์ด์ ธ์! iOS์์๋ iOS ์คํ์ผ๋ก, Android์์๋ Android ์คํ์ผ๋ก ์๋์ผ๋ก ๋ ๋๋ง๋์ฃ . ์ ๊ธฐํ์ง ์๋์? ๐ฎ
๐ ํ์ด์ง ์ด๋ํ๊ธฐ
Shell์ ์ฌ์ฉํ๋ฉด ํ์ด์ง ์ด๋์ด ์ ๋ง ๊ฐ๋จํด์. URI ๊ธฐ๋ฐ ๋ค๋น๊ฒ์ด์ ์ ์ฌ์ฉํ๊ฑฐ๋ ์!
// ๋จผ์ ๋ผ์ฐํธ๋ฅผ ๋ฑ๋กํด์
Routing.RegisterRoute("detailpage", typeof(DetailPage));
// ๊ทธ๋ฆฌ๊ณ ์ด๋!
await Shell.Current.GoToAsync("detailpage");
// ํ๋ผ๋ฏธํฐ๋ ์ ๋ฌํ ์ ์์ด์
await Shell.Current.GoToAsync($"detailpage?id={itemId}");
// ๋ค๋ก ๊ฐ๊ธฐ
await Shell.Current.GoToAsync("..");
ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ ํ์ด์ง๋ ์ด๋ ๊ฒ ๋ง๋ค์ด์:
[QueryProperty(nameof(ItemId), "id")]
public partial class DetailPage : ContentPage
{
private string itemId;
public string ItemId
{
get => itemId;
set
{
itemId = value;
LoadItemDetails(value);
}
}
private async void LoadItemDetails(string id)
{
// ID๋ก ์์ธ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์์
var item = await DataService.GetItemAsync(id);
BindingContext = item;
}
}
์ ๋ง ๊น๋ํ์ฃ ? ์น ๊ฐ๋ฐํ๋ ๊ฒ์ฒ๋ผ URL๋ก ๋ค๋น๊ฒ์ด์ ์ ๊ด๋ฆฌํ ์ ์์ด์! ๐
๐ฑ ํ๋ซํผ๋ณ ํนํ ๊ธฐ๋ฅ ๋ค๋ฃจ๊ธฐ
์๋ฌด๋ฆฌ ํฌ๋ก์ค ํ๋ซํผ์ด๋ผ๊ณ ํด๋, ๋๋ก๋ ๊ฐ ํ๋ซํผ์ ํน๋ณํ ๊ธฐ๋ฅ์ ์ฌ์ฉํด์ผ ํ ๋๊ฐ ์์ด์. ์๋ฅผ ๋ค์ด, iOS์ Face ID๋ Android์ ๋ฐฑ ๋ฒํผ ๊ฐ์ ๊ฑฐ์ฃ . .NET MAUI๋ ์ด๋ฐ ๊ฒ๋ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด์! ๐ญ
๐ง ์กฐ๊ฑด๋ถ ์ปดํ์ผ
๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ ์กฐ๊ฑด๋ถ ์ปดํ์ผ์ ์ฌ์ฉํ๋ ๊ฑฐ์์:
public void DoSomethingPlatformSpecific()
{
#if ANDROID
// Android ์ ์ฉ ์ฝ๋
Android.Widget.Toast.MakeText(
Platform.CurrentActivity,
"์๋๋ก์ด๋์์ ์คํ ์ค!",
Android.Widget.ToastLength.Short
).Show();
#elif IOS
// iOS ์ ์ฉ ์ฝ๋
var alert = UIAlertController.Create(
"์๋ฆผ",
"iOS์์ ์คํ ์ค!",
UIAlertControllerStyle.Alert
);
// ... alert ํ์ ์ฝ๋
#elif WINDOWS
// Windows ์ ์ฉ ์ฝ๋
var dialog = new Windows.UI.Popups.MessageDialog(
"์๋์ฐ์์ ์คํ ์ค!"
);
await dialog.ShowAsync();
#endif
}
๐ฏ Dependency Injection์ผ๋ก ๋ ์ฐ์ํ๊ฒ
๊ทผ๋ฐ ์กฐ๊ฑด๋ถ ์ปดํ์ผ์ ์ฝ๋๊ฐ ์ง์ ๋ถํด์ง ์ ์์ด์. ๋ ์ข์ ๋ฐฉ๋ฒ์ ์ธํฐํ์ด์ค์ ์์กด์ฑ ์ฃผ์ ์ ์ฌ์ฉํ๋ ๊ฑฐ์์!
// ๊ณตํต ์ธํฐํ์ด์ค
public interface INotificationService
{
void ShowNotification(string title, string message);
}
// Android ๊ตฌํ (Platforms/Android ํด๋์)
public class NotificationService : INotificationService
{
public void ShowNotification(string title, string message)
{
// Android ์๋ฆผ ๊ตฌํ
var builder = new NotificationCompat.Builder(
Platform.CurrentActivity,
"default"
)
.SetContentTitle(title)
.SetContentText(message)
.SetSmallIcon(Resource.Drawable.notification_icon);
// ์๋ฆผ ํ์
}
}
// iOS ๊ตฌํ (Platforms/iOS ํด๋์)
public class NotificationService : INotificationService
{
public void ShowNotification(string title, string message)
{
// iOS ์๋ฆผ ๊ตฌํ
var content = new UNMutableNotificationContent
{
Title = title,
Body = message
};
// ์๋ฆผ ํ์
}
}
// MauiProgram.cs์์ ๋ฑ๋ก
builder.Services.AddSingleton<INotificationService, NotificationService>();
// ์ฌ์ฉํ ๋
public class MyViewModel
{
private readonly INotificationService notificationService;
public MyViewModel(INotificationService notificationService)
{
this.notificationService = notificationService;
}
public void SendNotification()
{
notificationService.ShowNotification(
"์ ๋ฉ์์ง",
"์๋
ํ์ธ์!"
);
}
}
์ด๋ ๊ฒ ํ๋ฉด ๊ณตํต ์ฝ๋๋ ๊น๋ํ๊ฒ ์ ์งํ๋ฉด์๋ ๊ฐ ํ๋ซํผ์ ํน์ฑ์ ์ด๋ฆด ์ ์์ด์! ๐จ
๐ก ํ๋ซํผ ์ฒดํฌํ๊ธฐ
๋ฐํ์์ ํ์ฌ ํ๋ซํผ์ ํ์ธํ๊ณ ์ถ๋ค๋ฉด DeviceInfo.Platform์ ์ฌ์ฉํ์ธ์!
if (DeviceInfo.Platform == DevicePlatform.Android)
{
// Android ์ ์ฉ ๋ก์ง
}
else if (DeviceInfo.Platform == DevicePlatform.iOS)
{
// iOS ์ ์ฉ ๋ก์ง
}
๐จ ์คํ์ผ๋ง๊ณผ ํ ๋ง ์ ์ฉํ๊ธฐ
์์ ์ฑ์ ๋ง๋ค๊ณ ์ถ์ฃ ? .NET MAUI๋ ๊ฐ๋ ฅํ ์คํ์ผ๋ง ์์คํ ์ ์ ๊ณตํด์. CSS์ ๋น์ทํ์ง๋ง ๋ ํ์ ์์ ํ๊ณ ๊ฐ๋ ฅํ๋ต๋๋ค! ๐
๐ญ ๋ฆฌ์์ค ๋์ ๋๋ฆฌ
App.xaml ํ์ผ์ ์ ์ญ ์คํ์ผ์ ์ ์ํ ์ ์์ด์:
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
x:Class="MyApp.App">
<Application.Resources>
<ResourceDictionary>
<!-- ์์ ์ ์ -->
<Color x:Key="PrimaryColor">#6200EE</Color>
<Color x:Key="SecondaryColor">#03DAC6</Color>
<Color x:Key="BackgroundColor">#FFFFFF</Color>
<Color x:Key="TextColor">#000000</Color>
<!-- ์คํ์ผ ์ ์ -->
<Style TargetType="Button">
<Setter Property="BackgroundColor"
Value="{StaticResource PrimaryColor}" />
<Setter Property="TextColor"
Value="White" />
<Setter Property="CornerRadius"
Value="8" />
<Setter Property="Padding"
Value="16,8" />
</Style>
<Style x:Key="TitleLabel" TargetType="Label">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="TextColor"
Value="{StaticResource PrimaryColor}" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
์ด์ ์ฑ ์ ์ฒด์์ ์ด ์คํ์ผ๋ค์ ์ฌ์ฉํ ์ ์์ด์:
<Label Text="์ ๋ชฉ์
๋๋ค" Style="{StaticResource TitleLabel}" />
<Button Text="ํด๋ฆญํ์ธ์" /> <!-- ์๋์ผ๋ก ์คํ์ผ ์ ์ฉ -->
๐ ๋คํฌ ๋ชจ๋ ์ง์ํ๊ธฐ
์์ฆ ์ฑ์ ๋คํฌ ๋ชจ๋๋ฅผ ์ง์ํ๋ ๊ฒ ํ์์ฃ ! MAUI์์๋ ์ ๋ง ์ฌ์์:
<Application.Resources>
<ResourceDictionary>
<!-- ๋ผ์ดํธ ํ
๋ง -->
<Color x:Key="PageBackgroundColor">White</Color>
<Color x:Key="TextColor">Black</Color>
<!-- ๋คํฌ ํ
๋ง (AppThemeBinding ์ฌ์ฉ) -->
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light=White, Dark=#1E1E1E}" />
</Style>
<Style TargetType="Label">
<Setter Property="TextColor"
Value="{AppThemeBinding Light=Black, Dark=White}" />
</Style>
</ResourceDictionary>
</Application.Resources>
์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์์ ์์คํ ์ค์ ์ ๋ฐ๋ผ ์๋์ผ๋ก ํ ๋ง๊ฐ ๋ฐ๋์ด์! ์ฌ์ฉ์๊ฐ ๋คํฌ ๋ชจ๋๋ก ์ ํํ๋ฉด ์ฑ๋ ์ฆ์ ๋คํฌ ๋ชจ๋๋ก ๋ฐ๋์ฃ . ์ ๋ง ๋ฉ์ง์ง ์๋์? ๐
์ฝ๋์์ ํ์ฌ ํ ๋ง๋ฅผ ํ์ธํ๊ฑฐ๋ ๋ณ๊ฒฝํ ์๋ ์์ด์:
// ํ์ฌ ํ
๋ง ํ์ธ
var currentTheme = Application.Current.RequestedTheme;
// ํ
๋ง ๋ณ๊ฒฝ
Application.Current.UserAppTheme = AppTheme.Dark;
๐พ ๋ฐ์ดํฐ ์ ์ฅ๊ณผ ๊ด๋ฆฌ
์ฑ์ ๋ง๋ค๋ค ๋ณด๋ฉด ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด์ผ ํ ์ผ์ด ์ ๋ง ๋ง์์. ์ฌ์ฉ์ ์ค์ , ๋ก๊ทธ์ธ ์ ๋ณด, ์บ์ ๋ฐ์ดํฐ ๋ฑ๋ฑ... .NET MAUI๋ ๋ค์ํ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ๋ฒ์ ์ ๊ณตํด์! ๐ฆ
๐ Preferences - ๊ฐ๋จํ ํค-๊ฐ ์ ์ฅ
๊ฐ๋จํ ์ค์ ๊ฐ์ ์ ์ฅํ ๋๋ Preferences๋ฅผ ์ฌ์ฉํ์ธ์. ์ ๋ง ์ฌ์์!
// ์ ์ฅํ๊ธฐ
Preferences.Set("username", "ํ๊ธธ๋");
Preferences.Set("is_logged_in", true);
Preferences.Set("font_size", 16);
// ๋ถ๋ฌ์ค๊ธฐ
string username = Preferences.Get("username", "๊ธฐ๋ณธ๊ฐ");
bool isLoggedIn = Preferences.Get("is_logged_in", false);
int fontSize = Preferences.Get("font_size", 14);
// ์ญ์ ํ๊ธฐ
Preferences.Remove("username");
// ์ ์ฒด ์ญ์
Preferences.Clear();
๋ด๋ถ์ ์ผ๋ก iOS๋ NSUserDefaults, Android๋ SharedPreferences, Windows๋ ApplicationDataContainer๋ฅผ ์ฌ์ฉํด์. ์ฐ๋ฆฌ๋ ๊ทธ๋ฅ ๊ฐ๋จํ API๋ง ์ฌ์ฉํ๋ฉด ๋์ฃ ! ๐
๐ Secure Storage - ๋ฏผ๊ฐํ ์ ๋ณด ์ ์ฅ
๋น๋ฐ๋ฒํธ๋ ํ ํฐ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ SecureStorage๋ฅผ ์ฌ์ฉํ์ธ์. ์ํธํ๋์ด ์์ ํ๊ฒ ์ ์ฅ๋ผ์!
// ์ ์ฅํ๊ธฐ
await SecureStorage.SetAsync("oauth_token", "abc123xyz");
// ๋ถ๋ฌ์ค๊ธฐ
string token = await SecureStorage.GetAsync("oauth_token");
// ์ญ์ ํ๊ธฐ
SecureStorage.Remove("oauth_token");
๐๏ธ SQLite - ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค
๋ณต์กํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ค๋ฉด SQLite๋ฅผ ์ฌ์ฉํ์ธ์. .NET MAUI์์๋ sqlite-net-pcl NuGet ํจํค์ง๋ฅผ ๋ง์ด ์ฌ์ฉํด์.
// ๋ชจ๋ธ ์ ์
public class TodoItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Title { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
}
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋น์ค
public class TodoDatabase
{
private SQLiteAsyncConnection database;
public TodoDatabase(string dbPath)
{
database = new SQLiteAsyncConnection(dbPath);
database.CreateTableAsync<TodoItem>().Wait();
}
// ์ ์ฒด ์กฐํ
public Task<List<TodoItem>> GetItemsAsync()
{
return database.Table<TodoItem>().ToListAsync();
}
// ๋จ์ผ ์กฐํ
public Task<TodoItem> GetItemAsync(int id)
{
return database.Table<TodoItem>()
.Where(i => i.Id == id)
.FirstOrDefaultAsync();
}
// ์ ์ฅ
public Task<int> SaveItemAsync(TodoItem item)
{
if (item.Id != 0)
return database.UpdateAsync(item);
else
return database.InsertAsync(item);
}
// ์ญ์
public Task<int> DeleteItemAsync(TodoItem item)
{
return database.DeleteAsync(item);
}
}
// ์ฌ์ฉํ๊ธฐ
var dbPath = Path.Combine(
FileSystem.AppDataDirectory,
"todos.db3"
);
var database = new TodoDatabase(dbPath);
// ๋ฐ์ดํฐ ์ถ๊ฐ
await database.SaveItemAsync(new TodoItem
{
Title = "MAUI ๊ณต๋ถํ๊ธฐ",
IsCompleted = false,
CreatedAt = DateTime.Now
});
// ๋ฐ์ดํฐ ์กฐํ
var items = await database.GetItemsAsync();
SQLite๋ ์ ๋ง ๊ฐ๋ ฅํด์! ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๊ฐ๋ฅํ๊ณ , ๊ด๊ณํ ๋ฐ์ดํฐ๋ ์ ์ฅํ ์ ์์ฃ . ์คํ๋ผ์ธ ์ฑ์ ๋ง๋ค ๋ ํ์์์! ๐ช
๐ก Entity Framework Core๋ ์ฌ์ฉ ๊ฐ๋ฅ!
๋ ๊ฐ๋ ฅํ ORM์ ์ํ๋ค๋ฉด Entity Framework Core๋ .NET MAUI์์ ์ฌ์ฉํ ์ ์์ด์. ๋ง์ด๊ทธ๋ ์ด์ , ๋ณต์กํ ๊ด๊ณ ์ค์ ๋ฑ ๋ ๋ง์ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ฃ . ๋ค๋ง ์ฑ ํฌ๊ธฐ๊ฐ ์กฐ๊ธ ์ปค์ง ์ ์์ด์!
๐ ๋คํธ์ํฌ ํต์ ๊ณผ REST API
์์ฆ ์ฑ์ ๋๋ถ๋ถ ์๋ฒ์ ํต์ ํ์ฃ ? .NET MAUI์์ HTTP ํต์ ์ ์ ๋ง ๊ฐ๋จํด์. HttpClient๋ฅผ ์ฌ์ฉํ๋ฉด ๋๊ฑฐ๋ ์! ๐
๐ก ๊ธฐ๋ณธ HTTP ์์ฒญ
public class ApiService
{
private readonly HttpClient httpClient;
private const string BaseUrl = "https://api.example.com";
public ApiService()
{
httpClient = new HttpClient
{
BaseAddress = new Uri(BaseUrl)
};
}
// GET ์์ฒญ
public async Task<List<Product>> GetProductsAsync()
{
try
{
var response = await httpClient.GetAsync("/products");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(json);
return products;
}
catch (Exception ex)
{
// ์๋ฌ ์ฒ๋ฆฌ
Debug.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("/products", content);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
return false;
}
}
// ์ธ์ฆ ํ ํฐ ์ถ๊ฐ
public void SetAuthToken(string token)
{
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}
}
๐ Refit์ผ๋ก ๋ ์ฝ๊ฒ
Refit์ด๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด REST API ํธ์ถ์ด ํจ์ฌ ๊ฐ๋จํด์ ธ์!
// API ์ธํฐํ์ด์ค ์ ์
public interface IProductApi
{
[Get("/products")]
Task<List<Product>> GetProducts();
[Get("/products/{id}")]
Task<Product> GetProduct(int id);
[Post("/products")]
Task<Product> CreateProduct([Body] Product product);
[Put("/products/{id}")]
Task<Product> UpdateProduct(int id, [Body] Product product);
[Delete("/products/{id}")]
Task DeleteProduct(int id);
}
// ์ฌ์ฉํ๊ธฐ
var api = RestService.For<IProductApi>("https://api.example.com");
var products = await api.GetProducts();
๋ณด์ธ์! ์ธํฐํ์ด์ค๋ง ์ ์ํ๋ฉด Refit์ด ์๋์ผ๋ก ๊ตฌํ์ฒด๋ฅผ ๋ง๋ค์ด์ค์. ์ ๋ง ๋ง๋ฒ ๊ฐ์ฃ ? โจ
๐ถ ์ฐ๊ฒฐ ์ํ ํ์ธํ๊ธฐ
๋คํธ์ํฌ ์์ฒญ ์ ์ ์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํ๋ ๊ฒ ์ข์์:
public async Task<List<Product>> GetProductsAsync()
{
// ์ฐ๊ฒฐ ์ํ ํ์ธ
if (Connectivity.NetworkAccess != NetworkAccess.Internet)
{
await App.Current.MainPage.DisplayAlert(
"์ค๋ฅ",
"์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์.",
"ํ์ธ"
);
return new List<Product>();
}
// ๋คํธ์ํฌ ์์ฒญ ์งํ
var products = await apiService.GetProductsAsync();
return products;
}
// ์ฐ๊ฒฐ ์ํ ๋ณํ ๊ฐ์ง
Connectivity.ConnectivityChanged += (sender, e) =>
{
if (e.NetworkAccess == NetworkAccess.Internet)
{
Debug.WriteLine("์ธํฐ๋ท ์ฐ๊ฒฐ๋จ!");
}
else
{
Debug.WriteLine("์ธํฐ๋ท ์ฐ๊ฒฐ ๋๊น!");
}
};
๐ฌ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค ์ดํด๋ณด๊ธฐ
์ด์ ์ข ๋ ์ฌ๋ฏธ์๋ ๊ธฐ๋ฅ๋ค์ ์์๋ณผ๊น์? .NET MAUI๋ ์ ๋ง ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํด์! ๐ช
๐ธ ์นด๋ฉ๋ผ์ ๋ฏธ๋์ด
์ฌ์ง ์ฐ๊ธฐ๋ ๊ฐค๋ฌ๋ฆฌ์์ ์ด๋ฏธ์ง ์ ํํ๊ธฐ๋ MediaPicker๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ์:
// ์ฌ์ง ์ฐ๊ธฐ
public async Task<FileResult> TakePhotoAsync()
{
try
{
var photo = await MediaPicker.CapturePhotoAsync();
return photo;
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
return null;
}
}
// ๊ฐค๋ฌ๋ฆฌ์์ ์ ํ
public async Task<FileResult> PickPhotoAsync()
{
try
{
var photo = await MediaPicker.PickPhotoAsync(new MediaPickerOptions
{
Title = "์ฌ์ง์ ์ ํํ์ธ์"
});
return photo;
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
return null;
}
}
// ์ด๋ฏธ์ง ํ์ํ๊ธฐ
var photo = await PickPhotoAsync();
if (photo != null)
{
var stream = await photo.OpenReadAsync();
MyImage.Source = ImageSource.FromStream(() => stream);
}
๐ ์์น ์ ๋ณด
GPS ์์น๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ๋ ๊ฐ๋จํด์:
public async Task<Location> GetCurrentLocationAsync()
{
try
{
var location = await Geolocation.GetLastKnownLocationAsync();
if (location == null)
{
var request = new GeolocationRequest(
GeolocationAccuracy.Medium,
TimeSpan.FromSeconds(10)
);
location = await Geolocation.GetLocationAsync(request);
}
if (location != null)
{
Debug.WriteLine($"์๋: {location.Latitude}");
Debug.WriteLine($"๊ฒฝ๋: {location.Longitude}");
}
return location;
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
return null;
}
}
๊ถํ ์์ฒญ๋ ์์ง ๋ง์ธ์! Android์ ๊ฒฝ์ฐ AndroidManifest.xml์, iOS์ ๊ฒฝ์ฐ Info.plist์ ๊ถํ์ ์ถ๊ฐํด์ผ ํด์.
๐ ๋ก์ปฌ ์๋ฆผ
ํธ์ ์๋ฆผ์ด ์๋ ๋ก์ปฌ ์๋ฆผ์ ๋ณด๋ด๊ณ ์ถ๋ค๋ฉด Plugin.LocalNotification ๊ฐ์ ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ ์ ์์ด์:
// ์ฆ์ ์๋ฆผ
await LocalNotificationCenter.Current.Show(new NotificationRequest
{
NotificationId = 1,
Title = "์๋
ํ์ธ์!",
Description = "MAUI์์ ๋ณด๋ด๋ ์๋ฆผ์ด์์ ๐",
BadgeNumber = 1
});
// ์์ฝ ์๋ฆผ
await LocalNotificationCenter.Current.Show(new NotificationRequest
{
NotificationId = 2,
Title = "๋ฆฌ๋ง์ธ๋",
Description = "30๋ถ ํ์ ํ์๊ฐ ์์ด์!",
Schedule = new NotificationRequestSchedule
{
NotifyTime = DateTime.Now.AddMinutes(30)
}
});
๐จ ์ ๋๋ฉ์ด์
UI ์์์ ์ ๋๋ฉ์ด์ ์ ์ถ๊ฐํ๋ฉด ์ฑ์ด ํจ์ฌ ์๋๊ฐ ์์ด์ ธ์!
// ํ์ด๋ ์ธ/์์
await MyLabel.FadeTo(0, 1000); // 1์ด ๋์ ํฌ๋ช
ํ๊ฒ
await MyLabel.FadeTo(1, 1000); // 1์ด ๋์ ๋ถํฌ๋ช
ํ๊ฒ
// ํฌ๊ธฐ ๋ณํ
await MyButton.ScaleTo(1.5, 500); // 0.5์ด ๋์ 1.5๋ฐฐ ํ๋
await MyButton.ScaleTo(1, 500); // ์๋ ํฌ๊ธฐ๋ก
// ํ์
await MyImage.RotateTo(360, 2000); // 2์ด ๋์ 360๋ ํ์
// ์ด๋
await MyView.TranslateTo(100, 100, 1000); // 1์ด ๋์ (100,100)์ผ๋ก ์ด๋
// ๋ณตํฉ ์ ๋๋ฉ์ด์
var animation = new Animation();
animation.Add(0, 0.5, new Animation(v => MyView.Scale = v, 1, 1.5));
animation.Add(0.5, 1, new Animation(v => MyView.Rotation = v, 0, 360));
animation.Commit(this, "ComplexAnimation", length: 2000);
๐ ์ฑ๋ฅ ์ต์ ํ ํ
์ฑ์ ๋ง๋ค์๋๋ฐ ๋๋ฆฌ๋ค๋ฉด? ์ฌ์ฉ์๋ค์ด ๊ธ๋ฐฉ ๋ ๋๋ฒ๋ฆด ๊ฑฐ์์. ์ฑ๋ฅ ์ต์ ํ๋ ์ ๋ง ์ค์ํด์! ๐จ
1๏ธโฃ CollectionView ๊ฐ์ํ ํ์ฉ
๊ธด ๋ฆฌ์คํธ๋ฅผ ํ์ํ ๋๋ ListView ๋์ CollectionView๋ฅผ ์ฌ์ฉํ์ธ์. ์๋์ผ๋ก ๊ฐ์ํ๋์ด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํจ์ฌ ์ ๊ฒ ์ฌ์ฉํด์!
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<!-- ์์ดํ
ํ
ํ๋ฆฟ -->
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
2๏ธโฃ ์ด๋ฏธ์ง ์ต์ ํ
ํฐ ์ด๋ฏธ์ง๋ ์ฑ์ ๋๋ฆฌ๊ฒ ๋ง๋ค์ด์. ์ ์ ํ ํฌ๊ธฐ๋ก ๋ฆฌ์ฌ์ด์งํ๊ณ , ์บ์ฑ์ ํ์ฉํ์ธ์!
<Image Source="photo.jpg"
Aspect="AspectFill"
HeightRequest="200"
WidthRequest="200" />
<!-- URL ์ด๋ฏธ์ง๋ ์๋ ์บ์ฑ -->
<Image Source="https://example.com/image.jpg" />
3๏ธโฃ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ
๋ฌด๊ฑฐ์ด ์์ ์ ๋ฐ๋์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ์ธ์. UI ์ค๋ ๋๋ฅผ ๋ธ๋กํ๋ฉด ์ฑ์ด ๋ฉ์ถ ๊ฒ์ฒ๋ผ ๋ณด์ฌ์!
// โ ๋์ ์
public void LoadData()
{
var data = database.GetItems(); // UI ๋ธ๋ก!
Items = new ObservableCollection<Item>(data);
}
// โ
์ข์ ์
public async Task LoadDataAsync()
{
var data = await database.GetItemsAsync();
Items = new ObservableCollection<Item>(data);
}
4๏ธโฃ ์ปดํ์ผ ๋ชจ๋ ์ต์ ํ
๋ฆด๋ฆฌ์ค ๋น๋์์๋ XAML ์ปดํ์ผ๊ณผ ๋ง์ปค๋ฅผ ํ์ฑํํ์ธ์. ์ฑ ํฌ๊ธฐ๋ ์ค๊ณ ์ฑ๋ฅ๋ ์ข์์ ธ์!
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<XamlCompilation>Compile</XamlCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
</PropertyGroup>
5๏ธโฃ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํ๋ค๋ฉด ๋ฐ๋์ ํด์ ํ์ธ์!
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<string>(this, "UpdateData", (sender) =>
{
// ์ฒ๋ฆฌ
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<string>(this, "UpdateData");
}
๐งช ํ ์คํ ๊ณผ ๋๋ฒ๊น
๋ฒ๊ทธ ์๋ ์ฑ์ ์์ด์. ํ์ง๋ง ํ ์คํธ๋ฅผ ์ํ๋ฉด ๋ฒ๊ทธ๋ฅผ ์ต์ํํ ์ ์์ฃ ! ๐
๐ ๋จ์ ํ ์คํธ
ViewModel์ด๋ ๋น์ฆ๋์ค ๋ก์ง์ ๋จ์ ํ ์คํธ๋ก ๊ฒ์ฆํ์ธ์:
// xUnit ์ฌ์ฉ ์์
public class TodoViewModelTests
{
[Fact]
public void AddTodo_ShouldAddItemToCollection()
{
// Arrange
var viewModel = new TodoViewModel();
viewModel.NewTodoText = "ํ
์คํธ ํญ๋ชฉ";
// Act
viewModel.AddTodoCommand.Execute(null);
// Assert
Assert.Single(viewModel.TodoItems);
Assert.Equal("ํ
์คํธ ํญ๋ชฉ", viewModel.TodoItems[0].Title);
}
[Fact]
public void AddTodo_WithEmptyText_ShouldNotAddItem()
{
// Arrange
var viewModel = new TodoViewModel();
viewModel.NewTodoText = "";
// Act
viewModel.AddTodoCommand.Execute(null);
// Assert
Assert.Empty(viewModel.TodoItems);
}
}
๐ฏ UI ํ ์คํธ
UI ํ ์คํธ๋ Appium์ด๋ Xamarin.UITest๋ฅผ ์ฌ์ฉํ ์ ์์ด์. ์ค์ ์ฌ์ฉ์์ฒ๋ผ ์ฑ์ ์กฐ์ํ๋ฉด์ ํ ์คํธํ์ฃ !
๐ ๋๋ฒ๊น ํ
Hot Reload ํ์ฉ
XAML์ ์์ ํ๋ฉด ์ฑ์ ์ฌ์์ํ์ง ์์๋ ๋ฐ๋ก ๋ฐ์๋ผ์. ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ์! โก
Live Visual Tree
Visual Studio์ Live Visual Tree๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ ์ค์ธ ์ฑ์ UI ๊ตฌ์กฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ณผ ์ ์์ด์. ๋ ์ด์์ ๋ฌธ์ ๋ฅผ ์ฐพ์ ๋ ์ ๋ง ์ ์ฉํด์!
๋ก๊น
Debug.WriteLine()์ด๋ ๋ก๊น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ์ธ์:
Debug.WriteLine($"๋ฐ์ดํฐ ๋ก๋ ์๋ฃ: {items.Count}๊ฐ ํญ๋ชฉ");
// ๋๋ ILogger ์ฌ์ฉ
logger.LogInformation("์ฌ์ฉ์ {UserId}๊ฐ ๋ก๊ทธ์ธํ์ต๋๋ค", userId);
๐ฆ ๋ฐฐํฌํ๊ธฐ
๋๋์ด ์ฑ์ ์์ฑํ์ด์! ์ด์ ์ธ์์ ๊ณต๊ฐํ ์๊ฐ์ด์์! ๐
๐ iOS ์ฑ ๋ฐฐํฌ
1. Apple Developer ๊ณ์ ํ์
์ฐ๊ฐ $99๋ฅผ ๋ด๊ณ Apple Developer Program์ ๊ฐ์ ํด์ผ ํด์. (๊ฐ์ธ ๊ฐ๋ฐ์ ๊ธฐ์ค)
2. ์ธ์ฆ์์ ํ๋ก๋น์ ๋ ํ๋กํ์ผ ์์ฑ
Apple Developer ์ฌ์ดํธ์์ ์์ฑํ๊ฑฐ๋, Visual Studio์์ ์๋์ผ๋ก ์์ฑํ ์ ์์ด์.
3. App Store Connect์ ์ฑ ๋ฑ๋ก
์ฑ ์ ๋ณด, ์คํฌ๋ฆฐ์ท, ์ค๋ช ๋ฑ์ ์ ๋ ฅํ๊ณ ์ฌ์ฌ๋ฅผ ์์ฒญํด์. ๋ณดํต 1-3์ผ ์ ๋ ๊ฑธ๋ ค์.
๐ค Android ์ฑ ๋ฐฐํฌ
1. Google Play Console ๊ณ์
์ผํ์ฑ $25๋ฅผ ๋ด๋ฉด ํ์ ์ฌ์ฉํ ์ ์์ด์!
2. ์๋ช ํค ์์ฑ
Visual Studio์์ ์๋์ผ๋ก ์์ฑํ๊ฑฐ๋, keytool๋ก ์ง์ ๋ง๋ค ์ ์์ด์.
keytool -genkey -v -keystore myapp.keystore
-alias myapp -keyalg RSA -keysize 2048
-validity 10000
3. AAB ํ์ผ ์์ฑ ๋ฐ ์ ๋ก๋
Android App Bundle(.aab) ํ์ผ์ ์์ฑํด์ Play Console์ ์ ๋ก๋ํ๋ฉด ๋ผ์!
๐ช Windows ์ฑ ๋ฐฐํฌ
Microsoft Store๋ฅผ ํตํด ๋ฐฐํฌํ๊ฑฐ๋, ์ง์ ์ค์น ํ์ผ(.msix)์ ๋ฐฐํฌํ ์ ์์ด์. Store๋ฅผ ํตํ๋ฉด ์๋ ์ ๋ฐ์ดํธ ๊ฐ์ ํํ์ด ์์ฃ !
๐ก ๋ฒ ํ ํ ์คํธ ํ์ฉํ๊ธฐ
์ ์ ์ถ์ ์ ์ TestFlight(iOS)๋ Google Play ๋ด๋ถ ํ ์คํธ(Android)๋ฅผ ํ์ฉํด์ ๋ฒ ํ ํ ์คํฐ๋ค์๊ฒ ๋จผ์ ๊ณต๊ฐํด๋ณด์ธ์. ๋ฒ๊ทธ๋ฅผ ๋ฏธ๋ฆฌ ์ฐพ์ ์ ์์ด์!
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
.NET MAUI๋ฅผ ๋ฐฐ์ฐ๋ ์ฌ์ ์์ ํผ์๊ฐ ์๋์์! ์ ๋ง ๋ง์ ๋ฆฌ์์ค์ ํ๋ฐํ ์ปค๋ฎค๋ํฐ๊ฐ ์๋ต๋๋ค. ๐ค
๐ ๊ณต์ ๋ฌธ์
Microsoft์ ๊ณต์ ๋ฌธ์๋ ์ ๋ง ์ ๋์ด ์์ด์. learn.microsoft.com์์ .NET MAUI๋ฅผ ๊ฒ์ํด๋ณด์ธ์!
๐ฅ ์ ํ๋ธ ์ฑ๋
- dotNET ๊ณต์ ์ฑ๋
- James Montemagno (MAUI ํ ๋ฉค๋ฒ!)
- Gerald Versluis
๐ฌ ์ปค๋ฎค๋ํฐ
- GitHub Discussions
- Stack Overflow (#maui ํ๊ทธ)
- Discord ์๋ฒ๋ค
- Reddit r/dotnet
๐ฑ ์ํ ์ฑ
GitHub์ ์๋ microsoft/dotnet-maui-samples ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ๊ผญ ํ์ธํด๋ณด์ธ์. ์ค์ ๋์ํ๋ ์์ ๋ค์ด ๊ฐ๋ํด์!
๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์๋ .NET MAUI ๊ด๋ จ ๋ฉํ ๋ง์ด๋ ํ๋ก์ ํธ ์๋ขฐ๋ฅผ ์ฐพ์๋ณผ ์ ์์ด์. ์ค์ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์๋ค์๊ฒ ์ง์ ๋ฐฐ์ธ ์ ์๋ ์ข์ ๊ธฐํ์ฃ ! ๐
๐ ์ค์ ํ๋ก์ ํธ ์์ด๋์ด
์ด๋ก ๋ง ๊ณต๋ถํ๋ฉด ์ฌ๋ฏธ์์ฃ ? ์ง์ ๋ง๋ค์ด๋ณด๋ฉด์ ๋ฐฐ์์ผ ์ง์ง ์ค๋ ฅ์ด ๋์ด์! ์ฌ๊ธฐ ๋ช ๊ฐ์ง ํ๋ก์ ํธ ์์ด๋์ด๋ฅผ ๋๋ฆด๊ฒ์. ๐จ
1๏ธโฃ ๋ ์จ ์ฑ
OpenWeatherMap API๋ฅผ ์ฌ์ฉํด์ ํ์ฌ ์์น์ ๋ ์จ๋ฅผ ๋ณด์ฌ์ฃผ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์ธ์. ์์น ์ ๋ณด, HTTP ํต์ , UI ๋์์ธ์ ๋ชจ๋ ์ฐ์ตํ ์ ์์ด์! โ๏ธ
2๏ธโฃ ํ ์ผ ๊ด๋ฆฌ ์ฑ
SQLite๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ , MVVM ํจํด์ ์ ์ฉํด๋ณด์ธ์. ์๋ฆผ ๊ธฐ๋ฅ๋ ์ถ๊ฐํ๋ฉด ๋ ์ข๊ณ ์! โ
3๏ธโฃ ์์ ๋ฏธ๋์ด ํด๋ผ์ด์ธํธ
Twitter๋ Reddit API๋ฅผ ์ฌ์ฉํด์ ํผ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์ธ์. CollectionView, ์ด๋ฏธ์ง ๋ก๋ฉ, ๋ฌดํ ์คํฌ๋กค ๋ฑ์ ๋ฐฐ์ธ ์ ์์ด์! ๐ฑ
4๏ธโฃ ๊ฐ๊ณ๋ถ ์ฑ
์์ ๊ณผ ์ง์ถ์ ๊ธฐ๋กํ๊ณ , ์ฐจํธ๋ก ์๊ฐํํด๋ณด์ธ์. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ์ ๋ณต์กํ UI๋ฅผ ๋ค๋ฃจ๋ ์ฐ์ต์ด ๋ผ์! ๐ฐ
5๏ธโฃ ์์ ํ๋ ์ด์ด
๋ก์ปฌ ์์ ํ์ผ์ ์ฌ์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์ธ์. ๋ฏธ๋์ด ์ฌ์, ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ , ํ๋ซํผ๋ณ ๊ธฐ๋ฅ ๋ฑ์ ๋ฐฐ์ธ ์ ์์ด์! ๐ต
์ด๋ฐ ํ๋ก์ ํธ๋ค์ ๋ง๋ค๋ค ๋ณด๋ฉด ์ด๋์ .NET MAUI ์ ๋ฌธ๊ฐ๊ฐ ๋์ด ์์ ๊ฑฐ์์! ๊ทธ๋ฆฌ๊ณ ์์ฑํ ํ๋ก์ ํธ๋ ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํ ์๋ ์๊ณ , ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋น์ทํ ํ๋ก์ ํธ๋ฅผ ์๋ขฐ๋ฐ์ ๋ ํฐ ๋์์ด ๋์ฃ ! ๐ช
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ค์๋๋ผ ์ ๋ง ์๊ณ ๋ง์ผ์ จ์ด์! ๐
.NET MAUI๋ ์ ๋ง ๊ฐ๋ ฅํ ํ๋ ์์ํฌ์์. ํ ๋ฒ์ ์ฝ๋ ์์ฑ์ผ๋ก iOS, Android, Windows, macOS ์ฑ์ ๋ชจ๋ ๋ง๋ค ์ ์๋ค๋ ๊ฑด ์ ๋ง ๋๋ผ์ด ์ผ์ด์ฃ . ๋ฌผ๋ก ์ฒ์์๋ ๋ฐฐ์ธ ๊ฒ ๋ง์ ๋ณด์ผ ์ ์์ด์. XAML๋ ์ต์ํด์ ธ์ผ ํ๊ณ , MVVM ํจํด๋ ์ดํดํด์ผ ํ๊ณ , ๊ฐ ํ๋ซํผ์ ํน์ฑ๋ ์์์ผ ํ๋๊น์. ๐
ํ์ง๋ง ๊ฑฑ์ ํ์ง ๋ง์ธ์! ํ ๊ฑธ์์ฉ ์ฒ์ฒํ ๋์๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฉ์ง ์ฑ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์์. ์ ๋ ์ฒ์์๋ "์ด๊ฒ ๋๋?" ์ถ์๋๋ฐ, ์ง๊ธ์ MAUI ์์ด๋ ๋ชป ์ด ๊ฒ ๊ฐ์์. ๐
๐ ๊ธฐ์ตํด์ผ ํ ํต์ฌ ํฌ์ธํธ
โ
.NET MAUI๋ ์ง์ง ๋ค์ดํฐ๋ธ ์ฑ์ ๋ง๋ค์ด์
โ
C# ํ๋๋ก ๋ชจ๋ ํ๋ซํผ์ ์ปค๋ฒํ ์ ์์ด์
โ
MVVM ํจํด์ผ๋ก ๊น๋ํ ์ฝ๋๋ฅผ ์ ์งํ์ธ์
โ
์ปค๋ฎค๋ํฐ๊ฐ ํ๋ฐํ๋ ํผ์๊ฐ ์๋์์
โ
์ค์ ํ๋ก์ ํธ๋ก ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋นจ๋ผ์
๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์ ์ธ๊ณ๋ ์ ๋ง ๋๊ณ ์ฌ๋ฏธ์์ด์. ์ฌ๋ฌ๋ถ์ ์์ด๋์ด๋ฅผ ์ค์ ๋์ํ๋ ์ฑ์ผ๋ก ๋ง๋ค ์ ์๋ค๋ ๊ฑด ์ ๋ง ๋ฉ์ง ์ผ์ด์ฃ . ๊ทธ๋ฆฌ๊ณ .NET MAUI๋ ๊ทธ ์ฌ์ ์ ํจ์ฌ ์ฝ๊ณ ์ฆ๊ฒ๊ฒ ๋ง๋ค์ด์ค ๊ฑฐ์์. ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋์์ด ํ์ํ๋ค๋ฉด, ๊ณต์ ๋ฌธ์๋ฅผ ์ฐพ์๋ณด๊ฑฐ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ณด์ธ์. ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์ ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์. ๋๋ก๋ ์ ๋ฌธ๊ฐ์ ์กฐ์ธ ํ๋ง๋๊ฐ ๋ฉฐ์น ๊ฐ์ ์ฝ์ง์ ์ค์ฌ์ฃผ๊ธฐ๋ ํ๊ฑฐ๋ ์! ๐
์, ์ด์ ์ฌ๋ฌ๋ถ์ ์ฒซ .NET MAUI ์ฑ์ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์์! Visual Studio๋ฅผ ์ด๊ณ , ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ณ , ์ฝ๋ฉ์ ์์ํด๋ณด์ธ์. ์ฒ์์๋ ๊ฐ๋จํ ๊ฒ๋ถํฐ ์์ํ๋ฉด ๋ผ์. "Hello, MAUI!" ์ฑ๋ ์ถฉ๋ถํ ๋ฉ์ง ์์์ด๋๋๋ค! ๐ป
์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ํ์ด์ด ๊ฐ๋ํ๊ธธ ๋ฐ๋ผ์! ์ธ์ ๊ฐ ์ฑ์คํ ์ด๋ ํ๋ ์ด์คํ ์ด์์ ์ฌ๋ฌ๋ถ์ด ๋ง๋ ๋ฉ์ง ์ฑ์ ๋ง๋๊ฒ ๋๊ธธ ๊ธฐ๋ํ ๊ฒ์! ๐
Happy Coding! ๐
๐ .NET MAUI๋ก ๋น์ ์ ์์ด๋์ด๋ฅผ ํ์ค๋ก!
์ด ๊ธ์ด ๋์์ด ๋์๋ค๋ฉด, ์ง์ ํ๋ก์ ํธ๋ฅผ ์์ํด๋ณด์ธ์.
๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.