자바, 웹만 공부하던 개발자였던 나에게 (사실 자바 웹도 AI로 바이브코딩하는게 대부분이었다) 방산이라는 분야로 개발하겠다고 마음먹은지 얼마 되지않아 이무것도 모르는 시기에 운이좋게 취업이 되었다.

나를 뽑아주신 분들은 금방 배울것같다고 나를 뽑아주셨었고, 처음 해본것들에 치이면서 2달정도된 현재 무엇이 WPF고 MVVM인지는 알지만 내부 구조를 정확히 파악하지 못하는것 같아 블로그에 정리하면서 다시 복습하는 시간을 가져 보려고한다.

WPF란?

WPF와 데스크톱 UI 개요

첫번째로 WPF는 무엇인지 알아보자.

WPF(Windows Presentation Foundation)는 마이크로소프트가 개발한 윈도우 데스크톱 애플리케이션용 UI(사용자 인터페이스) 프레임워크

WPF의 특징으로는

  1. 화면 디자인인 (UI)는 XML 기반의 XAML 코드로 작성하며, 프로그램 동작 로직은 C#으로 개발한다.
  2. MVVM 패턴을 사용하기에 최적화 되어있어, 유지보수가 용이하다.
  3. 윈도우 폼에 비해 디자인, 커스터마이징이 자유롭다.

다음은 MVVM을 알아보자

MVVM 패턴이란

Model-View-View Model.

Model View ViewModel 구조도

MVVM 패턴은 Model, View, ViewModel로 이루어진 아키텍쳐 패턴으로 각각의 역할을 아래와 같다.

  1. View : XAML로 화면을 구성하고, DataContext에 ViewModel을 연결해서 데이터를 표시하는 역할.
  2. View와 Model 사이의 중개자. INotifyPropertyChanged로 속성 변경을 View에 알리고, ICommand(RelayCommand)로 버튼 클릭 같은 이벤트를 처리함.
  3. UI와 완전히 무관한 순수한 데이터 구조. DB 엔티티, DTO, 비즈니스 규칙 등이 여기 들어감.

여기서 중요한것은 View와 모델은 서로의 존재를 몰라야 한다는 것이다. 두 클래스 간 데이터 교류는 모두 ViewModel을 통해서 이루어진다.

이론적으로는 개인적으로 이런게 있고 대충 이런거구나 정도만 알고 실전 예제를 한번 해보는게 이해하는게 더 쉽지 않을까 싶다.

간단하게 숫자를 입력하면 x 2를 한 결과를 보여주는 WPF 프로젝트를 만들어보자

WPF 예제 프로젝트 디렉터리 구조

디렉터리 구조는 위와같이 잡는다.

나는 보통 MVVM에 더하여 Service까지 디렉토리로 만들어 역할 분리를 하는 편이다.

서비스는 주로 로직을 담당한다. 위 프로젝트에서는 x2를 해주는 로직이 서비스에 들어가게 된다.

첫번째로 볼 파일은 ViewModel 내부에 있는 RelayCommand와 ViewModelBase이다.

각각의 역할은 다음과 같다.

  1. ViewModelbase

    INotifyPropertyChanged 구현을 한 곳에 모아두는 베이스 클래스야. 모든 ViewModel이 이걸 상속받아서 중복 코드를 없애는 용도.

  2. RelayCommand

    ICommand를 간편하게 구현한 클래스야. WPF에서 버튼 클릭 같은 이벤트를 ViewModel에서 처리하려면 ICommand가 필요한데, 매번 클래스를 새로 만들기 번거러우니까 Action을 받아서 재사용하는 구조

위 두 코드는 거의 모든 WPF에서 사용되니 기억해뒀다가 재사용 할 수 있도록 하자.

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
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;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
    public void Execute(object parameter) => _execute(parameter);
    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

본인은 ViewModel 내부에 Base라는 폴더를 만들어 넣어놓는다.

ViewModelBase와 RelayCommand는 추후 다른글로 다뤄보도록 하겠다.

다음은 Model을 정의해보자.

NumberInfo 모델 파일 생성 화면

