์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ WPF MVVM ํŒจํ„ด์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฐ์Šคํฌํ†ฑ ์•ฑ ์•„ํ‚คํ…์ฒ˜

๐ŸŽจ WPF MVVM ํŒจํ„ด์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฐ์Šคํฌํ†ฑ ์•ฑ ์•„ํ‚คํ…์ฒ˜

์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ๋ฐฐ์šฐ๋Š” C# ๋ฐ์Šคํฌํ†ฑ ๊ฐœ๋ฐœ์˜ ์ •์„

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ WPF(Windows Presentation Foundation)์™€ MVVM(Model-View-ViewModel) ํŒจํ„ด์„ ํ™œ์šฉํ•œ ๋ฐ์Šคํฌํ†ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ ๊ฑฐ์•ผ.

ํ˜น์‹œ WPF๋กœ ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๋‹ค๊ฐ€ ์ฝ”๋“œ๊ฐ€ ์ŠคํŒŒ๊ฒŒํ‹ฐ์ฒ˜๋Ÿผ ์—‰์ผœ์„œ ๋จธ๋ฆฌ ์•„ํŒ ๋˜ ๊ฒฝํ—˜ ์žˆ์–ด? ๐Ÿ˜… ์•„๋‹ˆ๋ฉด UI ์ฝ”๋“œ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋’ค์„ž์—ฌ์„œ ๋‚˜์ค‘์— ์ˆ˜์ •ํ•  ๋•Œ ์–ด๋””๋ฅผ ๊ฑด๋“œ๋ ค์•ผ ํ• ์ง€ ๋ชฐ๋ผ ๋‹นํ™ฉํ–ˆ๋˜ ์ ์€?

๊ทธ๋Ÿฐ ๊ณ ๋ฏผ๋“ค์„ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ๊ฒŒ ๋ฐ”๋กœ MVVM ํŒจํ„ด์ด์•ผ. ์ด ํŒจํ„ด์„ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ์ ์šฉํ•˜๋ฉด, ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜๋„ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ. ๋งˆ์น˜ ์ž˜ ์ •๋ฆฌ๋œ ์„œ๋ž์žฅ์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐Ÿ“ฆ

View ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค XAML๋กœ UI ๊ตฌ์„ฑ ๋ฒ„ํŠผ, ํ…์ŠคํŠธ๋ฐ•์Šค ๋ฆฌ์ŠคํŠธ, ๊ทธ๋ฆฌ๋“œ ๋“ฑ ๐Ÿ‘๏ธ ViewModel ์ค‘๊ฐ„ ์—ฐ๊ฒฐ ๊ณ„์ธต ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์ปค๋งจ๋“œ ์ฒ˜๋ฆฌ INotifyPropertyChanged โš™๏ธ Model ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๐Ÿ’พ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๋ฐ์ดํ„ฐ ์š”์ฒญ

๐ŸŽฏ MVVM ํŒจํ„ด์ด ๋ญ๊ธธ๋ž˜?

MVVM์€ Model-View-ViewModel์˜ ์•ฝ์ž์•ผ. ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ๊ฐ€ WPF์™€ Silverlight๋ฅผ ์œ„ํ•ด 2005๋…„์— ์†Œ๊ฐœํ•œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์ด์ง€. ๐Ÿ—๏ธ

๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฑฐ์•ผ:

๐Ÿ“ฑ View (๋ทฐ)
์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋Š” ํ™”๋ฉด์ด์•ผ. XAML๋กœ ์ž‘์„ฑ๋˜๊ณ , ๋ฒ„ํŠผ, ํ…์ŠคํŠธ๋ฐ•์Šค, ๋ฆฌ์ŠคํŠธ ๊ฐ™์€ UI ์š”์†Œ๋“ค์ด ์—ฌ๊ธฐ ์žˆ์–ด. View๋Š” "์–ด๋–ป๊ฒŒ ๋ณด์ผ๊นŒ?"์—๋งŒ ์ง‘์ค‘ํ•ด.

โš™๏ธ ViewModel (๋ทฐ๋ชจ๋ธ)
View์™€ Model ์‚ฌ์ด์˜ ์ค‘๊ฐ„ ๋‹ค๋ฆฌ ์—ญํ• ์„ ํ•ด. View์— ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๊ณ , ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์„ ์ฒ˜๋ฆฌํ•ด์„œ Model์— ์ „๋‹ฌํ•˜์ง€. "์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ?"๋ฅผ ๋‹ด๋‹นํ•ด.

๐Ÿ’พ Model (๋ชจ๋ธ)
์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์žˆ๋Š” ๊ณณ์ด์•ผ. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ, ๊ณ„์‚ฐ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฐ™์€ ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋“ค์ด ์—ฌ๊ธฐ ์žˆ์–ด. "๋ฌด์—‡์„ ํ• ๊นŒ?"๋ฅผ ์ •์˜ํ•ด.

๐Ÿ’ก ์™œ ์ด๋ ‡๊ฒŒ ๋‚˜๋ˆŒ๊นŒ?

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ค๊ฐ€ ๋ ˆ์Šคํ† ๋ž‘์„ ์šด์˜ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด. ๐Ÿฝ๏ธ
- View๋Š” ์†๋‹˜์ด ๋ณด๋Š” ๋ฉ”๋‰ดํŒ๊ณผ ์‹๋‹น ์ธํ…Œ๋ฆฌ์–ด
- ViewModel์€ ์ฃผ๋ฌธ์„ ๋ฐ›๊ณ  ์ฃผ๋ฐฉ์— ์ „๋‹ฌํ•˜๋Š” ์›จ์ดํ„ฐ
- Model์€ ์‹ค์ œ ์š”๋ฆฌ๋ฅผ ๋งŒ๋“œ๋Š” ์ฃผ๋ฐฉ

๊ฐ์ž ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๋‹ˆ๊นŒ ํšจ์œจ์ ์ด๊ณ , ํ•œ ๋ถ€๋ถ„์„ ๋ฐ”๊ฟ”๋„ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ์˜ํ–ฅ์ด ์ ์–ด!

๐Ÿ” WPF๊ฐ€ MVVM๊ณผ ์ฐฐ๋–ก๊ถํ•ฉ์ธ ์ด์œ 

WPF๋Š” MVVM ํŒจํ„ด์„ ์ ์šฉํ•˜๊ธฐ์— ์ •๋ง ์™„๋ฒฝํ•œ ํ”Œ๋žซํผ์ด์•ผ. ์™œ๋ƒํ•˜๋ฉด WPF๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์ด MVVM์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ง€์›ํ•˜๊ฑฐ๋“ . ๐ŸŽจ

1๏ธโƒฃ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ (Data Binding)

WPF์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์•ผ. XAML์—์„œ UI ์š”์†Œ๋ฅผ ViewModel์˜ ์†์„ฑ์— ์ง์ ‘ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด.

์˜ˆ๋ฅผ ๋“ค์–ด, ํ…์ŠคํŠธ๋ฐ•์Šค์˜ ๋‚ด์šฉ์„ ViewModel์˜ ์†์„ฑ๊ณผ ๋ฐ”์ธ๋”ฉํ•˜๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋‚ด์šฉ์ด ์ž๋™์œผ๋กœ ViewModel์— ๋ฐ˜์˜๋˜๊ณ , ๋ฐ˜๋Œ€๋กœ ViewModel์˜ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด ํ™”๋ฉด๋„ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ผ. ๋งˆ์น˜ ๋งˆ๋ฒ•์ฒ˜๋Ÿผ! โœจ

<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />

์ด ํ•œ ์ค„๋กœ TextBox์™€ ViewModel์˜ UserName ์†์„ฑ์ด ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ๊ฒฐ๋ผ!

2๏ธโƒฃ ์ปค๋งจ๋“œ (Command)

๋ฒ„ํŠผ ํด๋ฆญ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ์ „ํ†ต์ ์ธ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋Œ€์‹  ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, UI ๋กœ์ง์„ ViewModel๋กœ ๊น”๋”ํ•˜๊ฒŒ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ์–ด. ๐ŸŽฎ

์ฝ”๋“œ-๋น„ํ•˜์ธ๋“œ์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ž”๋œฉ ์ž‘์„ฑํ•˜๋Š” ๋Œ€์‹ , ViewModel์—์„œ ์ปค๋งจ๋“œ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋„ ์‰ฝ๊ณ  ์žฌ์‚ฌ์šฉ๋„ ๊ฐ€๋Šฅํ•ด!

<Button Content="์ €์žฅ" Command="{Binding SaveCommand}" />

3๏ธโƒฃ ํ…œํ”Œ๋ฆฟ๊ณผ ์Šคํƒ€์ผ

WPF๋Š” UI๋ฅผ ๋ฐ์ดํ„ฐ๋กœ๋ถ€ํ„ฐ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. DataTemplate์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŽญ

์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ์นด๋“œ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ค„์ง€, ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ค„์ง€ ViewModel์„ ์ „ํ˜€ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  XAML๋งŒ ๋ฐ”๊ฟ”์„œ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์–ด!

4๏ธโƒฃ INotifyPropertyChanged ์ธํ„ฐํŽ˜์ด์Šค

์ด๊ฑด MVVM์˜ ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด์•ผ. ViewModel์˜ ์†์„ฑ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ View์—๊ฒŒ ์•Œ๋ ค์ฃผ๋Š” ์—ญํ• ์„ ํ•ด. ๐Ÿ“ข

์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด, ์†์„ฑ ๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ UI๊ฐ€ ์—…๋ฐ์ดํŠธ๋ผ. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ผ์ผ์ด UI๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์–ด!

๐Ÿ—๏ธ MVVM ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„์˜ ํ•ต์‹ฌ ์›์น™

์ด์ œ ์‹ค์ œ๋กœ MVVM ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•  ๋•Œ ์ง€์ผœ์•ผ ํ•  ํ•ต์‹ฌ ์›์น™๋“ค์„ ์•Œ์•„๋ณด์ž. ์ด ์›์น™๋“ค์„ ์ž˜ ๋”ฐ๋ฅด๋ฉด ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฝ๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

์›์น™ 1: View๋Š” ViewModel๋งŒ ์•Œ์•„์•ผ ํ•ด

View๋Š” ViewModel์— ๋Œ€ํ•œ ์ฐธ์กฐ๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•ด. Model์„ ์ง์ ‘ ์ฐธ์กฐํ•˜๋ฉด ์•ˆ ๋ผ! ๐Ÿšซ

View์˜ DataContext์— ViewModel์„ ์„ค์ •ํ•˜๊ณ , ๋ชจ๋“  ๋ฐ์ดํ„ฐ์™€ ์ปค๋งจ๋“œ๋Š” ๋ฐ”์ธ๋”ฉ์„ ํ†ตํ•ด ์ ‘๊ทผํ•ด์•ผ ํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด View์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋ผ.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();
    }
}

์ด๊ฒŒ ์ „๋ถ€์•ผ! View์˜ ์ฝ”๋“œ-๋น„ํ•˜์ธ๋“œ๋Š” ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•ด์•ผ ํ•ด. ๐Ÿ˜Š

์›์น™ 2: ViewModel์€ View๋ฅผ ๋ชฐ๋ผ์•ผ ํ•ด

์ด๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•ด! ViewModel์€ View์— ๋Œ€ํ•œ ์–ด๋–ค ์ฐธ์กฐ๋„ ๊ฐ€์ง€๋ฉด ์•ˆ ๋ผ. using System.Windows ๊ฐ™์€ UI ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋„ ViewModel์—์„œ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋ผ! ๐Ÿ™…โ€โ™‚๏ธ

์™œ๋ƒํ•˜๋ฉด ViewModel์€ ์ˆœ์ˆ˜ํ•œ C# ํด๋ž˜์Šค์—ฌ์•ผ ํ•˜๊ฑฐ๋“ . ๊ทธ๋ž˜์•ผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค๋ฅธ ํ”Œ๋žซํผ(์˜ˆ: Xamarin, UWP)์—์„œ๋„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

๋Œ€ํ™”์ƒ์ž๋ฅผ ๋„์šฐ๊ฑฐ๋‚˜ ํŒŒ์ผ์„ ์„ ํƒํ•˜๋Š” ๋“ฑ UI์™€ ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋Š” ์„œ๋น„์Šค ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.

์›์น™ 3: Model์€ ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•ด

Model์€ View๋‚˜ ViewModel์— ๋Œ€ํ•ด ์ „ํ˜€ ๋ชฐ๋ผ์•ผ ํ•ด. ์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์—ฌ์•ผ ํ•˜์ง€. ๐Ÿ’ผ

Model์€ ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ์ •๋„๋กœ ๋ฒ”์šฉ์ ์ด์–ด์•ผ ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฐ™์€ Model์„ ์›น API, ์ฝ˜์†” ์•ฑ, WPF ์•ฑ์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด!

