WPF 기반 포터블 라디오 프로그램

심심풀이로 만들어본 WPF 기반 라디오 프로그램.

압축풀어서 KoreaRadioLivesmart.exe 를 실행하면 됩니다. 모든 검사를 한 깨끗한 파일입니다. 자동업데이트 지원


맥용 라디오 프로그램. MacOS, SwiftUI

아이폰용 라디오, iOS앱, swiftui

[MVVM] 9.MVVM과 WPF 디자인 패턴 및 UI 커스터마이징, 데이터 템플릿과 스타일을 활용한 UI 커스터마이징

Windows Presentation Foundation (WPF)은 Powerful한 UI 기반 애플리케이션을 개발할 수 있는 .NET Framework의 한 구성 요소입니다. WPF는 사용자 인터페이스(UI)를 여러 방식으로 정의할 수 있게 하여, 개발자가 기능적인 애플리케이션을 만들기 위해 무한한 가능성을 제공합니다. 이 블로그 글에서는 MVVM 패턴을 중심으로 WPF의 디자인 패턴을 심도 있게 분석하고, UI 커스터마이징을 위한 데이터 템플릿과 스타일을 활용하는 방법에 대해 살펴보겠습니다.

1. MVVM 디자인 패턴 개요

MVVM(Model-View-ViewModel)은 WPF 애플리케이션을 개발하는 데 가장 널리 사용되는 디자인 패턴입니다. MVVM은 UI와 비즈니스 로직을 분리하여 코드의 재사용성과 유지 보수성을 높이고, 개발자와 디자이너 간의 협업을 원활하게 해줍니다.

1.1 MVVM의 구조

  • Model: 애플리케이션의 데이터와 비즈니스 로직을 관장합니다. 데이터베이스나 외부 API에서 데이터를 가져오는 역할을 수행합니다.
  • View: 사용자에게 보여지는 UI 요소들입니다. XAML을 사용하여 정의되며, ViewModel과 바인딩을 통해 UI 요소와 데이터를 연결합니다.
  • ViewModel: Model과 View 간의 중재자 역할을 합니다. View와 Model의 상호작용을 처리하고, Data Binding을 통해 View에 데이터를 제공합니다.

1.2 MVVM의 이점

MVVM 패턴은 여러 가지 이점을 제공합니다:

  • 코드 분리: UI 코드와 비즈니스 로직이 분리되어 유지보수성과 코드 가독성이 높아집니다.
  • 재사용성: ViewModel을 통해 여러 View에서 동일한 데이터를 쉽게 재사용할 수 있습니다.
  • 디자인 협업: 디자이너는 View를 XAML로 독립적으로 구현할 수 있으며, 개발자는 ViewModel에서 비즈니스 로직을 처리합니다.

2. WPF에서의 Data Binding과 Commands

WPF의 핵심은 데이터 바인딩과 명령(Command)입니다. 데이터 바인딩을 통해 View와 ViewModel 간의 데이터를 동기화하고, Command를 통해 사용자 상호작용을 처리합니다.

2.1 데이터 바인딩

데이터 바인딩이란 View와 ViewModel 간의 데이터 통신을 설정한 것입니다. 예를 들어, ViewModel에 있는 속성이 변경되면 View에 자동으로 업데이트됩니다. 다음은 기본적인 데이터 바인딩의 예입니다.

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MVVM Example" Height="350" Width="525">
    <Grid>
        <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
        <Button Command="{Binding SubmitCommand}" Content="Submit" />
    </Grid>
</Window>

위의 예제에서 TextBox의 Text 속성은 ViewModel의 UserName 속성과 바인딩되어 있습니다. 사용자가 텍스트를 입력하면 ViewModel의 UserName 속성이 자동으로 업데이트됩니다.

2.2 Command 인터페이스

WPF에서 사용자 상호작용을 처리하기 위해 Command를 사용합니다. 다음은 ICommand 인터페이스를 구현한 간단한 예제입니다.

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

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

위의 RelayCommand 클래스는 ICommand 인터페이스를 구현하며, 비즈니스 로직을 실행하는 Execute 메소드를 포함하고 있습니다.

