티스토리 뷰

2021.02.18 - [UWP & Windows App/Beginner] - MVVM Pattern을 사용하는 개발자를 위한 안내 (업데이트 : 2022/03/21)

제 블로그에서 가장 인기있는 포스팅이 바로 MVVM Pattern 관련 포스팅입니다. 그런데, 오래전에 등록한 포스팅이라 이번에 새롭게 WPF 버전으로 정리를 하려고 합니다.

MVVM Pattern 소개

1. MVVM Pattern History

  • MVVM은 WPF (Windows Presentation Foundation) 및 Silverlight의 기능을 활용하여 이벤트 중심 프로그래밍을 간소화하기 위해 Microsoft 아키텍처 Ken Cooper 및 Ted Peters가 개발했습니다. 
  • Microsoft의 WPF 및 Silverlight 아키텍트 중 한 명인 John Gossman은 2005 년 자신의 블로그에서 MVVM에 대한 내용을 포스팅 했습니다.
  • MVVM Model View ViewModel Part - 1

2. Motivation for the MVVM Pattern

  • Event-driven architecture 사용 응용 프로그램을 오랜 기간 유지보수하고 관리를 했을 때 발생되는 문제점 해결이 필요했습니다.
    • 코드의 복잡성 상승 문제 
    • UI 수정 난이도 상승
      • 중복코드 발생 - 기존 코드를 수정하지 않고, 복사해서 새로 만들어 사용
    • Unit Test를 할 수 없음
      • 비지니스 로직만 인스턴스 할 수 없음
    • 메모리 관리의 어려움
    • 전체적인 성능 저하
    • 최신 기술 도입 어려움
  • Advantages and Disadvantages of Event-Driven Programming

3. The MVVM Pattern

  • 뷰, 뷰모델, 모델은 분리된 구성 요소 
    • 대체 가능해야 함
    • 수정 시 다른 요소에 영향을 주지 않아야 함
    • 독립적인 작업이 가능해야 함
    • 격리 Unit test가 가능해야 함
  • Data Binding and Command 구성 요소
    • Binding
    • ICommand
  • Send Notifications 구성 요소
    • INotifyPropertyChanged
    • INotifyCollectionChanged

4. View

  • 사용자가 보는 화면(구조, 레이아웃 및 모양)을 정의하는 역할을 담당합니다.
  • View는 비즈니스 로직을 포함하지 않고, 제한된 code-behind만 사용하며, 순수하게 XAML로만 정의 됩니다.
  • WPF, UWP에서 View는 Window, Page, UserControl을 사용하나, Xamarin.Forms에서는 forms:WindowsPage, .NET MAUI에서는 ContentPage를 사용합니다.
  • 자신만의 뷰모델을 사용하거나, 상위 뷰모델을 상속받아 사용하고, 뷰모델의 변경 알림 이벤트(INotifyPropertyChanged)에 응답해서 뷰를 변경합니다.

예) MainWindow.xaml

 

<Window x:Class="CoreWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CoreWpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <Button Content="GetWindowRectangle" Command="{Binding GetCommand}"/>
            <TextBlock x:Name="Result"/>
        </StackPanel>
    </Grid>
</Window>

5. Model

  • 비즈니스 및 유효성 검사 로직과 함께 데이터 모델을 포함하는 응용 프로그램의 도메인 모델을 이야기합니다.
  • 모델은 대부분 class로 만들어지며, 내부에 property를 가지고 있습니다.
    • 예) Repositories, business objects, data transfer objects(DTOs), Plain Old CLR Objects(POCOs)
  • INotifyPropertyChanged 이벤트와 TwoWay Binding을 이용해서 View와 양방향 데이터 송수신을 할 수 있습니다.
  • 비즈니스 로직을 포함하지 않습니다.

예) Person.cs

using Microsoft.Toolkit.Mvvm.ComponentModel;

namespace BasicControlSample
{
    public class Person : ObservableObject
    {
        private string _name;
        public string Name
        {
            get { return _name; }
            set { SetProperty(ref _name, value); }
        }

        private bool _sex;
        public bool Sex
        {
            get { return _sex; }
            set { SetProperty(ref _sex, value); }
        }

        private int _age;
        public int Age
        {
            get { return _age; }
            set { SetProperty(ref _age, value); }
        }

        private string _address;
        public string Address
        {
            get { return _address; }
            set { SetProperty(ref _address, value); }
        }
    }
}

6. ViewModel

  • View와 Model 사이의 중개자 역할을 하며, View의 동작을 상태(Status)를 이용해 제어 합니다.
    • 여러개의 Model을 목록형으로 제공하면, View의 컨트롤이 이를 사용자에게 보여줍니다.
    • View에 Add버튼을 클릭하면, ICommand를 이용해서, 메소드를 실행 시키고, 메소드에서 데이터 목록에 새로운 Model을 추가합니다.
  • INotifyPropertyChanged 이벤트와 TwoWay Binding을 이용해서 View와 양방향 데이터 송수신을 할 수 있습니다.
  • 서비스들을 이용해서 비즈니스 로직을 처리합니다.