์›์น™ 4: ๋‹จ๋ฐฉํ–ฅ ์˜์กด์„ฑ

์˜์กด์„ฑ์€ ํ•ญ์ƒ ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ํ˜๋Ÿฌ์•ผ ํ•ด: View โ†’ ViewModel โ†’ Model ๐Ÿ“Š

์—ญ๋ฐฉํ–ฅ ์˜์กด์„ฑ์ด ์ƒ๊ธฐ๋ฉด ์•ˆ ๋ผ! ์ด๊ฑธ ์ง€ํ‚ค๋ฉด ๊ฐ ๋ ˆ์ด์–ด๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด. ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ์ƒ๊ธฐ๋ฉด ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›Œ์ ธ.

MVVM ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ViewModel ์ฒ˜๋ฆฌ Model ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ (PropertyChanged) ๐Ÿ‘† โš™๏ธ ๐Ÿ’พ ๐Ÿ””

๐Ÿ’ป ์‹ค์ „! ViewModel ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ViewModel์„ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์ž์„ธํžˆ ์•Œ์•„๋ณด์ž. ViewModel์€ MVVM์˜ ํ•ต์‹ฌ์ด๋‹ˆ๊นŒ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด! ๐Ÿ”‘

INotifyPropertyChanged ๊ตฌํ˜„

๋จผ์ € ๊ธฐ๋ณธ์ด ๋˜๋Š” BaseViewModel์„ ๋งŒ๋“ค์–ด๋ณด์ž. ๋ชจ๋“  ViewModel์ด ์ƒ์†๋ฐ›์„ ๊ธฐ๋ณธ ํด๋ž˜์Šค์•ผ:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

์ด BaseViewModel์€ ์†์„ฑ ๋ณ€๊ฒฝ ์•Œ๋ฆผ์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. SetProperty ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์†์„ฑ setter๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ˜Ž

์†์„ฑ(Property) ๊ตฌํ˜„

์ด์ œ ์‹ค์ œ ViewModel์—์„œ ์†์„ฑ์„ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๋ณด์ž:

public class UserViewModel : BaseViewModel
{
    private string _userName;
    public string UserName
    {
        get => _userName;
        set => SetProperty(ref _userName, value);
    }

    private int _age;
    public int Age
    {
        get => _age;
        set
        {
            if (SetProperty(ref _age, value))
            {
                // Age๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด IsAdult๋„ ์—…๋ฐ์ดํŠธ
                OnPropertyChanged(nameof(IsAdult));
            }
        }
    }

    public bool IsAdult => Age >= 18;
}

๋ณด๋‹ค์‹œํ”ผ SetProperty๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ์ •๋ง ๊ฐ„๊ฒฐํ•ด์ ธ! ๊ทธ๋ฆฌ๊ณ  ํ•œ ์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ๋‹ค๋ฅธ ์†์„ฑ๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•œ๋‹ค๋ฉด, OnPropertyChanged๋ฅผ ์ถ”๊ฐ€๋กœ ํ˜ธ์ถœํ•˜๋ฉด ๋ผ. ๐ŸŽฏ

๐Ÿ’ก ํ”„๋กœ ํŒ!

์†์„ฑ ์ด๋ฆ„์„ ๋ฌธ์ž์—ด๋กœ ์ง์ ‘ ์“ฐ๋Š” ๋Œ€์‹  nameof ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•ด! ์˜คํƒ€๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ๋„ ์•ˆ์ „ํ•ด. ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ฒดํฌํ•ด์ฃผ๊ฑฐ๋“ ! โœ…

โšก ์ปค๋งจ๋“œ(Command) ํŒจํ„ด ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

์ปค๋งจ๋“œ๋Š” ์‚ฌ์šฉ์ž์˜ ์•ก์…˜(๋ฒ„ํŠผ ํด๋ฆญ, ๋ฉ”๋‰ด ์„ ํƒ ๋“ฑ)์„ ์ฒ˜๋ฆฌํ•˜๋Š” MVVM์˜ ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด์•ผ. ์ „ํ†ต์ ์ธ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•ด! ๐Ÿš€

RelayCommand ๊ตฌํ˜„

๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์ปค๋งจ๋“œ ๊ตฌํ˜„ ๋ฐฉ์‹์ด์•ผ. ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด์ž:

using System;
using System.Windows.Input;

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }
}

์ด RelayCommand๋Š” ๋‘ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:
1๏ธโƒฃ Execute: ์‹ค์ œ๋กœ ์ˆ˜ํ–‰ํ•  ์ž‘์—…
2๏ธโƒฃ CanExecute: ์ปค๋งจ๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ (๋ฒ„ํŠผ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”)

ViewModel์—์„œ ์ปค๋งจ๋“œ ์‚ฌ์šฉํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ViewModel์—์„œ ์ปค๋งจ๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณด์ž:

public class MainViewModel : BaseViewModel
{
    private string _searchText;
    public string SearchText
    {
        get => _searchText;
        set => SetProperty(ref _searchText, value);
    }

    private ICommand _searchCommand;
    public ICommand SearchCommand
    {
        get
        {
            if (_searchCommand == null)
            {
                _searchCommand = new RelayCommand(
                    execute: param => PerformSearch(),
                    canExecute: param => !string.IsNullOrWhiteSpace(SearchText)
                );
            }
            return _searchCommand;
        }
    }

    private void PerformSearch()
    {
        // ์‹ค์ œ ๊ฒ€์ƒ‰ ๋กœ์ง
        // ์˜ˆ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ, API ํ˜ธ์ถœ ๋“ฑ
    }
}

XAML์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ๋ฐ”์ธ๋”ฉํ•ด:

<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="๊ฒ€์ƒ‰" Command="{Binding SearchCommand}" />

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด SearchText๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ ๋ฒ„ํŠผ์ด ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ผ! ๋ณ„๋„์˜ ์ฝ”๋“œ ์—†์ด ๋ง์ด์•ผ. ๋ฉ‹์ง€์ง€? ๐Ÿ˜Ž

๋น„๋™๊ธฐ ์ปค๋งจ๋“œ

์š”์ฆ˜ ์•ฑ์€ ๋Œ€๋ถ€๋ถ„ ๋น„๋™๊ธฐ ์ž‘์—…์„ ํ•ด์•ผ ํ•ด. ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ, ํŒŒ์ผ I/O, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋“ฑ๋“ฑ. ๋น„๋™๊ธฐ ์ปค๋งจ๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

public class AsyncRelayCommand : ICommand
{
    private readonly Func<object, Task> _execute;
    private readonly Func<object, bool> _canExecute;
    private bool _isExecuting;

    public AsyncRelayCommand(Func<object, Task> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return !_isExecuting && (_canExecute == null || _canExecute(parameter));
    }

    public async void Execute(object parameter)
    {
        _isExecuting = true;
        CommandManager.InvalidateRequerySuggested();

        try
        {
            await _execute(parameter);
        }
        finally
        {
            _isExecuting = false;
            CommandManager.InvalidateRequerySuggested();
        }
    }
}

์ด ๋น„๋™๊ธฐ ์ปค๋งจ๋“œ๋Š” ์‹คํ–‰ ์ค‘์— ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ผ์„œ ์ค‘๋ณต ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•ด์ค˜! ๐Ÿ‘

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

์ปค๋งจ๋“œ์—์„œ ๊ธด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด! ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด UI๊ฐ€ ๋ฉˆ์ถฐ์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜๋น ์ ธ. async/await๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์ž! ๐Ÿƒโ€โ™‚๏ธ

๐ŸŽจ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‹ฌํ™”

๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์€ WPF MVVM์˜ ๊ฝƒ์ด์•ผ! ๐ŸŒธ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ์‹ฌํ™” ํ…Œํฌ๋‹‰๋“ค์„ ์•Œ์•„๋ณด์ž!

๋ฐ”์ธ๋”ฉ ๋ชจ๋“œ (Binding Mode)

WPF๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐ”์ธ๋”ฉ ๋ชจ๋“œ๋ฅผ ์ œ๊ณตํ•ด:

๋ชจ๋“œ ์„ค๋ช… ์‚ฌ์šฉ ์˜ˆ์‹œ
OneWay ViewModel โ†’ View ๋‹จ๋ฐฉํ–ฅ ์ฝ๊ธฐ ์ „์šฉ ํ…์ŠคํŠธ, ๋ ˆ์ด๋ธ”
TwoWay ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” TextBox, CheckBox ๋“ฑ ์ž…๋ ฅ ์ปจํŠธ๋กค
OneTime ์ดˆ๊ธฐ ํ•œ ๋ฒˆ๋งŒ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ •์  ๋ฐ์ดํ„ฐ
OneWayToSource View โ†’ ViewModel ๋‹จ๋ฐฉํ–ฅ ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉ
<!-- OneWay: ์ฝ๊ธฐ ์ „์šฉ -->
<TextBlock Text="{Binding UserName, Mode=OneWay}" />

<!-- TwoWay: ์–‘๋ฐฉํ–ฅ -->
<TextBox Text="{Binding UserName, Mode=TwoWay}" />

<!-- OneTime: ์ดˆ๊ธฐํ™” ์‹œ ํ•œ ๋ฒˆ๋งŒ -->
<TextBlock Text="{Binding AppTitle, Mode=OneTime}" />

UpdateSourceTrigger

์–ธ์ œ ViewModel์„ ์—…๋ฐ์ดํŠธํ• ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ์†์„ฑ์ด์•ผ:

<!-- PropertyChanged: ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ (ํƒ€์ดํ•‘ํ•  ๋•Œ๋งˆ๋‹ค) -->
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />

<!-- LostFocus: ํฌ์ปค์Šค๋ฅผ ์žƒ์„ ๋•Œ (๊ธฐ๋ณธ๊ฐ’) -->
<TextBox Text="{Binding UserName, UpdateSourceTrigger=LostFocus}" />

<!-- Explicit: ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•  ๋•Œ๋งŒ -->
<TextBox Text="{Binding Email, UpdateSourceTrigger=Explicit}" />

์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ๋•Œ๋Š” PropertyChanged๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์ผ๋ฐ˜ ์ž…๋ ฅ ํผ์—์„œ๋Š” LostFocus๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์„ฑ๋Šฅ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ๊ท ํ˜•์„ ๋งž์ถฐ์•ผ ํ•ด! โš–๏ธ

์ปฌ๋ ‰์…˜ ๋ฐ”์ธ๋”ฉ

๋ฆฌ์ŠคํŠธ๋‚˜ ๊ทธ๋ฆฌ๋“œ์— ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•  ๋•Œ๋Š” ObservableCollection์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

using System.Collections.ObjectModel;

public class MainViewModel : BaseViewModel
{
    public ObservableCollection<UserModel> Users { get; set; }

    public MainViewModel()
    {
        Users = new ObservableCollection<UserModel>();
        LoadUsers();
    }

    private void LoadUsers()
    {
        // ๋ฐ์ดํ„ฐ ๋กœ๋“œ
        Users.Add(new UserModel { Name = "ํ™๊ธธ๋™", Age = 25 });
        Users.Add(new UserModel { Name = "๊น€์ฒ ์ˆ˜", Age = 30 });
    }
}

XAML์—์„œ๋Š”:

<ListBox ItemsSource="{Binding Users}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                <TextBlock Text="{Binding Age}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

ObservableCollection์€ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€/์‚ญ์ œ๋  ๋•Œ ์ž๋™์œผ๋กœ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์ค˜! ์ผ๋ฐ˜ List๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋ผ! ๐Ÿšซ

๊ฐ’ ๋ณ€ํ™˜๊ธฐ (Value Converter)

๋•Œ๋กœ๋Š” ViewModel์˜ ๋ฐ์ดํ„ฐ๋ฅผ View์— ํ‘œ์‹œํ•˜๊ธฐ ์ „์— ๋ณ€ํ™˜ํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด, bool ๊ฐ’์„ "์˜ˆ/์•„๋‹ˆ์˜ค"๋กœ ํ‘œ์‹œํ•˜๊ฑฐ๋‚˜, ๋‚ ์งœ ํ˜•์‹์„ ๋ฐ”๊พธ๋Š” ๋“ฑ์ด์ง€.

using System;
using System.Globalization;
using System.Windows.Data;

public class BoolToYesNoConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool boolValue)
        {
            return boolValue ? "์˜ˆ" : "์•„๋‹ˆ์˜ค";
        }
        return "์•Œ ์ˆ˜ ์—†์Œ";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string strValue)
        {
            return strValue == "์˜ˆ";
        }
        return false;
    }
}

XAML์—์„œ ์‚ฌ์šฉ:

<Window.Resources>
    <local:BoolToYesNoConverter x:Key="BoolToYesNo" />
</Window.Resources>

<TextBlock Text="{Binding IsActive, Converter={StaticResource BoolToYesNo}}" />

