์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Xamarin๊ณผ MAUI๋กœ ์‹œ์ž‘ํ•˜๋Š” ํฌ๋กœ์Šคํ”Œ๋žซํผ ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ

๐Ÿš€ Xamarin๊ณผ MAUI๋กœ ์‹œ์ž‘ํ•˜๋Š” ํฌ๋กœ์Šคํ”Œ๋žซํผ ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ

ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋กœ iOS, Android, Windows๊นŒ์ง€! C#์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฉ€ํ‹ฐํ”Œ๋žซํผ์˜ ์„ธ๊ณ„

iOS Android Windows C# Code One Codebase ๐Ÿ“ฑ ๐Ÿ’ป Xamarin + MAUI = Cross-Platform Magic!

๐ŸŽฏ ํฌ๋กœ์Šคํ”Œ๋žซํผ ๊ฐœ๋ฐœ, ์™œ ์ง€๊ธˆ ์ฃผ๋ชฉ๋ฐ›์„๊นŒ?

์นœ๊ตฌ์•ผ, ์ƒ์ƒํ•ด๋ด. ๋„ˆ๊ฐ€ ๋ฉ‹์ง„ ์•ฑ ์•„์ด๋””์–ด๋ฅผ ๋– ์˜ฌ๋ ธ์–ด. ๊ทธ๋Ÿฐ๋ฐ 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์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ Xamarin.iOS: iOS ์•ฑ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ๋ฐ”์ธ๋”ฉ
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 ๋ Œ๋”๋ง: ํ•ธ๋“ค๋Ÿฌ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜
ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ: ๋‹จ์ผ ํ”„๋กœ์ ํŠธ
ํ•ซ ๋ฆฌ๋กœ๋“œ: ์™„์ „ ์ง€์›
์„ฑ๋Šฅ: ๋”์šฑ ํ–ฅ์ƒ๋จ

โš ๏ธ ์ค‘์š”ํ•œ ์ „ํ™˜์  Microsoft๋Š” 2024๋…„ 5์›”๋ถ€ํ„ฐ Xamarin์— ๋Œ€ํ•œ ๊ณต์‹ ์ง€์›์„ ์ข…๋ฃŒํ–ˆ์–ด. ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋Š” .NET MAUI๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ์ง€. ๊ธฐ์กด Xamarin ํ”„๋กœ์ ํŠธ๋„ MAUI๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ํ•˜์ง€๋งŒ Xamarin์˜ ๊ฐœ๋…๊ณผ ํŒจํ„ด์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์€ ์—ฌ์ „ํžˆ ์ค‘์š”ํ•ด!

โœจ .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์— ํ‘œ์‹œ๋˜๋Š” ๊ฑฐ์ง€. ๋งˆ๋ฒ• ๊ฐ™์ง€? โœจ

๐Ÿ’ก CommunityToolkit.Mvvm ์‚ฌ์šฉํ•˜๊ธฐ ๋งค๋ฒˆ INotifyPropertyChanged๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ๊ท€์ฐฎ๋‹ค๋ฉด, CommunityToolkit.Mvvm NuGet ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•ด๋ด. Source Generator๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค˜. ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง€์ง€!

[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);
    }
}
๐Ÿ’ก Refit ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉํ•˜๊ธฐ

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)
    {
        // ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ
    }
}
โš ๏ธ ํ”„๋กœํŒŒ์ผ๋ง ๋„๊ตฌ ํ™œ์šฉ ์ถ”์ธกํ•˜์ง€ ๋ง๊ณ  ์ธก์ •ํ•ด! Visual Studio์˜ ํ”„๋กœํŒŒ์ผ๋ง ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‹ค์ œ ๋ณ‘๋ชฉ ์ง€์ ์„ ์ฐพ์•„๋‚ด. CPU ์‚ฌ์šฉ๋Ÿ‰, ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น, ๋„คํŠธ์›Œํฌ ํŠธ๋ž˜ํ”ฝ ๋“ฑ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿงช ํ…Œ์ŠคํŒ…๊ณผ ๋””๋ฒ„๊น…

๋ฒ„๊ทธ ์—†๋Š” ์•ฑ์€ ์—†์–ด. ํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ž˜ํ•˜๋ฉด ๋ฒ„๊ทธ๋ฅผ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ›