3. UI 커스터마이징 – 데이터 템플릿과 스타일

WPF에서는 데이터 템플릿(data templates)과 스타일(styles)을 사용하여 UI를 유연하게 커스터마이징할 수 있습니다.

3.1 데이터 템플릿

데이터 템플릿은 특정 데이터 유형에 대한 UI 표현을 정의합니다. 예를 들어, ListBox의 항목을 사용자 정의 방식으로 표시할 수 있습니다:

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

위 코드에서는 ListBox 내에서 항목의 데이터(사용자 이름 및 이메일)를 사용자 정의 방식으로 표시하고 있습니다.

3.2 스타일과 컨트롤 템플릿

스타일을 사용하여 WPF 컨트롤의 모양과 느낌을 조정할 수 있습니다. 예를 들어 모든 버튼의 기본 스타일을 정의할 수 있습니다:

<Window.Resources>
    <Style TargetType="Button">
        <Setter Property="Background" Value="DarkBlue"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="FontWeight" Value="Bold"/>
        </Style>
</Window.Resources>

이 스타일은 모든 버튼에 적용되며, 버튼의 배경색 및 글자색을 설정합니다.

4. 실전 예제: MVVM을 활용한 간단한 TODO 앱 구현

이제 MVVM 패턴을 사용하여 간단한 TODO 애플리케이션을 구현하는 예제를 살펴보겠습니다.

4.1 Model 클래스

public class TodoItem
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

4.2 ViewModel 클래스

public class TodoViewModel : INotifyPropertyChanged
{
    private ObservableCollection<TodoItem> _todos;
    private string _newTodoTitle;

    public ObservableCollection<TodoItem> Todos
    {
        get { return _todos; }
        set { _todos = value; OnPropertyChanged(nameof(Todos)); }
    }

    public string NewTodoTitle
    {
        get { return _newTodoTitle; }
        set { _newTodoTitle = value; OnPropertyChanged(nameof(NewTodoTitle)); }
    }

    public ICommand AddTodoCommand { get; set; }

    public TodoViewModel()
    {
        Todos = new ObservableCollection<TodoItem>();
        AddTodoCommand = new RelayCommand(AddTodo);
    }