์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ž˜ ํ™œ์šฉํ•˜๋ฉด ViewModel์„ ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋‹ค์–‘ํ•œ ํ‘œํ˜„์ด ๊ฐ€๋Šฅํ•ด! ๐ŸŽจ

๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‹œ๋‚˜๋ฆฌ์˜ค XAML (View) TextBox Text ์†์„ฑ ๐Ÿ“ ViewModel UserName ์†์„ฑ string ํƒ€์ž… โš™๏ธ TwoWay Binding ์‚ฌ์šฉ์ž ์ž…๋ ฅ ํ™๊ธธ๋™ ์ž…๋ ฅ ๐Ÿ‘ค ์ž๋™ ์—…๋ฐ์ดํŠธ PropertyChanged ์ด๋ฒคํŠธ ๐Ÿ””

๐Ÿ›๏ธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค๊ณ„

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ MVVM์„ ์ ์šฉํ•  ๋•Œ ์–ด๋–ป๊ฒŒ ํด๋”์™€ ํŒŒ์ผ์„ ๊ตฌ์กฐํ™”ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž. ์ข‹์€ ๊ตฌ์กฐ๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜! ๐Ÿ“

๊ธฐ๋ณธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์ผ๋ฐ˜์ ์œผ๋กœ ์ด๋Ÿฐ ๊ตฌ์กฐ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด:

MyWpfApp/
โ”œโ”€โ”€ Models/
โ”‚   โ”œโ”€โ”€ User.cs
โ”‚   โ”œโ”€โ”€ Product.cs
โ”‚   โ””โ”€โ”€ Order.cs
โ”œโ”€โ”€ ViewModels/
โ”‚   โ”œโ”€โ”€ BaseViewModel.cs
โ”‚   โ”œโ”€โ”€ MainViewModel.cs
โ”‚   โ”œโ”€โ”€ UserViewModel.cs
โ”‚   โ””โ”€โ”€ ProductViewModel.cs
โ”œโ”€โ”€ Views/
โ”‚   โ”œโ”€โ”€ MainWindow.xaml
โ”‚   โ”œโ”€โ”€ UserView.xaml
โ”‚   โ””โ”€โ”€ ProductView.xaml
โ”œโ”€โ”€ Services/
โ”‚   โ”œโ”€โ”€ IDataService.cs
โ”‚   โ”œโ”€โ”€ DataService.cs
โ”‚   โ”œโ”€โ”€ IDialogService.cs
โ”‚   โ””โ”€โ”€ DialogService.cs
โ”œโ”€โ”€ Commands/
โ”‚   โ”œโ”€โ”€ RelayCommand.cs
โ”‚   โ””โ”€โ”€ AsyncRelayCommand.cs
โ”œโ”€โ”€ Converters/
โ”‚   โ”œโ”€โ”€ BoolToVisibilityConverter.cs
โ”‚   โ””โ”€โ”€ DateTimeConverter.cs
โ”œโ”€โ”€ Helpers/
โ”‚   โ””โ”€โ”€ ValidationHelper.cs
โ””โ”€โ”€ App.xaml

๊ฐ ํด๋”์˜ ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๋ฉด ํŒ€ ์ž‘์—…ํ•  ๋•Œ๋„ ํ˜ผ๋ž€์ด ์—†์–ด! ๐Ÿ‘ฅ

๋ ˆ์ด์–ด๋ณ„ ์ฑ…์ž„

๊ฐ ๋ ˆ์ด์–ด๊ฐ€ ์–ด๋–ค ์ฑ…์ž„์„ ๊ฐ€์ง€๋Š”์ง€ ์ •๋ฆฌํ•ด๋ณด์ž:

๐Ÿ“ฑ Views (๋ทฐ)
- XAML ํŒŒ์ผ๊ณผ ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ-๋น„ํ•˜์ธ๋“œ
- UI ๋ ˆ์ด์•„์›ƒ๊ณผ ์Šคํƒ€์ผ ์ •์˜
- ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์‹œ๊ฐ ํšจ๊ณผ
- DataContext ์„ค์ •

โš™๏ธ ViewModels (๋ทฐ๋ชจ๋ธ)
- ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ ์ค€๋น„
- ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ (์ปค๋งจ๋“œ)
- ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
- Model๊ณผ View ์‚ฌ์ด์˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜

๐Ÿ’พ Models (๋ชจ๋ธ)
- ๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ ์ •์˜
- ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
- ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™
- ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง

๐Ÿ”ง Services (์„œ๋น„์Šค)
- ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, API)
- ํŒŒ์ผ I/O
- ๋Œ€ํ™”์ƒ์ž ํ‘œ์‹œ
- ๋„ค๋น„๊ฒŒ์ด์…˜
- ๋กœ๊น…

๐Ÿ’ก ์‹ค๋ฌด ํŒ

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด ๊ฐ ํด๋” ์•ˆ์— ํ•˜์œ„ ํด๋”๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋” ์„ธ๋ถ„ํ™”ํ•  ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด:
- Views/User/ (์‚ฌ์šฉ์ž ๊ด€๋ จ ๋ทฐ๋“ค)
- Views/Product/ (์ œํ’ˆ ๊ด€๋ จ ๋ทฐ๋“ค)
- ViewModels/User/ (์‚ฌ์šฉ์ž ๊ด€๋ จ ๋ทฐ๋ชจ๋ธ๋“ค)

์ด๋ ‡๊ฒŒ ๊ธฐ๋Šฅ๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•˜๋ฉด ์ฐพ๊ธฐ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ! ๐Ÿ”

๐Ÿ”Œ ์˜์กด์„ฑ ์ฃผ์ž… (Dependency Injection)

์˜์กด์„ฑ ์ฃผ์ž…์€ MVVM ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํŒจํ„ด์ด์•ผ. ์ฝ”๋“œ์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๋†’์—ฌ์ค˜! ๐Ÿ’‰

์™œ ์˜์กด์„ฑ ์ฃผ์ž…์ด ํ•„์š”ํ• ๊นŒ?

์˜ˆ๋ฅผ ๋“ค์–ด, ViewModel์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž. ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฉด:

// โŒ ๋‚˜์œ ์˜ˆ
public class UserViewModel : BaseViewModel
{
    private DatabaseService _database;

    public UserViewModel()
    {
        _database = new DatabaseService(); // ๊ฐ•ํ•œ ๊ฒฐํ•ฉ!
    }
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ:
1๏ธโƒฃ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์›Œ (์‹ค์ œ DB๊ฐ€ ํ•„์š”)
2๏ธโƒฃ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋กœ ๋ฐ”๊พธ๊ธฐ ์–ด๋ ค์›Œ
3๏ธโƒฃ ViewModel์ด ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„์— ์˜์กดํ•ด

๋Œ€์‹  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›์ž:

// โœ… ์ข‹์€ ์˜ˆ
public interface IDataService
{
    Task<List<User>> GetUsersAsync();
    Task SaveUserAsync(User user);
}

public class UserViewModel : BaseViewModel
{
    private readonly IDataService _dataService;

    public UserViewModel(IDataService dataService)
    {
        _dataService = dataService; // ์˜์กด์„ฑ ์ฃผ์ž…!
    }

    public async Task LoadUsers()
    {
        var users = await _dataService.GetUsersAsync();
        // ...
    }
}

์ด์ œ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๊ฐ€์งœ(Mock) ์„œ๋น„์Šค๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽญ

DI ์ปจํ…Œ์ด๋„ˆ ์‚ฌ์šฉํ•˜๊ธฐ

์ˆ˜๋™์œผ๋กœ ์˜์กด์„ฑ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑด ๋ณต์žกํ•ด. DI ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜! Microsoft.Extensions.DependencyInjection์„ ์‚ฌ์šฉํ•ด๋ณด์ž:

using Microsoft.Extensions.DependencyInjection;

public partial class App : Application
{
    private ServiceProvider _serviceProvider;

    public App()
    {
        var services = new ServiceCollection();
        ConfigureServices(services);
        _serviceProvider = services.BuildServiceProvider();
    }

    private void ConfigureServices(IServiceCollection services)
    {
        // Services ๋“ฑ๋ก
        services.AddSingleton<IDataService, DatabaseService>();
        services.AddSingleton<IDialogService, DialogService>();
        
        // ViewModels ๋“ฑ๋ก
        services.AddTransient<MainViewModel>();
        services.AddTransient<UserViewModel>();
        
        // Views ๋“ฑ๋ก
        services.AddTransient<MainWindow>();
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        
        var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }
}

์ด์ œ MainWindow์˜ ์ƒ์„ฑ์ž์—์„œ ViewModel์„ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ์–ด:

public partial class MainWindow : Window
{
    public MainWindow(MainViewModel viewModel)
    {
        InitializeComponent();
        this.DataContext = viewModel;
    }
}

์™„์ „ ์ž๋™ํ™”! ๐Ÿค– DI ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•Œ์•„์„œ ํ•„์š”ํ•œ ์˜์กด์„ฑ๋“ค์„ ์ƒ์„ฑํ•˜๊ณ  ์ฃผ์ž…ํ•ด์ค˜.

์„œ๋น„์Šค ์ˆ˜๋ช… (Service Lifetime)

DI ์ปจํ…Œ์ด๋„ˆ์— ์„œ๋น„์Šค๋ฅผ ๋“ฑ๋กํ•  ๋•Œ ์„ธ ๊ฐ€์ง€ ์ˆ˜๋ช… ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด:

์ˆ˜๋ช… ์„ค๋ช… ์‚ฌ์šฉ ์‹œ๊ธฐ
Singleton ์•ฑ ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์„ค์ •, ๋กœ๊น…, ์บ์‹œ ๋“ฑ
Transient ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ ์ธ์Šคํ„ด์Šค ViewModel, ๊ฐ€๋ฒผ์šด ์„œ๋น„์Šค
Scoped ๋ฒ”์œ„ ๋‚ด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค ์›น ์•ฑ์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ (WPF์—์„œ๋Š” ๋“œ๋ฌผ๊ฒŒ)
// Singleton: ์•ฑ ์ „์ฒด์—์„œ ํ•˜๋‚˜๋งŒ
services.AddSingleton<IDataService, DatabaseService>();

// Transient: ๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ
services.AddTransient<MainViewModel>();

// Scoped: ๋ฒ”์œ„ ๋‚ด์—์„œ ํ•˜๋‚˜
services.AddScoped<IUnitOfWork, UnitOfWork>();

โš ๏ธ ์ฃผ์˜!

Singleton ์„œ๋น„์Šค๋Š” ์•ฑ์ด ์ข…๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋ฉ”๋ชจ๋ฆฌ์— ๋‚จ์•„์žˆ์–ด. ํฐ ๊ฐ์ฒด๋ฅผ Singleton์œผ๋กœ ๋“ฑ๋กํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์กฐ์‹ฌํ•ด! ๐Ÿ’ง

๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

MVVM ํŒจํ„ด์˜ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๋‹ค๋Š” ๊ฑฐ์•ผ! ViewModel์€ UI์™€ ๋…๋ฆฝ์ ์ด๋‹ˆ๊นŒ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์™„๋ฒฝํ•ด. ๐ŸŽฏ

ViewModel ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

xUnit๊ณผ Moq๋ฅผ ์‚ฌ์šฉํ•ด์„œ ViewModel์„ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž:

using Xunit;
using Moq;
using System.Threading.Tasks;

public class UserViewModelTests
{
    [Fact]
    public async Task LoadUsers_ShouldPopulateUsersList()
    {
        // Arrange
        var mockDataService = new Mock<IDataService>();
        mockDataService.Setup(x => x.GetUsersAsync())
            .ReturnsAsync(new List<User>
            {
                new User { Name = "ํ™๊ธธ๋™", Age = 25 },
                new User { Name = "๊น€์ฒ ์ˆ˜", Age = 30 }
            });

        var viewModel = new UserViewModel(mockDataService.Object);

        // Act
        await viewModel.LoadUsersCommand.Execute(null);

        // Assert
        Assert.Equal(2, viewModel.Users.Count);
        Assert.Equal("ํ™๊ธธ๋™", viewModel.Users[0].Name);
    }

    [Fact]
    public void UserName_WhenChanged_ShouldRaisePropertyChanged()
    {
        // Arrange
        var mockDataService = new Mock<IDataService>();
        var viewModel = new UserViewModel(mockDataService.Object);
        bool propertyChangedRaised = false;
        
        viewModel.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName == nameof(viewModel.UserName))
                propertyChangedRaised = true;
        };

        // Act
        viewModel.UserName = "์ƒˆ์ด๋ฆ„";

        // Assert
        Assert.True(propertyChangedRaised);
        Assert.Equal("์ƒˆ์ด๋ฆ„", viewModel.UserName);
    }
}