โœ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (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์˜ ์ž๋™ ๋ฐ ์ˆ˜๋™ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณ ์Šคํ† ์–ด์— ๋“ฑ๋ก๋ผ.

๐Ÿ’ก CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ• ์ˆ˜๋™ ๋ฐฐํฌ๋Š” ๋ฒˆ๊ฑฐ๋กœ์›Œ. **GitHub Actions**, **Azure DevOps**, **App Center** ๊ฐ™์€ CI/CD ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๋ฅผ ํ‘ธ์‹œํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๋นŒ๋“œํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด. ์‹œ๊ฐ„๋„ ์ ˆ์•ฝ๋˜๊ณ  ์‹ค์ˆ˜๋„ ์ค„์–ด๋“ค์ง€! ๐Ÿค–

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ

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 ํŒจํ‚ค์ง€, ์˜คํ”ˆ์†Œ์Šค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ํŠœํ† ๋ฆฌ์–ผ์ด ๊ณ„์† ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์–ด. ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ํ™œ๋ฐœํ• ์ˆ˜๋ก ํ”„๋ ˆ์ž„์›Œํฌ๋„ ๊ฐ•ํ•ด์ง€์ง€.

โš ๏ธ ์ฃผ์˜ํ•  ์  MAUI๋Š” ์•„์ง ์™„๋ฒฝํ•˜์ง€ ์•Š์•„. ๊ฐ€๋” ๋ฒ„๊ทธ๊ฐ€ ์žˆ๊ณ , ๋ฌธ์„œ๊ฐ€ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„๋„ ์žˆ์–ด. ํ•˜์ง€๋งŒ Microsoft๊ฐ€ ์ ๊ทน์ ์œผ๋กœ ๊ฐœ์„ ํ•˜๊ณ  ์žˆ์œผ๋‹ˆ ๊ธฐ๋Œ€ํ•ด๋„ ์ข‹์•„. ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ๋ฐฐ์šฐ๋Š” ๊ฑด ํ•ญ์ƒ ๋„์ „์ด์ง€๋งŒ, ๊ทธ๋งŒํผ ๊ธฐํšŒ๋„ ๋งŽ์•„! ๐ŸŒŸ

๐Ÿ’ก ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

์ด๋ก ๋งŒ ๊ณต๋ถ€ํ•˜๋ฉด ์ง€๋ฃจํ•ด. ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ์ตœ๊ณ ์•ผ! ๋ช‡ ๊ฐ€์ง€ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด๋ฅผ ์ œ์•ˆํ• ๊ฒŒ. ๐ŸŽจ

๐Ÿ“ ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ (To-Do App)

โ€ข SQLite๋กœ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์ €์žฅ
โ€ข MVVM ํŒจํ„ด ์ ์šฉ
โ€ข ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„๋ฅ˜
โ€ข ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง

๐ŸŒค๏ธ ๋‚ ์”จ ์•ฑ

โ€ข REST API ํ˜ธ์ถœ (OpenWeatherMap ๋“ฑ)
โ€ข ์œ„์น˜ ์ •๋ณด ํ™œ์šฉ
โ€ข ์ฐจํŠธ๋กœ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”
โ€ข ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์—…๋ฐ์ดํŠธ
โ€ข ์œ„์ ฏ ์ง€์›

๐Ÿ’ฐ ๊ฐ€๊ณ„๋ถ€ ์•ฑ

โ€ข ์ˆ˜์ž…/์ง€์ถœ ๊ธฐ๋ก
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„
โ€ข ๊ทธ๋ž˜ํ”„๋กœ ์‹œ๊ฐํ™”
โ€ข ์˜ˆ์‚ฐ ์„ค์ • ๋ฐ ์•Œ๋ฆผ
โ€ข ๋ฐ์ดํ„ฐ ๋ฐฑ์—…/๋ณต์›

๐Ÿ“ท ์‚ฌ์ง„ ๊ฐค๋Ÿฌ๋ฆฌ ์•ฑ

โ€ข ์นด๋ฉ”๋ผ ๋ฐ ๊ฐค๋Ÿฌ๋ฆฌ ์ ‘๊ทผ
โ€ข ์ด๋ฏธ์ง€ ํ•„ํ„ฐ ์ ์šฉ
โ€ข ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™”
โ€ข ์•จ๋ฒ” ๊ด€๋ฆฌ
โ€ข ๊ณต์œ  ๊ธฐ๋Šฅ

๐ŸŽต ์Œ์•… ํ”Œ๋ ˆ์ด์–ด

โ€ข ๋กœ์ปฌ ์Œ์•… ํŒŒ์ผ ์žฌ์ƒ
โ€ข ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ
โ€ข ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์ƒ
โ€ข ๋ฏธ๋””์–ด ์ปจํŠธ๋กค
โ€ข ๊ฐ€์‚ฌ ํ‘œ์‹œ

๐Ÿ’ก ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ ํŒ ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ž‘์€ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ ์ง„์ ์œผ๋กœ ๋ฐœ์ „์‹œ์ผœ. MVP(Minimum Viable Product)๋ฅผ ๋จผ์ € ๋งŒ๋“ค๊ณ , ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉด์„œ ๊ฐœ์„ ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๊ทธ๋ฆฌ๊ณ  GitHub์— ์ฝ”๋“œ๋ฅผ ์˜ฌ๋ ค์„œ ํฌํŠธํด๋ฆฌ์˜ค๋กœ ํ™œ์šฉํ•ด๋ด! ๐Ÿ“‚

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋‹ˆ ๋Œ€๋‹จํ•ด! ๐Ÿ‘ Xamarin๊ณผ MAUI์— ๋Œ€ํ•ด ๊ฝค ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์„ ๊ฑฐ์•ผ. ํฌ๋กœ์Šคํ”Œ๋žซํผ ๊ฐœ๋ฐœ์€ ์ •๋ง ๋งค๋ ฅ์ ์ธ ๋ถ„์•ผ์•ผ. ํ•œ ๋ฒˆ์˜ ๊ฐœ๋ฐœ๋กœ ์—ฌ๋Ÿฌ ํ”Œ๋žซํผ์—์„œ ๋™์ž‘ํ•˜๋Š” ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋‹ˆ, ํšจ์œจ์ ์ด์ง€ ์•Š์•„?

๋ฌผ๋ก  ์ฒ˜์Œ์—๋Š” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด. XAML๋„ ์ต์ˆ™ํ•ด์ ธ์•ผ ํ•˜๊ณ , MVVM ํŒจํ„ด๋„ ์ดํ•ดํ•ด์•ผ ํ•˜๊ณ , ๊ฐ ํ”Œ๋žซํผ์˜ ํŠน์„ฑ๋„ ์•Œ์•„์•ผ ํ•˜๋‹ˆ๊นŒ. ํ•˜์ง€๋งŒ ๊ฑฑ์ • ๋งˆ! ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์šฐ๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋ฉ‹์ง„ ์•ฑ์„ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๐Ÿ˜Š

MAUI๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. Microsoft์˜ ์ง€์›๋„ ํƒ„ํƒ„ํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ๋„ ํ™œ๋ฐœํ•ด. .NET ์ƒํƒœ๊ณ„์˜ ๋ชจ๋“  ์žฅ์ ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ๋„ ํฐ ๋ฉ”๋ฆฌํŠธ์ง€. C# ๊ฐœ๋ฐœ์ž๋ผ๋ฉด MAUI๋ฅผ ๋ฐฐ์›Œ๋‘๋Š” ๊ฒŒ ์ •๋ง ์ข‹์€ ํˆฌ์ž๊ฐ€ ๋  ๊ฑฐ์•ผ. ๐Ÿ’Ž

์ด์ œ ๋„ˆ์˜ ์ฐจ๋ก€์•ผ! ์ด ๊ธ€์—์„œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ฒซ MAUI ์•ฑ์„ ๋งŒ๋“ค์–ด๋ด. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ์•ฑ์œผ๋กœ ๋ฐœ์ „์‹œ์ผœ ๋‚˜๊ฐ€. ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„๋ณด๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ํ•„์š”ํ•˜๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„๋„ ์ข‹์•„.

๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ์˜ ์„ธ๊ณ„๋Š” ๋ฌด๊ถ๋ฌด์ง„ํ•ด. ๋„ˆ์˜ ์•„์ด๋””์–ด๋ฅผ ํ˜„์‹ค๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋ฅผ ์ด์ œ ์†์— ์ฅ์—ˆ์–ด. ์ž, ์ด์ œ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ํ™”์ดํŒ…! ๐Ÿš€โœจ

Happy Coding! ๐ŸŽ‰

๋‹น์‹ ์˜ ์ฒซ MAUI ์•ฑ์ด ์„ฑ๊ณตํ•˜๊ธฐ๋ฅผ ์‘์›ํ•ด์š”!

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.