Models에 NumberInfo.cs를 만든다.

namespace WpfBasicPrc.Models
{
    public class NumberInfo
    {
        public int Number { get; set; }
        public int FinalNumber { get; set; }
    }
}

다음 ViewModel을 보자.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using WpfBasicPrc.Models;
using WpfBasicPrc.Services;
using WpfBasicPrc.ViewModels.Base;

namespace WpfBasicPrc.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        // CalculateService: x2 계산 로직을 담당하는 서비스
        private readonly CalculateService _calculateService;

        // NumberInfo: 입력값(Number)과 결과값(FinalNumber)을 담는 모델
        private NumberInfo _numberInfo = new NumberInfo();

        // 계산 전 숫자 - View의 입력 TextBox와 바인딩
        public int Number
        {
            get => _numberInfo.Number;
            set { _numberInfo.Number = value; OnPropertyChanged(); }
        }

        // 계산 후 숫자 - View의 결과 TextBox와 바인딩
        public int FinalNumber
        {
            get => _numberInfo.FinalNumber;
            set { _numberInfo.FinalNumber = value; OnPropertyChanged(); }
        }

        // 계산 버튼 클릭 시 실행되는 커맨드 - View의 Button과 바인딩
        public ICommand CalculateCommand { get; }

        // 생성자: 서비스 초기화 및 커맨드 등록
        public MainViewModel()
        {
            // 서비스 인스턴스 생성
            _calculateService = new CalculateService();

            // 버튼 클릭 시 Service에 Model을 넘겨 계산 후 결과를 FinalNumber에 저장
            CalculateCommand = new RelayCommand(_ =>
            {
                FinalNumber = _calculateService.Double(_numberInfo);
            });
        }
    }
}

위 예제에서 ViewModel은 Model <-> ViewModel, ViewModel <-> Service를 담당한다. 이떄 최종 처리된 값은 추후 View에서 사용하게 된다.

다음 Service를 보자.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfBasicPrc.Models;

namespace WpfBasicPrc.Services
{
    class CalculateService
    {
        //들어온 인자를 2배해서 리턴
        public int Double(NumberInfo numberInfo)
        {
            return numberInfo.Number * 2;
        }
    }
}

들어온 값을 2배해서 넘겨주는 로직이다. 예제에서는 간단하지만 DB연결과같은 처리를 보통 서비스에서 해준다.

마지막으로 View를 보겠다. WPF의 속성같은건 다른 글에서 다루니 일단은 따라치면서 흐름을 이해하는 용도로 사용하였으면 한다.

<Window x:Class="WpfBasicPrc.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:WpfBasicPrc"
        xmlns:vm="clr-namespace:WpfBasicPrc.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- DataContext: MainViewModel View 연결 -->
    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>

    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <StackPanel Orientation="Horizontal">

                <!-- 입력값: Number 프로퍼티에 바인딩, 입력 즉시 업데이트 -->
                <TextBox Width="200"
                         Text="{Binding Number, UpdateSourceTrigger=PropertyChanged}"/>

                <!-- 계산 버튼: CalculateCommand 실행 -->
                <Button Content="x2 계산"
                        Command="{Binding CalculateCommand}"
                        Margin="10 0 0 0"/>

                <!-- 결과값: FinalNumber 프로퍼티에 바인딩, 읽기 전용 -->
                <TextBox Width="200"
                         Text="{Binding FinalNumber}"
                         IsReadOnly="True"
                         Margin="10 0 0 0"/>

            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

숫자를 두 배로 계산하는 WPF 예제 실행 결과

결과물이다. 단순한 예제였고, 처음은 그냥 따라치면서 흐름, 이런게 WPF고 MVVM이구나 라는 느낌만 가져갔으면 좋겠다. 추후 후임이 들어오거나, WPF를 가르쳐야 하는 경우가 있을 경우를 대비하여 공부하면서 초보자가 읽어도 이해할 수 있는 수준으로 글을 작성하고 싶다.

댓글로 잘못된 정보나 추가했으면 하는 내용을 편하게 남겨주면 좋겠다.