๋ณด๋‹ค์‹œํ”ผ UI ์—†์ด๋„ ViewModel์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ MVVM์˜ ์ง„์ •ํ•œ ํž˜์ด์•ผ. ๐Ÿ’ช

์ปค๋งจ๋“œ ํ…Œ์ŠคํŠธ

์ปค๋งจ๋“œ์˜ ์‹คํ–‰๊ณผ CanExecute๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด:

[Fact]
public void SaveCommand_WhenUserNameIsEmpty_ShouldNotExecute()
{
    // Arrange
    var mockDataService = new Mock<IDataService>();
    var viewModel = new UserViewModel(mockDataService.Object);
    viewModel.UserName = "";

    // Act
    bool canExecute = viewModel.SaveCommand.CanExecute(null);

    // Assert
    Assert.False(canExecute);
}

[Fact]
public async Task SaveCommand_WhenExecuted_ShouldCallDataService()
{
    // Arrange
    var mockDataService = new Mock<IDataService>();
    var viewModel = new UserViewModel(mockDataService.Object);
    viewModel.UserName = "ํ™๊ธธ๋™";

    // Act
    viewModel.SaveCommand.Execute(null);
    await Task.Delay(100); // ๋น„๋™๊ธฐ ์ž‘์—… ๋Œ€๊ธฐ

    // Assert
    mockDataService.Verify(x => x.SaveUserAsync(It.IsAny<User>()), Times.Once);
}

๐Ÿ’ก ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํŒ

1๏ธโƒฃ ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ๋งŒ ๊ฒ€์ฆํ•ด์•ผ ํ•ด
2๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ ๋ช…ํ™•ํ•˜๊ฒŒ (๋ฌด์—‡์„_์–ธ์ œ_์–ด๋–ป๊ฒŒ)
3๏ธโƒฃ AAA ํŒจํ„ด ์‚ฌ์šฉ: Arrange(์ค€๋น„), Act(์‹คํ–‰), Assert(๊ฒ€์ฆ)
4๏ธโƒฃ Mock ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ•ด์„œ ์™ธ๋ถ€ ์˜์กด์„ฑ ์ œ๊ฑฐ
5๏ธโƒฃ ํ…Œ์ŠคํŠธ๋Š” ๋น ๋ฅด๊ณ  ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•ด! โšก

๐ŸŽญ ๋Œ€ํ™”์ƒ์ž์™€ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฒ˜๋ฆฌ

MVVM์—์„œ ๊ฐ€์žฅ ๊นŒ๋‹ค๋กœ์šด ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋Œ€ํ™”์ƒ์ž์™€ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฒ˜๋ฆฌ์•ผ. ViewModel์€ View๋ฅผ ๋ชฐ๋ผ์•ผ ํ•˜๋Š”๋ฐ, ์–ด๋–ป๊ฒŒ ๋Œ€ํ™”์ƒ์ž๋ฅผ ๋„์šธ๊นŒ? ๐Ÿค”

Dialog Service ํŒจํ„ด

์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ๋Œ€ํ™”์ƒ์ž๋ฅผ ์ถ”์ƒํ™”ํ•˜์ž:

public interface IDialogService
{
    void ShowMessage(string message, string title = "์•Œ๋ฆผ");
    bool ShowConfirmation(string message, string title = "ํ™•์ธ");
    string ShowOpenFileDialog(string filter = "๋ชจ๋“  ํŒŒ์ผ|*.*");
    string ShowSaveFileDialog(string filter = "๋ชจ๋“  ํŒŒ์ผ|*.*");
}

public class DialogService : IDialogService
{
    public void ShowMessage(string message, string title = "์•Œ๋ฆผ")
    {
        MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
    }

    public bool ShowConfirmation(string message, string title = "ํ™•์ธ")
    {
        var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
        return result == MessageBoxResult.Yes;
    }

    public string ShowOpenFileDialog(string filter = "๋ชจ๋“  ํŒŒ์ผ|*.*")
    {
        var dialog = new OpenFileDialog { Filter = filter };
        return dialog.ShowDialog() == true ? dialog.FileName : null;
    }

    public string ShowSaveFileDialog(string filter = "๋ชจ๋“  ํŒŒ์ผ|*.*")
    {
        var dialog = new SaveFileDialog { Filter = filter };
        return dialog.ShowDialog() == true ? dialog.FileName : null;
    }
}

ViewModel์—์„œ ์‚ฌ์šฉ:

public class MainViewModel : BaseViewModel
{
    private readonly IDialogService _dialogService;

    public MainViewModel(IDialogService dialogService)
    {
        _dialogService = dialogService;
    }

    private void DeleteUser()
    {
        bool confirmed = _dialogService.ShowConfirmation(
            "์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", 
            "์‚ญ์ œ ํ™•์ธ"
        );

        if (confirmed)
        {
            // ์‚ญ์ œ ๋กœ์ง
            _dialogService.ShowMessage("์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
        }
    }
}

์ด์ œ ViewModel์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ Mock DialogService๋ฅผ ์ฃผ์ž…ํ•˜๋ฉด ๋ผ! ๐ŸŽ‰

Navigation Service

ํ™”๋ฉด ์ „ํ™˜๋„ ์„œ๋น„์Šค๋กœ ์ถ”์ƒํ™”ํ•  ์ˆ˜ ์žˆ์–ด:

public interface INavigationService
{
    void NavigateTo<TViewModel>() where TViewModel : BaseViewModel;
    void NavigateTo<TViewModel>(object parameter) where TViewModel : BaseViewModel;
    void GoBack();
    bool CanGoBack { get; }
}

public class NavigationService : INavigationService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly Frame _frame;
    private readonly Stack<Type> _navigationStack;

    public NavigationService(IServiceProvider serviceProvider, Frame frame)
    {
        _serviceProvider = serviceProvider;
        _frame = frame;
        _navigationStack = new Stack<Type>();
    }

    public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
    {
        var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
        var viewType = GetViewTypeForViewModel(typeof(TViewModel));
        var view = (Page)Activator.CreateInstance(viewType);
        view.DataContext = viewModel;
        
        _navigationStack.Push(typeof(TViewModel));
        _frame.Navigate(view);
    }

    public void GoBack()
    {
        if (CanGoBack)
        {
            _navigationStack.Pop();
            _frame.GoBack();
        }
    }

    public bool CanGoBack => _navigationStack.Count > 1;

    private Type GetViewTypeForViewModel(Type viewModelType)
    {
        // ViewModel ์ด๋ฆ„์—์„œ View ์ด๋ฆ„ ์œ ์ถ”
        var viewName = viewModelType.Name.Replace("ViewModel", "View");
        var viewType = Type.GetType($"MyApp.Views.{viewName}");
        return viewType;
    }
}

์‚ฌ์šฉ ์˜ˆ์‹œ:

public class MainViewModel : BaseViewModel
{
    private readonly INavigationService _navigationService;

    public ICommand NavigateToUserCommand { get; }

    public MainViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;
        NavigateToUserCommand = new RelayCommand(_ => NavigateToUser());
    }

    private void NavigateToUser()
    {
        _navigationService.NavigateTo<UserViewModel>();
    }
}

โš ๏ธ ์‹ค๋ฌด ๊ณ ๋ ค์‚ฌํ•ญ

Navigation Service๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ๋‹ค์Œ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด:
- ๋’ค๋กœ ๊ฐ€๊ธฐ ์Šคํƒ ๊ด€๋ฆฌ
- ํŽ˜์ด์ง€ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
- ๋„ค๋น„๊ฒŒ์ด์…˜ ์ด๋ฒคํŠธ (์ง„์ž…/์ดํƒˆ)
- ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ (ํŽ˜์ด์ง€ ์บ์‹ฑ)

๋ณต์žกํ•œ ๋„ค๋น„๊ฒŒ์ด์…˜์ด ํ•„์š”ํ•˜๋‹ค๋ฉด Prism ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด๋ด! ๐Ÿš€

๐Ÿ“š MVVM ํ”„๋ ˆ์ž„์›Œํฌ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฑธ ์ง์ ‘ ๋งŒ๋“ค ํ•„์š”๋Š” ์—†์–ด! ๊ฒ€์ฆ๋œ MVVM ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด ๋งŽ์ด ์žˆ๊ฑฐ๋“ . ๊ฐ๊ฐ์˜ ํŠน์ง•์„ ์•Œ์•„๋ณด์ž. ๐Ÿ› ๏ธ

1๏ธโƒฃ Prism

๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” WPF MVVM ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. Microsoft Patterns & Practices ํŒ€์ด ๋งŒ๋“ค์—ˆ์–ด.

์ฃผ์š” ๊ธฐ๋Šฅ:
โœ… ๊ฐ•๋ ฅํ•œ DI ์ปจํ…Œ์ด๋„ˆ ํ†ตํ•ฉ
โœ… ๋ชจ๋“ˆํ™” ์‹œ์Šคํ…œ
โœ… ์ด๋ฒคํŠธ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํ„ฐ
โœ… ๋„ค๋น„๊ฒŒ์ด์…˜ ์„œ๋น„์Šค
โœ… ๋Œ€ํ™”์ƒ์ž ์„œ๋น„์Šค
โœ… ์ปค๋งจ๋“œ ๊ตฌํ˜„์ฒด ์ œ๊ณต

// Prism ์‚ฌ์šฉ ์˜ˆ์‹œ
public class MainViewModel : BindableBase
{
    private string _title;
    public string Title
    {
        get => _title;
        set => SetProperty(ref _title, value);
    }

    public DelegateCommand SaveCommand { get; }

    public MainViewModel()
    {
        SaveCommand = new DelegateCommand(ExecuteSave, CanExecuteSave)
            .ObservesProperty(() => Title);
    }

    private void ExecuteSave()
    {
        // ์ €์žฅ ๋กœ์ง
    }

    private bool CanExecuteSave()
    {
        return !string.IsNullOrEmpty(Title);
    }
}

์žฅ์ : ๊ธฐ๋Šฅ์ด ํ’๋ถ€ํ•˜๊ณ  ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉ
๋‹จ์ : ํ•™์Šต ๊ณก์„ ์ด ์žˆ๊ณ  ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ฌด๊ฑฐ์›Œ์งˆ ์ˆ˜ ์žˆ์–ด

2๏ธโƒฃ MVVM Light Toolkit

๊ฐ€๋ณ๊ณ  ๋ฐฐ์šฐ๊ธฐ ์‰ฌ์šด ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ์ตœ๊ทผ์—๋Š” ๊ฐœ๋ฐœ์ด ์ค‘๋‹จ๋˜์—ˆ์ง€๋งŒ ์—ฌ์ „ํžˆ ๋งŽ์ด ์‚ฌ์šฉ๋ผ.

์ฃผ์š” ๊ธฐ๋Šฅ:
โœ… ViewModelBase ํด๋ž˜์Šค
โœ… RelayCommand ๊ตฌํ˜„
โœ… Messenger (์ด๋ฒคํŠธ ๋ฒ„์Šค)
โœ… ๊ฐ„๋‹จํ•œ DI ์ปจํ…Œ์ด๋„ˆ
โœ… ๋””์ž์ธ ํƒ€์ž„ ๋ฐ์ดํ„ฐ ์ง€์›

// MVVM Light ์‚ฌ์šฉ ์˜ˆ์‹œ
public class MainViewModel : ViewModelBase
{
    private string _userName;
    public string UserName
    {
        get => _userName;
        set => Set(ref _userName, value);
    }

    public RelayCommand SaveCommand { get; }

    public MainViewModel()
    {
        SaveCommand = new RelayCommand(Save, CanSave);
    }
}

์žฅ์ : ๊ฐ€๋ณ๊ณ  ๋ฐฐ์šฐ๊ธฐ ์‰ฌ์›Œ
๋‹จ์ : ๋” ์ด์ƒ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Œ

3๏ธโƒฃ CommunityToolkit.Mvvm (MVVM Toolkit)

Microsoft๊ฐ€ ๊ณต์‹์ ์œผ๋กœ ์ง€์›ํ•˜๋Š” ์ตœ์‹  MVVM ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ! MVVM Light์˜ ์ •์‹ ์  ํ›„๊ณ„์ž๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ์–ด. ๐ŸŒŸ

์ฃผ์š” ๊ธฐ๋Šฅ:
โœ… Source Generator ํ™œ์šฉ (์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ)
โœ… ObservableObject ๊ธฐ๋ณธ ํด๋ž˜์Šค
โœ… RelayCommand์™€ AsyncRelayCommand
โœ… Messenger (๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ)
โœ… IoC ์ปจํ…Œ์ด๋„ˆ ์ง€์›
โœ… .NET 6+ ์ตœ์ ํ™”

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