    private void AddTodo(object obj)
    {
        Todos.Add(new TodoItem { Title = NewTodoTitle });
        NewTodoTitle = string.Empty;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

4.3 View (XAML)

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TODO App" Height="450" Width="400">
    <Window.DataContext>
        <local:TodoViewModel />
    </Window.DataContext>

    <Grid>
        <StackPanel>
            <TextBox Text="{Binding NewTodoTitle, UpdateSourceTrigger=PropertyChanged}" />
            <Button Command="{Binding AddTodoCommand}" Content="Add Todo" />
            <ListBox ItemsSource="{Binding Todos}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <CheckBox IsChecked="{Binding IsCompleted}" />
                            <TextBlock Text="{Binding Title}" Margin="5,0,0,0"/>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </StackPanel>
    </Grid>
</Window>

5. 결론

이 블로그 글에서는 MVVM 패턴을 통해 WPF에서 애플리케이션을 개발하는 방법을 설명하였으며, 데이터 템플릿과 스타일을 통해 UI를 커스터마이징하는 방법을 제시했습니다. MVVM을 사용하면 코드의 가독성과 유지 보수성을 높일 수 있을 뿐만 아니라, UI 디자인과 비즈니스 로직 간의 간섭을 최소화하여 효율적인 개발 환경을 제공합니다.

이번 예제를 바탕으로 여러분의 프로젝트에 MVVM 패턴을 적용하고, 데이터 템플릿 및 스타일을 통해 UI를 더욱 매력적으로 만들어 보세요. WPF는 매우 강력한 프레임워크이며, MVVM 디자인 패턴은 여러분의 애플리케이션 개발에 큰 도움이 될 것입니다.

기타 질문이나 피드백이 있으시면 댓글로 남겨주세요!

[MVVM] 8.유닛 테스트와 MVVM, ViewModel의 유닛 테스트 작성 요령

소프트웨어 개발에서 유닛 테스트는 코드를 작고 독립적인 단위로 테스트하여 버그를 조기에 발견하고 코드 품질을 유지하기 위해 필수적인 과정입니다. 특히, MVVM 패턴은 WPF (Windows Presentation Foundation) 애플리케이션의 구조적 설계를 위해 널리 사용되며, ViewModel은 MVVM의 핵심 구성 요소입니다. 이 글에서는 MVVM에서 ViewModel을 어떻게 유닛 테스트할 수 있는지, 그리고 효과적인 유닛 테스트를 작성하기 위한 요령을 자세히 설명합니다.

1. MVVM 패턴의 이해

MVVM(Model-View-ViewModel) 패턴은 WPF 애플리케이션의 아키텍처를 구조화하는 데 사용됩니다. 이 패턴은 다음과 같은 세 가지 주요 구성 요소로 나뉩니다:

  • Model: 애플리케이션의 데이터 및 비즈니스 로직을 포함합니다.
  • View: 사용자 인터페이스를 구성하며, 사용자와 상호작용합니다.
  • ViewModel: Model과 View 사이의 중재자로, UI 로직과 비즈니스 로직을 분리합니다.

이 구조는 단위 테스트를 보다 수월하게 해주며, 특히 ViewModel에서는 독립적인 비즈니스 로직을 테스트할 수 있습니다.

2. 유닛 테스트란?

유닛 테스트는 애플리케이션의 각 구성 요소를 독립적으로 검증하는 자동화된 테스트입니다. 이를 통해 개발자는 코드를 변경한 후에도 기존 기능이 올바르게 작동하는지 확인할 수 있습니다. C#에서는 NUnit, MSTest, xUnit 등 다양한 테스트 프레임워크를 사용하여 유닛 테스트를 작성할 수 있습니다.

3. 유닛 테스트의 장점

유닛 테스트의 주요 장점은 다음과 같습니다:

  • 버그 조기 발견: 코드 변경 부작용을 쉽게 찾아낼 수 있습니다.
  • 리팩토링의 안전성: 코드를 개선할 때 기존 동작을 그대로 유지하는지 검증할 수 있습니다.
  • 문서화: 테스트는 코드가 어떻게 작동하는지에 대한 문서 역할을 할 수 있습니다.
  • 개발 속도 향상: 자동화된 테스트로 인해 반복적인 수작업 테스트를 줄일 수 있습니다.

4. ViewModel의 테스트 준비

ViewModel의 테스트를 준비하기 위해서는 몇 가지 고려사항이 필요합니다:

  1. 의존성 주입: ViewModel에서 필요로 하는 서비스 또는 데이터 접근 객체에 대한 의존성을 주입하여 테스트 대역(mock)을 활용할 수 있도록 해야 합니다.
  2. 속성 바인딩: WPF의 데이터 바인딩을 활용하기 위해 INotifyPropertyChanged 인터페이스를 구현하여 속성의 변경을 감지하도록 합니다.

5. ViewModel 예제

다음은 간단한 ViewModel의 예제입니다. 이 ViewModel은 사용자 이름을 저장하고, 변경할 때마다 알림을 제공합니다:

using System.ComponentModel;

public class UserViewModel : INotifyPropertyChanged
{
    private string _userName;

    public string UserName
    {
        get { return _userName; }
        set
        {
            if (_userName != value)
            {
                _userName = value;
                OnPropertyChanged(nameof(UserName));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

이 ViewModel은 INotifyPropertyChanged를 구현하여 View에 속성 변화를 알립니다. 다음 섹션에서 이 ViewModel을 위한 유닛 테스트를 작성해 보겠습니다.

6. ViewModel 유닛 테스트 작성하기

이제 UserViewModel에 대한 유닛 테스트를 작성해 보겠습니다. NUnit을 사용할 경우, 다음과 같은 테스트 코드를 작성할 수 있습니다:

using NUnit.Framework;
using System.ComponentModel;

[TestFixture]
public class UserViewModelTests
{
    private UserViewModel _viewModel;

    [SetUp]
    public void SetUp()
    {
        _viewModel = new UserViewModel();
    }

    [Test]
    public void UserName_Should_Update_Property()
    {
        // Arrange
        string expected = "John Doe";

        // Act
        _viewModel.UserName = expected;

        // Assert
        Assert.AreEqual(expected, _viewModel.UserName);
    }

    [Test]
    public void UserName_Should_Raise_PropertyChanged_Event()
    {
        // Arrange
        bool eventRaised = false;
        _viewModel.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName == nameof(UserViewModel.UserName))
            {
                eventRaised = true;
            }
        };

        // Act
        _viewModel.UserName = "Jane Doe";

        // Assert
        Assert.IsTrue(eventRaised);
    }
}

7. 모의 객체(Mock Object) 사용하기

뷰모델이 더 복잡해지면 외부 서비스에 대한 의존성을 처리해야 할 수도 있습니다. 이때 모의 객체(mock object)를 사용하여 외부 의존성을 제거하고 ViewModel의 기능을 테스트할 수 있습니다. Moq 라이브러리를 사용하여 간단한 모의 객체를 생성해 보겠습니다:

using Moq;

public interface IUserService
{
    string GetUserName();
}

public class UserViewModel
{
    private readonly IUserService _userService;

    public UserViewModel(IUserService userService)
    {
        _userService = userService;
    }

    public string UserName => _userService.GetUserName();
}

// Test
[Test]
public void UserName_Returns_Correct_Value_From_Service()
{
    // Arrange
    var mockService = new Mock<IUserService>();
    mockService.Setup(s => s.GetUserName()).Returns("Mock User");
    var viewModel = new UserViewModel(mockService.Object);

    // Act
    var userName = viewModel.UserName;

    // Assert
    Assert.AreEqual("Mock User", userName);
}

위의 예제에서는 Moq를 사용하여 IUserService 인터페이스의 모의 객체를 생성하고, GetUserName 메서드가 반환할 값을 지정합니다. 이렇게 하면 외부 서비스에 의존하지 않고 ViewModel을 테스트할 수 있습니다.

8. 여러 테스트 시나리오 다루기

유닛 테스트를 작성할 때는 다양한 테스트 시나리오를 고려해야 합니다. 다음과 같은 상황을 테스트할 수 있습니다:

  • 범위 내의 유효한 입력 값
  • 경계 값 (예를 들어, 최소 및 최대 값)
  • 잘못된 입력 값
  • 예외가 발생하는 경우

예를 들어, UserViewModel에서 사용자 이름 입력의 유효성을 검사하는 메서드를 추가하고 이를 테스트할 수 있습니다.

public bool IsUserNameValid(string userName)
{
    return !string.IsNullOrWhiteSpace(userName) && userName.Length > 2;
}

// Test
[Test]
public void IsUserNameValid_Should_Return_False_When_Empty()
{
    Assert.IsFalse(_viewModel.IsUserNameValid(string.Empty));
}

9. 테스트 실행 및 CI/CD 통합

유닛 테스트는 코드 변경시 빠른 피드백을 제공하므로, 지속적인 통합(CI) 및 지속적인 배포(CD) 파이프라인에 통합하는 것이 좋습니다. GitHub Actions, Azure DevOps, Jenkins와 같은 CI/CD 도구를 활용하여 코드를 푸시할 때마다 자동으로 모든 유닛 테스트를 실행할 수 있습니다.

10. 마무리

MVVM 패턴에서 ViewModel의 유닛 테스트는 애플리케이션의 품질과 유지보수성을 높이는 데 매우 중요합니다. 이번 글을 통해 유닛 테스트의 개념, ViewModel에 대한 유닛 테스트 작성 방법, 모의 객체 사용 및 다양한 테스트 시나리오를 다루는 방법을 배웠습니다. 지속적인 테스트 및 CI/CD 통합을 통해 코드 품질을 지속적으로 유지하고, 향후 수정을 더욱 쉽게 수행하시길 바랍니다.

11. 추가 리소스