예) MainViewModel.cs

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace BasicControlSample
{
    public class MainViewModel : ObservableObject
    {
        private IList<Person> _persons = new ObservableCollection<Person>
            {
                new Person{ Name = "kaki0104", Sex = true, Age = 11, Address = "Seoul1" },
                new Person{ Name = "kaki0143", Sex = false, Age = 150, Address = "Seoul140" },
            };
        public IList<Person> Persons { get { return _persons; } }

        private Person _selectedListItem;
        public Person SelectedListItem
        {
            get { return _selectedListItem; }
            set { SetProperty(ref _selectedListItem, value); }
        }

        private Person _selectedComboItem;
        public Person SelectedComboItem
        {
            get { return _selectedComboItem; }
            set { SetProperty(ref _selectedComboItem, value); }
        }

        private Person _selectedListItem2;
        public Person SelectedListItem2
        {
            get { return _selectedListItem2; }
            set { SetProperty(ref _selectedListItem2, value); }
        }

        public IRelayCommand DeleteListItemCommand { get; set; }

        private Person _selectedComboItem2;
        public Person SelectedComboItem2
        {
            get { return _selectedComboItem2; }
            set { SetProperty(ref _selectedComboItem2, value); }
        }

        public IRelayCommand DeleteComboItemCommand { get; set; }

        public MainViewModel()
        {
            Init();
        }

        private void Init()
        {
            SelectedListItem = Persons.FirstOrDefault();
            SelectedComboItem = Persons.FirstOrDefault();

            DeleteListItemCommand = new RelayCommand(OnDeleteListItem,
                () => SelectedListItem2 != null && SelectedListItem2.Age % 2 == 0);
            DeleteComboItemCommand = new RelayCommand(OnDeleteComboItem,
                () => SelectedComboItem2 != null && SelectedComboItem2.Age % 2 == 1);
        }

        private void OnDeleteComboItem()
        {
            Persons.Remove(SelectedComboItem2);
        }

        private void OnDeleteListItem()
        {
            Persons.Remove(SelectedListItem2);
        }
    }
}

7. The Benefits of MVVM

  • 관심사의 분리(Separation of concerns)를 이용하여 문제를 해결할 수 있습니다.
    • 코드의 복잡성 축소, UI 수정 난이도 하락, 메모리 관리 용이, 전체적인 성능 향상, 최신 기술 도입 용이
  • ViewModel과 Model은 개발자가, View는 디자이너가 각자 동시 작업이 가능합니다.
  • 개발자는 UI(View)없이 ViewModel을 인스턴스시켜 Unit Test  프로젝트를 생성하고 테스트할 수 있습니다.

8. Platform 차이점(2022-04-26)

구분 WPF UWP Xamarin.Forms Uno Platform .NET MAUI Windows App SDK
Windows 10, 11 O O O O O O
Android X X O O O X
iOS, macOS X X iOS O O X
Support OS       Linux, Tizen    
WPF XAML O O O O
Windows 7 O X O O X X
안정성 Stable Stable Stable Stable Preview Experimental
Sandbox X O ? UWP:O, WPF:X ? X

WPF vs Xamarin.Forms 차이점

9. 화면 개발자 vs 공통 개발자

  • MVVM Pattern을 사용하는 화면 개발자
    • MVVM 기본 개념 파악하고(Binding, Template, ICommand) 이미 만들어져있는 다른 화면을 참고하면서 개발
    • IValueConverter나 CustomControl 등 고급 기능이 필요한 경우 공통 개발자(팀)에 문의 후 사용
    • 비지니스 로직을 이해하고 구현하는것이 목표
  • MVVM Pattern을 사용하는 Framework(공통) 개발자
    • MVVM Pattern의 모든 기능을 이해하고, 개발할 수 있어야 함
    • IValueConver, Behavior, IoC Container, Dependency Injection, Custom Control, Style, Animation, Selector 등
    • 화면 개발자들이 빠르게 개발을 진행할 수 있게 서포트를 하면서, 성능 이슈가 발생하지 않도록 코드를 정규화 시켜야 함

10. Q&A

  • Q1. MVVM Pattern을 꼭 사용해야 하나요?
    • A1. XAML을 사용하는 플랫폼에서는 사용하는 것을 권장합니다. 
  • Q2. 코드 비하인드(code-behind)에 코딩을 하지 못하나요?
    • A2. 코딩을 할 수 있고, 일부 코딩이 필요하기도 합니다. 하지만, MVVM Pattern은 View와 ViewModel이 서로 의존성을 가지지 않도록 개발하기 때문에 거의 필요도 없고, 추천하지 않습니다.
  • Q3. View의 Button_Click 이벤트를 생성하고, 코드 비하인드에서 화면을 열거나 간단한 동작등은 처리해도 괜찮나요?
    • A3. 코드 비하인드에 코딩은 추천하지 않습니다. ViewModel이 해당 화면에 대한 모든 논리를 담당해야하는데, 나누어져있다면 코드 복잡도가 상승하여 운영시에 문제가 발생할 수 있습니다.
  • Q4. 개발 속도는 어떤가요?
    • A4. WinForm의 Event-driven 방식에 비해서 1.5배 정도는 느립니다. 하지만, 화면 개발자가 익숙해지면 큰 차이는 없을 것이라고 생각됩니다. 
  • Q5. View나 Control을 ViewModel에서 직접 제어하면 않되나요?
    • A5. 네. 그렇게 사용하시면 View와 ViewModel의 의존성이 생기기 때문에 사용하시면 않됩니다.
  • Q6. View나 Control의 모든 기능을 ViewModel에서 제어가 가능한가요?
    • A6. ViewModel은 화면의 논리적인 부분을 담당하고, 각 컨트롤들의 제어는 논리를 기준으로 처리되어야 합니다. 컨트롤을 복잡하게 제어해야하는 경우에는 Behavior나 Custom Control 등을 이용하면 어떤 화면이든지 만들 수 있습니다.

11. 참고

 

TAG
댓글
댓글쓰기 폼
Total
679,123
Today
159
Yesterday
348
«   2022/05   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
05-27 15:44
글 보관함