// Source Generator๊ฐ€ ์ž๋™์œผ๋กœ ์ฝ”๋“œ ์ƒ์„ฑ!
public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _userName;

    [ObservableProperty]
    private int _age;

    [RelayCommand]
    private void Save()
    {
        // ์ €์žฅ ๋กœ์ง
    }

    [RelayCommand(CanExecute = nameof(CanDelete))]
    private void Delete()
    {
        // ์‚ญ์ œ ๋กœ์ง
    }

    private bool CanDelete() => Age >= 18;
}

์œ„ ์ฝ”๋“œ์—์„œ [ObservableProperty] ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ๋ถ™์ด๋ฉด, Source Generator๊ฐ€ ์ž๋™์œผ๋กœ ์ „์ฒด ์†์„ฑ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด์ค˜! ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ํ™• ์ค„์–ด๋“ค์ง€? ๐ŸŽ‰

์žฅ์ : ์ตœ์‹  ๊ธฐ์ˆ , ์ ์€ ์ฝ”๋“œ, Microsoft ๊ณต์‹ ์ง€์›
๋‹จ์ : .NET 6 ์ด์ƒ ํ•„์š”

4๏ธโƒฃ Caliburn.Micro

์ปจ๋ฒค์…˜ ๊ธฐ๋ฐ˜์˜ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋ช…๋ช… ๊ทœ์น™๋งŒ ๋”ฐ๋ฅด๋ฉด ๋งŽ์€ ๊ฑธ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜.

์ฃผ์š” ๊ธฐ๋Šฅ:
โœ… ์ปจ๋ฒค์…˜ ๊ธฐ๋ฐ˜ ๋ฐ”์ธ๋”ฉ
โœ… ํ™”๋ฉด ๊ด€๋ฆฌ
โœ… ์ด๋ฒคํŠธ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํ„ฐ
โœ… ์ฝ”๋ฃจํ‹ด ์ง€์›
โœ… ๋ถ€ํŠธ์ŠคํŠธ๋ž˜ํผ

// Caliburn.Micro๋Š” ๋ช…๋ช… ๊ทœ์น™์œผ๋กœ ์ž๋™ ๋ฐ”์ธ๋”ฉ
public class ShellViewModel : Screen
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            NotifyOfPropertyChange(() => Name);
            NotifyOfPropertyChange(() => CanSayHello);
        }
    }

    // ๋ฒ„ํŠผ ์ด๋ฆ„์ด "SayHello"๋ฉด ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ!
    public void SayHello()
    {
        MessageBox.Show($"Hello {Name}!");
    }

    // Can + ๋ฉ”์„œ๋“œ๋ช… = ํ™œ์„ฑํ™” ์กฐ๊ฑด
    public bool CanSayHello => !string.IsNullOrEmpty(Name);
}

์žฅ์ : ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ง๊ด€์ 
๋‹จ์ : ์ปจ๋ฒค์…˜์„ ๋ฐฐ์›Œ์•ผ ํ•˜๊ณ , ๋””๋ฒ„๊น…์ด ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด

๐Ÿ’ก ์–ด๋–ค ๊ฑธ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ?

์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: CommunityToolkit.Mvvm (๊ฐ€๋ณ๊ณ  ํ˜„๋Œ€์ )
์ค‘๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: Prism (๊ธฐ๋Šฅ์ด ํ’๋ถ€)
๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ž…: Caliburn.Micro (์ปจ๋ฒค์…˜ ๊ธฐ๋ฐ˜)
ํ•™์Šต ๋ชฉ์ : ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ! (์›๋ฆฌ ์ดํ•ด์— ์ตœ๊ณ ) ๐Ÿ“š

๊ฐœ์ธ์ ์œผ๋กœ๋Š” CommunityToolkit.Mvvm์„ ์ถ”์ฒœํ•ด! Microsoft๊ฐ€ ๊ณต์‹ ์ง€์›ํ•˜๊ณ , Source Generator๋กœ ์ƒ์‚ฐ์„ฑ์ด ์ •๋ง ์ข‹๊ฑฐ๋“ . ๐Ÿ˜Š

๐ŸŽจ ๊ณ ๊ธ‰ XAML ๋ฐ”์ธ๋”ฉ ํ…Œํฌ๋‹‰

XAML ๋ฐ”์ธ๋”ฉ์„ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰๋“ค์„ ์•Œ์•„๋ณด์ž. ๐Ÿš€

MultiBinding (๋‹ค์ค‘ ๋ฐ”์ธ๋”ฉ)

์—ฌ๋Ÿฌ ์†์„ฑ์„ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์–ด:

<!-- Converter ์ •์˜ -->
public class FullNameConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && values[0] is string firstName && values[1] is string lastName)
        {
            return $"{firstName} {lastName}";
        }
        return string.Empty;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

<!-- XAML์—์„œ ์‚ฌ์šฉ -->
<Window.Resources>
    <local:FullNameConverter x:Key="FullNameConverter" />
</Window.Resources>

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource FullNameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด FirstName๊ณผ LastName์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ „์ฒด ์ด๋ฆ„์ด ์—…๋ฐ์ดํŠธ๋ผ! โœจ

StringFormat (๋ฌธ์ž์—ด ํฌ๋งท)

๊ฐ„๋‹จํ•œ ํฌ๋งทํŒ…์€ Converter ์—†์ด๋„ ๊ฐ€๋Šฅํ•ด:

<!-- ์ˆซ์ž ํฌ๋งท -->
<TextBlock Text="{Binding Price, StringFormat=๊ฐ€๊ฒฉ: {0:C}}" />
<!-- ์ถœ๋ ฅ: ๊ฐ€๊ฒฉ: โ‚ฉ10,000 -->

<!-- ๋‚ ์งœ ํฌ๋งท -->
<TextBlock Text="{Binding BirthDate, StringFormat=์ƒ๋…„์›”์ผ: {0:yyyy-MM-dd}}" />
<!-- ์ถœ๋ ฅ: ์ƒ๋…„์›”์ผ: 1990-01-01 -->

<!-- ํผ์„ผํŠธ -->
<TextBlock Text="{Binding Progress, StringFormat={}{0:P0}}" />
<!-- ์ถœ๋ ฅ: 75% -->

<!-- ์—ฌ๋Ÿฌ ๊ฐ’ ์กฐํ•ฉ -->
<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{}{0}๋‹˜์˜ ๋‚˜์ด๋Š” {1}์„ธ์ž…๋‹ˆ๋‹ค.">
            <Binding Path="Name" />
            <Binding Path="Age" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

StringFormat์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•œ ํฌ๋งทํŒ…์„ ์œ„ํ•ด Converter๋ฅผ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์—†์–ด! ํŽธํ•˜์ง€? ๐Ÿ˜Š

FallbackValue์™€ TargetNullValue

๋ฐ”์ธ๋”ฉ์ด ์‹คํŒจํ•˜๊ฑฐ๋‚˜ null์ผ ๋•Œ ํ‘œ์‹œํ•  ๊ฐ’์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์–ด:

<!-- ๋ฐ”์ธ๋”ฉ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’ -->
<TextBlock Text="{Binding UserName, FallbackValue=์•Œ ์ˆ˜ ์—†์Œ}" />

<!-- null์ผ ๋•Œ ํ‘œ์‹œํ•  ๊ฐ’ -->
<TextBlock Text="{Binding Description, TargetNullValue=์„ค๋ช… ์—†์Œ}" />

<!-- ๋‘˜ ๋‹ค ์‚ฌ์šฉ -->
<TextBlock Text="{Binding Email, 
                         TargetNullValue=์ด๋ฉ”์ผ ์—†์Œ,
                         FallbackValue=์˜ค๋ฅ˜}" />

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด null ์ฒดํฌ ์ฝ”๋“œ๋ฅผ ViewModel์— ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์–ด! ๐ŸŽฏ

RelativeSource (์ƒ๋Œ€ ์†Œ์Šค)

ํ˜„์žฌ ์š”์†Œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ƒ๋Œ€์ ์ธ ์œ„์น˜์˜ ์š”์†Œ๋ฅผ ๋ฐ”์ธ๋”ฉํ•  ์ˆ˜ ์žˆ์–ด:

<!-- ์ž๊ธฐ ์ž์‹ ์˜ ๋‹ค๋ฅธ ์†์„ฑ ๋ฐ”์ธ๋”ฉ -->
<TextBox x:Name="myTextBox" 
         Text="Hello"
         ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}" />

<!-- ๋ถ€๋ชจ ์š”์†Œ ๋ฐ”์ธ๋”ฉ -->
<Grid x:Name="parentGrid" Tag="ParentData">
    <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}" />
</Grid>

<!-- DataTemplate ์•ˆ์—์„œ ๋ถ€๋ชจ ViewModel ์ ‘๊ทผ -->
<ListBox ItemsSource="{Binding Users}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Button Content="์‚ญ์ œ"
                    Command="{Binding DataContext.DeleteCommand, 
                                    RelativeSource={RelativeSource AncestorType=ListBox}}"
                    CommandParameter="{Binding}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

RelativeSource๋Š” ํŠนํžˆ DataTemplate ์•ˆ์—์„œ ๋ถ€๋ชจ ViewModel์˜ ์ปค๋งจ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์œ ์šฉํ•ด! ๐Ÿ’ก

ValidationRules (์œ ํšจ์„ฑ ๊ฒ€์‚ฌ)

XAML์—์„œ ์ง์ ‘ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด:

// ์ปค์Šคํ…€ ValidationRule
public class AgeValidationRule : ValidationRule
{
    public int MinAge { get; set; } = 0;
    public int MaxAge { get; set; } = 150;

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (int.TryParse(value?.ToString(), out int age))
        {
            if (age < MinAge || age > MaxAge)
            {
                return new ValidationResult(false, 
                    $"๋‚˜์ด๋Š” {MinAge}์—์„œ {MaxAge} ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
            }
            return ValidationResult.ValidResult;
        }
        return new ValidationResult(false, "์˜ฌ๋ฐ”๋ฅธ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.");
    }
}

// XAML์—์„œ ์‚ฌ์šฉ
<TextBox>
    <TextBox.Text>
        <Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:AgeValidationRule MinAge="18" MaxAge="100" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <TextBox.Style>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" 
                            Value="{Binding RelativeSource={RelativeSource Self},
                                          Path=(Validation.Errors)[0].ErrorContent}" />
                    <Setter Property="BorderBrush" Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ๋นจ๊ฐ„ ํ…Œ๋‘๋ฆฌ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  ํˆดํŒ์— ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜ํƒ€๋‚˜! ๐Ÿ”ด

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

MVVM ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž. ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ํŠนํžˆ ์ค‘์š”ํ•ด! ๐Ÿš€

๊ฐ€์ƒํ™” (Virtualization)

์ˆ˜์ฒœ, ์ˆ˜๋งŒ ๊ฐœ์˜ ํ•ญ๋ชฉ์„ ํ‘œ์‹œํ•  ๋•Œ๋Š” ๊ฐ€์ƒํ™”๊ฐ€ ํ•„์ˆ˜์•ผ:

<!-- ListBox ๊ฐ€์ƒํ™” -->
<ListBox ItemsSource="{Binding LargeDataSet}"
         VirtualizingPanel.IsVirtualizing="True"
         VirtualizingPanel.VirtualizationMode="Recycling"
         VirtualizingPanel.CacheLength="20,20"
         VirtualizingPanel.CacheLengthUnit="Item">
    <!-- ItemTemplate -->
</ListBox>

<!-- DataGrid ๊ฐ€์ƒํ™” -->
<DataGrid ItemsSource="{Binding Users}"
          EnableRowVirtualization="True"
          EnableColumnVirtualization="True"
          VirtualizingPanel.IsVirtualizing="True"
          VirtualizingPanel.VirtualizationMode="Recycling" />

๊ฐ€์ƒํ™”๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ ๋ Œ๋”๋งํ•ด์„œ ๋ฉ”๋ชจ๋ฆฌ์™€ ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ๊ฐœ์„ ๋ผ! 10,000๊ฐœ ํ•ญ๋ชฉ๋„ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŽข

๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ

๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฐ˜๋“œ์‹œ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด:

public class MainViewModel : BaseViewModel
{
    private ObservableCollection<User> _users;
    public ObservableCollection<User> Users
    {
        get => _users;
        set => SetProperty(ref _users, value);
    }

    private bool _isLoading;
    public bool IsLoading
    {
        get => _isLoading;
        set => SetProperty(ref _isLoading, value);
    }

    public ICommand LoadDataCommand { get; }

    public MainViewModel(IDataService dataService)
    {
        LoadDataCommand = new AsyncRelayCommand(async () => await LoadDataAsync());
    }

    private async Task LoadDataAsync()
    {
        IsLoading = true;
        try
        {
            var users = await _dataService.GetUsersAsync();
            
            // UI ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                Users = new ObservableCollection<User>(users);
            });
        }
        finally
        {
            IsLoading = false;
        }
    }
}

XAML์—์„œ ๋กœ๋”ฉ ํ‘œ์‹œ:

<Grid>
    <ListBox ItemsSource="{Binding Users}" 
             Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityInverse}}" />
    
    <ProgressBar IsIndeterminate="True"
                 Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibility}}" />
</Grid>

PropertyChanged ์ด๋ฒคํŠธ ์ตœ์ ํ™”

๋ถˆํ•„์š”ํ•œ PropertyChanged ์ด๋ฒคํŠธ๋ฅผ ์ค„์ด์ž:

// โŒ ๋‚˜์œ ์˜ˆ: ๋งค๋ฒˆ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
private string _name;
public string Name
{
    get => _name;
    set
    {
        _name = value;
        OnPropertyChanged(); // ๊ฐ’์ด ๊ฐ™์•„๋„ ๋ฐœ์ƒ!
    }
}

// โœ… ์ข‹์€ ์˜ˆ: ๊ฐ’์ด ์‹ค์ œ๋กœ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ
private string _name;
public string Name
{
    get => _name;
    set => SetProperty(ref _name, value); // ๊ฐ’์ด ๋‹ค๋ฅผ ๋•Œ๋งŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
}

// โœ… ๋” ์ข‹์€ ์˜ˆ: ์—ฌ๋Ÿฌ ์†์„ฑ์„ ํ•œ ๋ฒˆ์— ์—…๋ฐ์ดํŠธ
public void UpdateUser(string name, int age, string email)
{
    // ์ด๋ฒคํŠธ๋ฅผ ์ผ์‹œ์ ์œผ๋กœ ์ค‘๋‹จ
    _suspendNotifications = true;
    
    Name = name;
    Age = age;
    Email = email;
    
    _suspendNotifications = false;
    
    // ํ•œ ๋ฒˆ์— ์•Œ๋ฆผ
    OnPropertyChanged(nameof(Name));
    OnPropertyChanged(nameof(Age));
    OnPropertyChanged(nameof(Email));
}

์ปฌ๋ ‰์…˜ ์—…๋ฐ์ดํŠธ ์ตœ์ ํ™”

๋Œ€๋Ÿ‰์˜ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€/์‚ญ์ œํ•  ๋•Œ๋Š” ์ผ๊ด„ ์ฒ˜๋ฆฌํ•˜์ž:

// โŒ ๋‚˜์œ ์˜ˆ: ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ (๋งค๋ฒˆ UI ์—…๋ฐ์ดํŠธ)
foreach (var user in newUsers)
{
    Users.Add(user); // 1000๋ฒˆ UI ์—…๋ฐ์ดํŠธ!
}

// โœ… ์ข‹์€ ์˜ˆ: ์ผ๊ด„ ์ถ”๊ฐ€
public class RangeObservableCollection<T> : ObservableCollection<T>
{
    public void AddRange(IEnumerable<T> items)
    {
        CheckReentrancy();
        
        foreach (var item in items)
        {
            Items.Add(item);
        }
        
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Reset));
    }

    public void RemoveRange(IEnumerable<T> items)
    {
        CheckReentrancy();
        
        foreach (var item in items)
        {
            Items.Remove(item);
        }
        
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Reset));
    }
}

// ์‚ฌ์šฉ
Users.AddRange(newUsers); // ํ•œ ๋ฒˆ๋งŒ UI ์—…๋ฐ์ดํŠธ!

๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ ์•ฝํ•œ ์ฐธ์กฐ๋ฅผ ์ฃผ์˜ํ•ด์•ผ ํ•ด:

// โŒ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜
public class MainViewModel : BaseViewModel
{
    public MainViewModel()
    {
        // ๊ฐ•ํ•œ ์ฐธ์กฐ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜!
        SomeStaticEvent.EventOccurred += OnEventOccurred;
    }

    private void OnEventOccurred(object sender, EventArgs e)
    {
        // ...
    }
}

// โœ… ์ด๋ฒคํŠธ ํ•ด์ œ
public class MainViewModel : BaseViewModel, IDisposable
{
    public MainViewModel()
    {
        SomeStaticEvent.EventOccurred += OnEventOccurred;
    }

    public void Dispose()
    {
        SomeStaticEvent.EventOccurred -= OnEventOccurred;
    }
}

// โœ… ์•ฝํ•œ ์ด๋ฒคํŠธ ํŒจํ„ด ์‚ฌ์šฉ
public class MainViewModel : BaseViewModel
{
    public MainViewModel()
    {
        WeakEventManager<SomeClass, EventArgs>
            .AddHandler(someInstance, nameof(SomeClass.EventOccurred), OnEventOccurred);
    }
}

โš ๏ธ ์„ฑ๋Šฅ ์ธก์ •ํ•˜๊ธฐ

์ตœ์ ํ™”ํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ํ”„๋กœํŒŒ์ผ๋ง์„ ํ•ด์•ผ ํ•ด! ์ถ”์ธก์œผ๋กœ ์ตœ์ ํ™”ํ•˜๋ฉด ์•ˆ ๋ผ.

Visual Studio์˜ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ:
- CPU ์‚ฌ์šฉ๋Ÿ‰
- ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น
- UI ์‘๋‹ต์„ฑ

์„ ์ธก์ •ํ•˜๊ณ , ์‹ค์ œ ๋ณ‘๋ชฉ ์ง€์ ์„ ์ฐพ์•„์„œ ์ตœ์ ํ™”ํ•˜์ž! ๐Ÿ“Š

๐ŸŽฏ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์˜ˆ์ œ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๊ฐ„๋‹จํ•œ ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ์„ MVVM ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐Ÿ“

1๋‹จ๊ณ„: Model ์ •์˜

// Models/TodoItem.cs
public class TodoItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime CreatedDate { get; set; }
    public DateTime? DueDate { get; set; }
    public Priority Priority { get; set; }
}

public enum Priority
{
    Low,
    Normal,
    High,
    Urgent
}

// Services/ITodoService.cs
public interface ITodoService
{
    Task<List<TodoItem>> GetAllTodosAsync();
    Task<TodoItem> GetTodoByIdAsync(int id);
    Task AddTodoAsync(TodoItem todo);
    Task UpdateTodoAsync(TodoItem todo);
    Task DeleteTodoAsync(int id);
}

// Services/TodoService.cs
public class TodoService : ITodoService
{
    private readonly List<TodoItem> _todos = new();
    private int _nextId = 1;

    public Task<List<TodoItem>> GetAllTodosAsync()
    {
        return Task.FromResult(_todos.ToList());
    }

    public Task<TodoItem> GetTodoByIdAsync(int id)
    {
        var todo = _todos.FirstOrDefault(t => t.Id == id);
        return Task.FromResult(todo);
    }

    public Task AddTodoAsync(TodoItem todo)
    {
        todo.Id = _nextId++;
        todo.CreatedDate = DateTime.Now;
        _todos.Add(todo);
        return Task.CompletedTask;
    }

    public Task UpdateTodoAsync(TodoItem todo)
    {
        var existing = _todos.FirstOrDefault(t => t.Id == todo.Id);
        if (existing != null)
        {
            var index = _todos.IndexOf(existing);
            _todos[index] = todo;
        }
        return Task.CompletedTask;
    }

    public Task DeleteTodoAsync(int id)
    {
        var todo = _todos.FirstOrDefault(t => t.Id == id);
        if (todo != null)
        {
            _todos.Remove(todo);
        }
        return Task.CompletedTask;
    }
}

2๋‹จ๊ณ„: ViewModel ๊ตฌํ˜„

// ViewModels/TodoItemViewModel.cs
public class TodoItemViewModel : BaseViewModel
{
    private readonly TodoItem _model;

    public TodoItemViewModel(TodoItem model)
    {
        _model = model;
    }

    public int Id => _model.Id;

    public string Title
    {
        get => _model.Title;
        set
        {
            if (_model.Title != value)
            {
                _model.Title = value;
                OnPropertyChanged();
            }
        }
    }

    public bool IsCompleted
    {
        get => _model.IsCompleted;
        set
        {
            if (_model.IsCompleted != value)
            {
                _model.IsCompleted = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(StatusText));
            }
        }
    }

    public string StatusText => IsCompleted ? "์™„๋ฃŒ" : "์ง„ํ–‰์ค‘";

    public TodoItem GetModel() => _model;
}

// ViewModels/MainViewModel.cs
public class MainViewModel : BaseViewModel
{
    private readonly ITodoService _todoService;
    private readonly IDialogService _dialogService;

    public ObservableCollection<TodoItemViewModel> Todos { get; }

    private TodoItemViewModel _selectedTodo;
    public TodoItemViewModel SelectedTodo
    {
        get => _selectedTodo;
        set => SetProperty(ref _selectedTodo, value);
    }

    private string _newTodoTitle;
    public string NewTodoTitle
    {
        get => _newTodoTitle;
        set => SetProperty(ref _newTodoTitle, value);
    }

    public ICommand LoadTodosCommand { get; }
    public ICommand AddTodoCommand { get; }
    public ICommand DeleteTodoCommand { get; }
    public ICommand ToggleCompleteCommand { get; }

    public MainViewModel(ITodoService todoService, IDialogService dialogService)
    {
        _todoService = todoService;
        _dialogService = dialogService;
        
        Todos = new ObservableCollection<TodoItemViewModel>();

        LoadTodosCommand = new AsyncRelayCommand(LoadTodosAsync);
        AddTodoCommand = new AsyncRelayCommand(AddTodoAsync, CanAddTodo);
        DeleteTodoCommand = new AsyncRelayCommand<TodoItemViewModel>(DeleteTodoAsync);
        ToggleCompleteCommand = new AsyncRelayCommand<TodoItemViewModel>(ToggleCompleteAsync);

        // ์ดˆ๊ธฐ ๋กœ๋“œ
        _ = LoadTodosAsync();
    }

    private async Task LoadTodosAsync()
    {
        var todos = await _todoService.GetAllTodosAsync();
        
        Todos.Clear();
        foreach (var todo in todos)
        {
            Todos.Add(new TodoItemViewModel(todo));
        }
    }

    private bool CanAddTodo()
    {
        return !string.IsNullOrWhiteSpace(NewTodoTitle);
    }

    private async Task AddTodoAsync()
    {
        var newTodo = new TodoItem
        {
            Title = NewTodoTitle,
            IsCompleted = false,
            Priority = Priority.Normal
        };

        await _todoService.AddTodoAsync(newTodo);
        Todos.Add(new TodoItemViewModel(newTodo));

        NewTodoTitle = string.Empty;
        _dialogService.ShowMessage("ํ•  ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
    }

    private async Task DeleteTodoAsync(TodoItemViewModel todoVm)
    {
        if (todoVm == null) return;

        bool confirmed = _dialogService.ShowConfirmation(
            "์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?",
            "์‚ญ์ œ ํ™•์ธ"
        );

        if (confirmed)
        {
            await _todoService.DeleteTodoAsync(todoVm.Id);
            Todos.Remove(todoVm);
        }
    }

    private async Task ToggleCompleteAsync(TodoItemViewModel todoVm)
    {
        if (todoVm == null) return;

        todoVm.IsCompleted = !todoVm.IsCompleted;
        await _todoService.UpdateTodoAsync(todoVm.GetModel());
    }
}

3๋‹จ๊ณ„: View (XAML) ๊ตฌํ˜„

<Window x:Class="TodoApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ํ•  ์ผ ๊ด€๋ฆฌ" Height="600" Width="800">
    
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />
    </Window.Resources>

    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- ์ œ๋ชฉ -->
        <TextBlock Grid.Row="0" 
                   Text="๐Ÿ“ ํ•  ์ผ ๊ด€๋ฆฌ"
                   FontSize="28"
                   FontWeight="Bold"
                   Margin="0,0,0,20" />

        <!-- ์ƒˆ ํ•  ์ผ ์ถ”๊ฐ€ -->
        <Grid Grid.Row="1" Margin="0,0,0,20">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBox Grid.Column="0"
                     Text="{Binding NewTodoTitle, UpdateSourceTrigger=PropertyChanged}"
                     FontSize="16"
                     Padding="10"
                     VerticalContentAlignment="Center">
                <TextBox.InputBindings>
                    <KeyBinding Key="Enter" Command="{Binding AddTodoCommand}" />
                </TextBox.InputBindings>
            </TextBox>

            <Button Grid.Column="1"
                    Content="โž• ์ถ”๊ฐ€"
                    Command="{Binding AddTodoCommand}"
                    Margin="10,0,0,0"
                    Padding="20,10"
                    FontSize="16"
                    Background="#667eea"
                    Foreground="White"
                    BorderThickness="0"
                    Cursor="Hand" />
        </Grid>

        <!-- ํ•  ์ผ ๋ชฉ๋ก -->
        <ListBox Grid.Row="2"
                 ItemsSource="{Binding Todos}"
                 SelectedItem="{Binding SelectedTodo}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="#e9ecef"
                            BorderThickness="1"
                            CornerRadius="8"
                            Padding="15"
                            Margin="0,5"
                            Background="White">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <!-- ์™„๋ฃŒ ์ฒดํฌ๋ฐ•์Šค -->
                            <CheckBox Grid.Column="0"
                                      IsChecked="{Binding IsCompleted}"
                                      Command="{Binding DataContext.ToggleCompleteCommand,
                                                      RelativeSource={RelativeSource AncestorType=ListBox}}"
                                      CommandParameter="{Binding}"
                                      VerticalAlignment="Center"
                                      Margin="0,0,15,0" />

                            <!-- ์ œ๋ชฉ๊ณผ ์ƒํƒœ -->
                            <StackPanel Grid.Column="1" VerticalAlignment="Center">
                                <TextBlock Text="{Binding Title}"
                                           FontSize="16"
                                           FontWeight="SemiBold">
                                    <TextBlock.Style>
                                        <Style TargetType="TextBlock">
                                            <Style.Triggers>
                                                <DataTrigger Binding="{Binding IsCompleted}" Value="True">
                                                    <Setter Property="TextDecorations" Value="Strikethrough" />
                                                    <Setter Property="Foreground" Value="#adb5bd" />
                                                </DataTrigger>
                                            </Style.Triggers>
                                        </Style>
                                    </TextBlock.Style>
                                </TextBlock>
                                
                                <TextBlock Text="{Binding StatusText}"
                                           FontSize="12"
                                           Foreground="#6c757d"
                                           Margin="0,5,0,0" />
                            </StackPanel>

                            <!-- ์‚ญ์ œ ๋ฒ„ํŠผ -->
                            <Button Grid.Column="2"
                                    Content="๐Ÿ—‘๏ธ"
                                    Command="{Binding DataContext.DeleteTodoCommand,
                                                    RelativeSource={RelativeSource AncestorType=ListBox}}"
                                    CommandParameter="{Binding}"
                                    Background="Transparent"
                                    BorderThickness="0"
                                    FontSize="18"
                                    Cursor="Hand"
                                    Padding="10" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

4๋‹จ๊ณ„: ์˜์กด์„ฑ ์ฃผ์ž… ์„ค์ •

// App.xaml.cs
public partial class App : Application
{
    private ServiceProvider _serviceProvider;

    public App()
    {
        var services = new ServiceCollection();
        ConfigureServices(services);
        _serviceProvider = services.BuildServiceProvider();
    }

    private void ConfigureServices(IServiceCollection services)
    {
        // Services
        services.AddSingleton<ITodoService, TodoService>();
        services.AddSingleton<IDialogService, DialogService>();

        // ViewModels
        services.AddTransient<MainViewModel>();

        // Views
        services.AddTransient<MainWindow>();
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    protected override void OnExit(ExitEventArgs e)
    {
        _serviceProvider?.Dispose();
        base.OnExit(e);
    }
}

๐Ÿ’ก ์ด ์˜ˆ์ œ์—์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค

โœ… Model-View-ViewModel ๋ถ„๋ฆฌ
โœ… ์˜์กด์„ฑ ์ฃผ์ž… ํ™œ์šฉ
โœ… ๋น„๋™๊ธฐ ์ปค๋งจ๋“œ ์ฒ˜๋ฆฌ
โœ… ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ (์–‘๋ฐฉํ–ฅ, ์ปค๋งจ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ)
โœ… RelativeSource๋ฅผ ํ†ตํ•œ ๋ถ€๋ชจ ViewModel ์ ‘๊ทผ
โœ… DataTrigger๋ฅผ ํ™œ์šฉํ•œ ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง
โœ… ObservableCollection ์‚ฌ์šฉ

์ด ํŒจํ„ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋” ๋ณต์žกํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

๐ŸŽ“ MVVM ํ•™์Šต ๋กœ๋“œ๋งต๊ณผ ๋ฆฌ์†Œ์Šค

MVVM์„ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ ์œ„ํ•œ ํ•™์Šต ๊ฒฝ๋กœ์™€ ์œ ์šฉํ•œ ๋ฆฌ์†Œ์Šค๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด. ๋‹จ๊ณ„๋ณ„๋กœ ์ฐจ๊ทผ์ฐจ๊ทผ ํ•™์Šตํ•˜๋ฉด ๋ผ! ๐Ÿ“š

ํ•™์Šต ๋‹จ๊ณ„

๐ŸŒฑ ์ดˆ๊ธ‰ (1-2๊ฐœ์›”)
1๏ธโƒฃ C# ๊ธฐ์ดˆ ๋ฌธ๋ฒ• ์™„๋ฒฝํžˆ ์ดํ•ดํ•˜๊ธฐ
2๏ธโƒฃ WPF ๊ธฐ๋ณธ ๊ฐœ๋… (XAML, ๋ ˆ์ด์•„์›ƒ, ์ปจํŠธ๋กค)
3๏ธโƒฃ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๊ธฐ์ดˆ
4๏ธโƒฃ INotifyPropertyChanged ์ดํ•ดํ•˜๊ณ  ๊ตฌํ˜„
5๏ธโƒฃ ๊ฐ„๋‹จํ•œ ์ปค๋งจ๋“œ ํŒจํ„ด ๊ตฌํ˜„
6๏ธโƒฃ ๊ธฐ๋ณธ์ ์ธ MVVM ๊ตฌ์กฐ๋กœ ๊ฐ„๋‹จํ•œ ์•ฑ ๋งŒ๋“ค๊ธฐ

๐ŸŒฟ ์ค‘๊ธ‰ (2-3๊ฐœ์›”)
1๏ธโƒฃ ์˜์กด์„ฑ ์ฃผ์ž… ์ดํ•ด์™€ ์ ์šฉ
2๏ธโƒฃ ์„œ๋น„์Šค ํŒจํ„ด (Dialog, Navigation)
3๏ธโƒฃ ๊ณ ๊ธ‰ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ (MultiBinding, Converter)
4๏ธโƒฃ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ (async/await)
5๏ธโƒฃ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
6๏ธโƒฃ MVVM ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ (CommunityToolkit.Mvvm)
7๏ธโƒฃ ์ค‘๊ฐ„ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ ์™„์„ฑ

๐ŸŒณ ๊ณ ๊ธ‰ (3-6๊ฐœ์›”)
1๏ธโƒฃ ๋Œ€๊ทœ๋ชจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„
2๏ธโƒฃ ๋ชจ๋“ˆํ™”์™€ ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ
3๏ธโƒฃ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ•
4๏ธโƒฃ ๊ณ ๊ธ‰ XAML ํ…Œํฌ๋‹‰
5๏ธโƒฃ ๋ฉ”์‹œ์ง• ํŒจํ„ด๊ณผ ์ด๋ฒคํŠธ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํ„ฐ
6๏ธโƒฃ ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜
7๏ธโƒฃ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ ๊ธฐ์—ฌ

์ถ”์ฒœ ํ•™์Šต ๋ฆฌ์†Œ์Šค

๐Ÿ“– ๊ณต์‹ ๋ฌธ์„œ
- Microsoft Learn WPF ํŠœํ† ๋ฆฌ์–ผ
- .NET API ๋ธŒ๋ผ์šฐ์ €
- CommunityToolkit.Mvvm ๋ฌธ์„œ

๐ŸŽฅ ์˜จ๋ผ์ธ ๊ฐ•์˜
- Pluralsight: WPF MVVM ์‹œ๋ฆฌ์ฆˆ
- Udemy: Complete WPF & MVVM
- YouTube: IAmTimCorey, SingletonSean

๐Ÿ“š ์ถ”์ฒœ ๋„์„œ
- "Pro WPF in C#" by Matthew MacDonald
- "WPF 4.5 Unleashed" by Adam Nathan
- "MVVM Survival Guide" by Muhammad Shujaat Siddiqi

๐Ÿ’ป ์‹ค์Šต ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด
- ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ (TodoList)
- ๋ฉ”๋ชจ์žฅ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
- ์—ฐ๋ฝ์ฒ˜ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
- ๊ฐ„๋‹จํ•œ POS ์‹œ์Šคํ…œ
- ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ
- ์Œ์•… ํ”Œ๋ ˆ์ด์–ด
- ์ฑ„ํŒ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ๋„์›€ ๋ฐ›๊ธฐ

๐ŸŒ ์˜จ๋ผ์ธ ์ปค๋ฎค๋‹ˆํ‹ฐ
- Stack Overflow (wpf, mvvm ํƒœ๊ทธ)
- Reddit: r/csharp, r/dotnet
- GitHub Discussions
- Discord: C# ์„œ๋ฒ„๋“ค

๐Ÿค ํ•œ๊ตญ ์ปค๋ฎค๋‹ˆํ‹ฐ
- OKKY
- ์ƒํ™œ์ฝ”๋”ฉ
- ์ธํ”„๋Ÿฐ ์งˆ๋ฌธ ๊ฒŒ์‹œํŒ
- ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ฉ˜ํ†  ์ฐพ๊ธฐ

ํ˜น์‹œ WPF MVVM ๊ฐœ๋ฐœ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ์€๋ฐ ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๊ฒฝํ—˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ์‹ค๋ฌด ๊ฒฝํ—˜์„ ๊ฐ€์ง„ ์„ ๋ฐฐ ๊ฐœ๋ฐœ์ž๋“ค์ด 1:1๋กœ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ํ•ด์ฃผ๊ณ , ํ”„๋กœ์ ํŠธ ์„ค๊ณ„๋„ ๋„์™€์ค„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐ŸŽฏ

MVVM ํ•™์Šต ๋กœ๋“œ๋งต ๐ŸŒฑ ์ดˆ๊ธ‰ ๊ธฐ๋ณธ ๊ฐœ๋… 1-2๊ฐœ์›” ๐ŸŒฟ ์ค‘๊ธ‰ ์‹ค๋ฌด ํŒจํ„ด 2-3๊ฐœ์›” ๐ŸŒณ ๊ณ ๊ธ‰ ์•„ํ‚คํ…์ฒ˜ 3-6๊ฐœ์›” ์ดˆ๊ธ‰ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ“ C# ๊ธฐ์ดˆ โœ“ XAML ์ดํ•ด โœ“ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ โœ“ ๊ฐ„๋‹จํ•œ ์•ฑ ์ œ์ž‘ ์ค‘๊ธ‰ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ“ DI ํŒจํ„ด โœ“ ์„œ๋น„์Šค ๋ ˆ์ด์–ด โœ“ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ โœ“ ํ”„๋ ˆ์ž„์›Œํฌ ํ™œ์šฉ ๊ณ ๊ธ‰ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ“ ๋Œ€๊ทœ๋ชจ ์„ค๊ณ„ โœ“ ์„ฑ๋Šฅ ์ตœ์ ํ™” โœ“ ๊ณ ๊ธ‰ ํŒจํ„ด โœ“ ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ

๐Ÿš€ ์‹ค๋ฌด์—์„œ์˜ MVVM ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์‹ค์ œ ์—…๋ฌด์—์„œ MVVM์„ ์ ์šฉํ•  ๋•Œ ์•Œ์•„๋‘๋ฉด ์ข‹์€ ์‹ค๋ฌด ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ์ด๊ฑด ๊ฒฝํ—˜์—์„œ ๋‚˜์˜จ ์ง„์งœ ๋…ธํ•˜์šฐ์•ผ. ๐Ÿ’Ž

1. ๋ช…๋ช… ๊ทœ์น™ ์ผ๊ด€์„ฑ ์œ ์ง€

ํŒ€ ์ „์ฒด๊ฐ€ ๊ฐ™์€ ๋ช…๋ช… ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ ํ•ด:

// ViewModel ๋ช…๋ช…
MainViewModel, UserListViewModel, SettingsViewModel

// View ๋ช…๋ช…
MainWindow, UserListView, SettingsView

// Command ๋ช…๋ช…
SaveCommand, DeleteCommand, LoadDataCommand (๋™์‚ฌ + Command)

// Property ๋ช…๋ช…
UserName, IsEnabled, SelectedItem (๋ช…์‚ฌ ๋˜๋Š” ํ˜•์šฉ์‚ฌ)

// Service ๋ช…๋ช…
IDataService, IDialogService, INavigationService (I + ๋ช…์‚ฌ + Service)

2. ViewModel์€ ๊ฐ€๋ณ๊ฒŒ ์œ ์ง€

ViewModel์— ๋„ˆ๋ฌด ๋งŽ์€ ๋กœ์ง์„ ๋„ฃ์ง€ ๋งˆ! ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Service๋‚˜ Model๋กœ ๋ถ„๋ฆฌํ•ด:

// โŒ ๋‚˜์œ ์˜ˆ: ViewModel์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
public class UserViewModel : BaseViewModel
{
    public void SaveUser()
    {
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
        using var connection = new SqlConnection(connectionString);
        connection.Open();
        
        // SQL ์ฟผ๋ฆฌ ์‹คํ–‰
        var command = new SqlCommand("INSERT INTO Users...", connection);
        command.ExecuteNonQuery();
        
        // ํŒŒ์ผ ์ €์žฅ
        File.WriteAllText("user.json", JsonSerializer.Serialize(user));
    }
}

// โœ… ์ข‹์€ ์˜ˆ: Service๋กœ ๋ถ„๋ฆฌ
public class UserViewModel : BaseViewModel
{
    private readonly IUserService _userService;

    public async Task SaveUser()
    {
        await _userService.SaveUserAsync(User);
    }
}

3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ „๋žต

์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•ด:

public class MainViewModel : BaseViewModel
{
    private readonly IDialogService _dialogService;
    private readonly ILogger _logger;

    private string _errorMessage;
    public string ErrorMessage
    {
        get => _errorMessage;
        set => SetProperty(ref _errorMessage, value);
    }

    private bool _hasError;
    public bool HasError
    {
        get => _hasError;
        set => SetProperty(ref _hasError, value);
    }

    public ICommand SaveCommand { get; }

    private async Task SaveAsync()
    {
        try
        {
            HasError = false;
            ErrorMessage = string.Empty;

            await _dataService.SaveAsync(Data);
            _dialogService.ShowMessage("์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
        }
        catch (ValidationException ex)
        {
            // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜ - ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ
            HasError = true;
            ErrorMessage = ex.Message;
            _logger.LogWarning(ex, "Validation failed");
        }
        catch (Exception ex)
        {
            // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ - ๋กœ๊ทธ ๊ธฐ๋ก ํ›„ ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€
            HasError = true;
            ErrorMessage = "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
            _logger.LogError(ex, "Save failed");
            
            _dialogService.ShowMessage(ErrorMessage, "์˜ค๋ฅ˜");
        }
    }
}

4. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ

ํฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ์— ์‹ ๊ฒฝ ์จ์•ผ ํ•ด:

public class LargeDataViewModel : BaseViewModel, IDisposable
{
    private ObservableCollection<DataItem> _items;
    private CancellationTokenSource _cts;

    public async Task LoadDataAsync()
    {
        _cts = new CancellationTokenSource();

        try
        {
            // ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
            const int pageSize = 100;
            var page = 0;

            while (!_cts.Token.IsCancellationRequested)
            {
                var items = await _dataService.GetPageAsync(page, pageSize, _cts.Token);
                
                if (items.Count == 0) break;

                foreach (var item in items)
                {
                    Items.Add(item);
                }

                page++;
            }
        }
        catch (OperationCanceledException)
        {
            // ์ทจ์†Œ๋จ
        }
    }

    public void Dispose()
    {
        _cts?.Cancel();
        _cts?.Dispose();
        
        // ํฐ ์ปฌ๋ ‰์…˜ ์ •๋ฆฌ
        Items?.Clear();
    }
}

5. ๋””์ž์ธ ํƒ€์ž„ ๋ฐ์ดํ„ฐ

๋””์ž์ด๋„ˆ์—์„œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์œ„ํ•œ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ด:

public class MainViewModel : BaseViewModel
{
    public ObservableCollection<User> Users { get; set; }

    public MainViewModel() : this(null)
    {
    }

    public MainViewModel(IUserService userService)
    {
        if (userService == null)
        {
            // ๋””์ž์ธ ํƒ€์ž„ ๋ฐ์ดํ„ฐ
            Users = new ObservableCollection<User>
            {
                new User { Name = "ํ™๊ธธ๋™", Age = 25 },
                new User { Name = "๊น€์ฒ ์ˆ˜", Age = 30 },
                new User { Name = "์ด์˜ํฌ", Age = 28 }
            };
        }
        else
        {
            // ๋Ÿฐํƒ€์ž„ ๋ฐ์ดํ„ฐ
            Users = new ObservableCollection<User>();
            LoadUsers(userService);
        }
    }
}

// XAML์—์„œ ๋””์ž์ธ ํƒ€์ž„ ViewModel ์„ค์ •
<Window xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:MyApp.ViewModels"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
    <!-- UI ์š”์†Œ๋“ค -->
</Window>

6. ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

MVVM ์ฝ”๋“œ๋ฅผ ๋ฆฌ๋ทฐํ•  ๋•Œ ํ™•์ธํ•  ์‚ฌํ•ญ๋“ค:

โœ… View๊ฐ€ ViewModel๋งŒ ์ฐธ์กฐํ•˜๋Š”๊ฐ€?
โœ… ViewModel์ด View๋ฅผ ์ฐธ์กฐํ•˜์ง€ ์•Š๋Š”๊ฐ€?
โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด Service๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๋Š”๊ฐ€?
โœ… ๋ชจ๋“  ์†์„ฑ์ด PropertyChanged๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”๊ฐ€?
โœ… ์ปค๋งจ๋“œ์˜ CanExecute๊ฐ€ ์ ์ ˆํžˆ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š”๊ฐ€?
โœ… ๋น„๋™๊ธฐ ์ž‘์—…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋Š”๊ฐ€?
โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•œ๊ฐ€?
โœ… ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ์€ ์—†๋Š”๊ฐ€?
โœ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š”๊ฐ€?
โœ… ๋ช…๋ช… ๊ทœ์น™์ด ์ผ๊ด€์ ์ธ๊ฐ€?

๐Ÿ’ก ์‹ค๋ฌด ์กฐ์–ธ

์™„๋ฒฝํ•œ MVVM์„ ์ถ”๊ตฌํ•˜๋‹ค๊ฐ€ ์˜คํžˆ๋ ค ์ƒ์‚ฐ์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด. ๋•Œ๋กœ๋Š” ์‹ค์šฉ์ ์ธ ํƒ€ํ˜‘์ด ํ•„์š”ํ•ด!

์˜ˆ๋ฅผ ๋“ค์–ด:
- ์•„์ฃผ ๊ฐ„๋‹จํ•œ UI ๋กœ์ง์€ ์ฝ”๋“œ-๋น„ํ•˜์ธ๋“œ์— ๋‘๋Š” ๊ฒŒ ๋‚˜์„ ์ˆ˜ ์žˆ์–ด
- ๋ชจ๋“  ๊ฑธ ์ถ”์ƒํ™”ํ•  ํ•„์š”๋Š” ์—†์–ด (YAGNI ์›์น™)
- ํŒ€์˜ ์ˆ™๋ จ๋„์— ๋งž์ถฐ ์ ์ง„์ ์œผ๋กœ ๋„์ž…ํ•ด

์ค‘์š”ํ•œ ๊ฑด ์œ ์ง€๋ณด์ˆ˜ ๊ฐ€๋Šฅํ•˜๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฑฐ์•ผ! ๐ŸŽฏ

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

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. ๐ŸŽ‰ WPF MVVM ํŒจํ„ด์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋‹ค๋ค˜์ง€?

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, MVVM์€ ํ•œ ๋ฒˆ ์ต์ˆ™ํ•ด์ง€๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง€๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›Œ์ง€๊ณ , ํŒ€ ํ˜‘์—…๋„ ํ›จ์”ฌ ์ˆ˜์›”ํ•ด์ ธ. ๐Ÿš€

ํ•ต์‹ฌ ์š”์•ฝ ์ •๋ฆฌ

1๏ธโƒฃ MVVM์˜ ์„ธ ๊ฐ€์ง€ ๋ ˆ์ด์–ด
- Model: ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
- View: ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค (XAML)
- ViewModel: ์ค‘๊ฐ„ ์—ฐ๊ฒฐ ๊ณ„์ธต

2๏ธโƒฃ ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜
- ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์œผ๋กœ View์™€ ViewModel ์—ฐ๊ฒฐ
- INotifyPropertyChanged๋กœ ๋ณ€๊ฒฝ ์•Œ๋ฆผ
- Command ํŒจํ„ด์œผ๋กœ ์‚ฌ์šฉ์ž ์•ก์…˜ ์ฒ˜๋ฆฌ

3๏ธโƒฃ ์„ค๊ณ„ ์›์น™
- View๋Š” ViewModel๋งŒ ์•Œ์•„์•ผ ํ•จ
- ViewModel์€ View๋ฅผ ๋ชฐ๋ผ์•ผ ํ•จ
- ๋‹จ๋ฐฉํ–ฅ ์˜์กด์„ฑ ์œ ์ง€
- ์˜์กด์„ฑ ์ฃผ์ž… ํ™œ์šฉ

4๏ธโƒฃ ์‹ค๋ฌด ํŒ
- ๋ช…๋ช… ๊ทœ์น™ ์ผ๊ด€์„ฑ
- ViewModel์€ ๊ฐ€๋ณ๊ฒŒ
- ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
- ์„ฑ๋Šฅ ์ตœ์ ํ™”
- ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

MVVM์€ ๋‹จ์ˆœํžˆ ์ฝ”๋”ฉ ํŒจํ„ด์ด ์•„๋‹ˆ๋ผ, ์ข‹์€ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์‚ฌ๊ณ ๋ฐฉ์‹์ด์•ผ. ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ, ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ, ์œ ์ง€๋ณด์ˆ˜์„ฑ ๊ฐ™์€ ์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™์˜ ํ•ต์‹ฌ ์›์น™๋“ค์„ ์‹ค์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์ง€. ๐Ÿ’ก

์ฒ˜์Œ์—๋Š” ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. "์ด๋ ‡๊ฒŒ๊นŒ์ง€ ํ•ด์•ผ ํ•˜๋‚˜?" ์‹ถ์„ ์ˆ˜๋„ ์žˆ๊ณ . ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๊ณ  ํŒ€์›์ด ๋Š˜์–ด๋‚˜๋ฉด, MVVM์˜ ์ง„๊ฐ€๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฑฐ์•ผ. 6๊ฐœ์›” ํ›„์— ์ž์‹ ์ด ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ๋ดค์„ ๋•Œ, "์•„, ์ด ์ฝ”๋“œ ์ •๋ง ์ž˜ ์งฐ๋„ค!" ํ•˜๊ณ  ๊ฐํƒ„ํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๐Ÿ˜Š

๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„

์ด์ œ ์ง์ ‘ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž!

1๏ธโƒฃ ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ (ํ•  ์ผ ๊ด€๋ฆฌ, ๋ฉ”๋ชจ์žฅ ๋“ฑ)
2๏ธโƒฃ ์ ์ง„์ ์œผ๋กœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
3๏ธโƒฃ ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ์—ฐ์Šต
4๏ธโƒฃ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
5๏ธโƒฃ GitHub์— ์˜ฌ๋ฆฌ๊ณ  ํ”ผ๋“œ๋ฐฑ ๋ฐ›๊ธฐ

ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๊ฒฝํ—˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ํ•ด์ฃผ๊ณ , ์‹ค๋ฌด ๋…ธํ•˜์šฐ๋„ ๊ณต์œ ํ•ด์ค„ ๊ฑฐ์•ผ. ๐Ÿค

๊ธฐ์–ตํ•ด! ์™„๋ฒฝํ•œ ์ฝ”๋“œ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ž‘์„ฑํ•˜๋Š” ์‚ฌ๋žŒ์€ ์—†์–ด. ๊ณ„์† ๋ฐฐ์šฐ๊ณ , ์‹ค์ˆ˜ํ•˜๊ณ , ๊ฐœ์„ ํ•˜๋ฉด์„œ ์„ฑ์žฅํ•˜๋Š” ๊ฑฐ์•ผ. MVVM๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ์ฒ˜์Œ์—” ์„œํˆด๋Ÿฌ๋„ ๊ดœ์ฐฎ์•„. ๊ณ„์† ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ MVVM์œผ๋กœ ์ƒ๊ฐํ•˜๊ณ  ์ฝ”๋”ฉํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ! ๐Ÿ’ช

WPF MVVM ๊ฐœ๋ฐœ์ž๋กœ์„œ์˜ ์—ฌ์ •์„ ์‘์›ํ• ๊ฒŒ! ํ™”์ดํŒ…! ๐ŸŽ‰โœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ฝ”๋“œ๋„ ๋งŽ์ด ์ฝ์–ด๋ด. ๊ทธ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ์„ฑ์žฅ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ“š

์ž, ์ด์ œ ์—๋””ํ„ฐ๋ฅผ ์—ด๊ณ  ๋ฉ‹์ง„ WPF ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿš€

๐ŸŽ“ Happy Coding with WPF MVVM! ๐ŸŽ“
๋‹น์‹ ์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์„ ์‘์›ํ•ฉ๋‹ˆ๋‹ค!

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

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

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

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