WPF 개발 핵심 정리

WPF 개발 핵심 정리

WPF를 처음 보면 XAML 때문에 조금 낯설다.

C# 코드만으로 UI를 만드는 방식과 달리 화면 구조는 XAML로 작성하고, 실제 동작은 C#으로 처리한다. 여기에 DependencyProperty, Binding, DataContext, Command, Resource, Style, Template 같은 개념이 붙는다.

처음에는 각각 별개의 기능처럼 보인다.

하지만 WPF의 구조를 하나의 흐름으로 보면 생각보다 단순하다.

WPF의 핵심은 UI와 데이터를 분리하고 Binding을 통해 연결하는 것이다.

XAML -> View -> DataContext -> Binding -> ViewModel -> Service -> Data

사용자의 입력은 반대로 애플리케이션 로직으로 전달된다.

사용자 입력 -> View -> Command / Event -> ViewModel -> Business Logic -> Data

데이터가 변경되면 다시 UI로 돌아온다.

Data -> ViewModel -> PropertyChanged -> Binding -> View -> UI 갱신

이 흐름을 이해하면 WPF에서 등장하는 대부분의 개념을 하나의 구조 안에서 볼 수 있다.


WPF란

WPF(Windows Presentation Foundation)는 Windows 데스크톱 애플리케이션을 개발하기 위한 .NET UI 프레임워크다.

Windows 애플리케이션의 화면을 구성하고 사용자 입력을 처리하며 데이터 바인딩, 스타일, 템플릿, 애니메이션, 그래픽 렌더링 등의 기능을 제공한다.

WPF 개발의 기본 구조는 다음과 같이 볼 수 있다.

XAML -> View -> ViewModel -> Service -> Data

XAML은 UI를 선언하고 C#은 애플리케이션의 동작과 로직을 구현한다.

하지만 이것을 단순히 “XAML은 화면, C#은 코드”라고만 이해하면 WPF의 핵심을 놓치게 된다.

WPF에서 중요한 것은 UI와 데이터 사이를 Binding System으로 연결하는 구조다.


XAML

WPF에서는 XAML을 사용해 UI를 선언적으로 작성할 수 있다.

<StackPanel>
    <TextBlock Text="Hello" />
    <Button Content="Start" />
</StackPanel>

이 코드는 UI의 구조를 선언한다.

XAML -> UI Object -> UI Tree

C#에서 직접 UI 객체를 생성하는 대신 XAML을 사용하면 화면의 구조와 속성을 코드와 분리할 수 있다.

복잡한 UI에서는 이 차이가 상당히 크다.

XAML -> UI 구조
C# -> 동작 / 로직

그리고 Binding을 사용하면 UI와 ViewModel까지 연결된다.

XAML -> View -> Binding -> ViewModel

UI Tree

WPF의 UI는 부모와 자식 관계를 가지는 Tree 구조로 구성된다.

예를 들어 화면이 다음과 같이 구성될 수 있다.

Window -> Grid -> StackPanel -> TextBlock

다른 UI도 같은 방식으로 연결된다.

Window -> Grid -> StackPanel -> Button

WPF에서 UI Tree가 중요한 이유는 단순히 화면의 계층 구조를 표현하기 때문이 아니다.

여러 WPF 기능이 이 Tree를 기준으로 동작한다.

UI Tree -> DataContext -> Resource -> Binding -> Routed Event

따라서 WPF에서 특정 Control의 동작이 예상과 다르다면 해당 Control만 보는 것이 아니라 UI Tree에서 어디에 위치해 있는지 같이 확인해야 한다.


DependencyObject

WPF의 주요 UI 객체들은 DependencyObject를 기반으로 한다.

WPF의 Property System을 이해하려면 이 구조를 알아둘 필요가 있다.

DependencyObject -> UIElement -> FrameworkElement -> Control

모든 WPF 객체가 동일한 계층을 가지는 것은 아니지만, 많은 UI 객체가 이 구조를 기반으로 만들어진다.

DependencyProperty 역시 이 Property System과 연결되어 있다.


DependencyProperty

WPF에서 일반적인 C# Property와 다른 핵심 개념이 DependencyProperty다.

대표적인 WPF 속성은 다음과 같다.

TextBox.Text
Button.IsEnabled
Control.Background
FrameworkElement.Width

이러한 Property들은 WPF의 Property System에 의해 관리된다.

DependencyProperty -> Binding
DependencyProperty -> Style
DependencyProperty -> Animation
DependencyProperty -> Trigger
DependencyProperty -> Property Inheritance

일반 C# Property가 단순히 값을 저장하고 반환하는 역할에 가깝다면 DependencyProperty는 WPF의 여러 시스템과 연결되는 Property다.

그래서 WPF의 UI Property를 단순한 변수처럼 생각하면 Binding이나 Style이 어떻게 동작하는지 이해하기 어려워진다.


DataContext

WPF에서 Binding을 이해하려면 DataContext가 중요하다.

DataContext는 현재 UI에서 Binding이 기본적으로 참조할 객체를 지정한다.

예를 들어 Window에 MainViewModel을 DataContext로 지정할 수 있다.

Window -> DataContext -> MainViewModel

ViewModel에 다음과 같은 Property가 있다고 하자.

public string UserName { get; set; }

XAML에서는 다음과 같이 사용할 수 있다.

<TextBlock Text="{Binding UserName}" />

Binding은 현재 UI의 DataContext를 기준으로 UserName을 찾는다.

DataContext -> MainViewModel -> UserName -> TextBlock.Text

DataContext는 UI Tree를 따라 전달될 수 있기 때문에 부모와 자식 관계가 매우 중요하다.


Data Binding

WPF의 핵심 기능 중 하나가 Data Binding이다.

UI와 데이터를 직접 연결하는 대신 Binding System을 통해 연결한다.

ViewModel.Property -> Binding -> UI.Property

예를 들어

<TextBlock Text="{Binding UserName}" />

이라고 하면 ViewModel의 UserName과 TextBlock의 Text가 연결된다.

Binding 방향은 크게 OneWay와 TwoWay로 생각할 수 있다.

OneWay -> ViewModel -> UI
TwoWay -> ViewModel <-> UI

입력창처럼 사용자가 값을 변경하는 UI에서는 TwoWay Binding을 사용할 수 있다.

<TextBox Text="{Binding UserName, Mode=TwoWay}" />

이렇게 하면 사용자가 입력한 값이 ViewModel로 전달될 수 있다.


INotifyPropertyChanged

Binding이 연결되어 있다고 해서 일반 C# Property의 변경을 WPF가 자동으로 감지하는 것은 아니다.

예를 들어

public string UserName { get; set; }

만 가지고 있다면 코드에서 UserName을 변경했을 때 Binding된 UI가 변경 사실을 알 수 있도록 별도의 알림이 필요하다.

이때 INotifyPropertyChanged를 사용한다.

전체 흐름은 다음과 같다.

ViewModel Property 변경 -> PropertyChanged -> Binding 감지 -> UI 갱신

MVVM에서 ViewModel이 INotifyPropertyChanged를 구현하는 이유도 여기에 있다.


MVVM

WPF에서는 MVVM(Model-View-ViewModel) 패턴을 많이 사용한다.

각 역할은 다음과 같이 볼 수 있다.

Model -> 데이터 / 도메인
View -> UI
ViewModel -> 화면 상태 / 사용자 동작

전체적인 구조는 다음과 같다.

View -> ViewModel -> Service -> Repository -> Data

View는 화면을 표현하고 ViewModel은 화면에서 필요한 상태와 사용자 동작을 관리한다.

Service나 Repository는 실제 데이터 접근과 외부 시스템 연동을 담당한다.

MVVM의 핵심은 파일을 세 개로 나누는 것이 아니다.

UI와 애플리케이션 로직의 결합을 줄이는 것이 핵심이다.


Command

버튼 클릭처럼 사용자의 행동을 ViewModel에서 처리할 때 Command를 사용할 수 있다.

<Button
    Content="Save"
    Command="{Binding SaveCommand}" />

전체 흐름은 다음과 같다.

Button -> Command -> ViewModel -> Business Logic -> Service -> Data

Code-behind에서 직접 Click 이벤트를 처리하는 방식과 비교하면 UI와 애플리케이션 로직을 분리하기 쉽다.

하지만 모든 이벤트를 무조건 Command로 만들어야 하는 것은 아니다.

View에만 의미가 있는 단순한 UI 동작이라면 Code-behind가 더 적절할 수도 있다.


Code-behind

XAML과 연결된 .xaml.cs 파일을 Code-behind라고 한다.

예를 들어

MainWindow.xaml -> MainWindow.xaml.cs

구조를 가진다.

Code-behind에서는 View에 종속적인 동작을 직접 처리할 수 있다.

Button Event -> Code-behind -> UI 처리

작은 프로그램에서는 이것만으로도 충분할 수 있다.

문제는 애플리케이션의 핵심 로직까지 Code-behind에 넣는 경우다.

Button Click -> HTTP 요청 -> DB 조회 -> Business Logic -> UI 갱신

이렇게 되면 View가 너무 많은 책임을 가지게 된다.

따라서 일반적인 애플리케이션에서는 다음과 같은 경계를 두는 것이 좋다.

View -> ViewModel -> Service -> Data

Resource

WPF의 Resource는 UI에서 공유할 객체를 정의하는 기능이다.

예를 들어 Brush를 Resource로 등록할 수 있다.

<Window.Resources>
    <SolidColorBrush
        x:Key="PrimaryBrush"
        Color="SteelBlue" />
</Window.Resources>

이후 UI에서 사용할 수 있다.

<Button
    Background="{StaticResource PrimaryBrush}"
    Content="Start" />

Resource는 여러 범위에서 정의할 수 있다.

Application.Resources -> Window.Resources -> Panel.Resources -> Control.Resources

필요한 Resource를 가까운 범위에서 찾고 없으면 상위 범위를 탐색하는 방식으로 이해하면 된다.


StaticResource와 DynamicResource

Resource를 사용할 때 StaticResourceDynamicResource를 사용할 수 있다.

StaticResource -> Resource 참조
DynamicResource -> 실행 중 Resource 변경에 대응하는 참조

일반적인 고정 Resource는 StaticResource로 충분한 경우가 많다.

Theme처럼 실행 중 Resource가 변경될 수 있는 구조에서는 DynamicResource가 필요할 수 있다.


Style

여러 Control에 동일한 속성을 적용하려면 Style을 사용할 수 있다.

<Style TargetType="Button">
    <Setter Property="Margin" Value="5" />
    <Setter Property="Padding" Value="10" />
</Style>

이 구조를 통해 공통 UI 규칙을 중앙에서 관리할 수 있다.

Resource -> Style -> Control

애플리케이션의 버튼, 입력창, TextBlock 등의 디자인 규칙이 많아질수록 Style의 가치가 커진다.


ControlTemplate

ControlTemplate은 Control의 시각적인 구조를 변경할 때 사용한다.

Style이 주로 Control의 속성을 설정한다면 ControlTemplate은 Control이 어떤 구조로 표현되는지를 변경한다.

Control -> ControlTemplate -> Visual Structure

예를 들어 Button의 기본 모양을 완전히 다른 UI로 바꿀 수 있다.

Button -> ControlTemplate -> Custom Button UI

Control의 기능과 시각적인 표현을 분리할 수 있다는 점이 WPF Template 시스템의 중요한 특징이다.


DataTemplate

DataTemplate은 데이터를 UI로 표현하는 방법을 정의한다.

예를 들어 User 목록이 있다면

Users -> ItemsSource -> DataTemplate -> User UI

형태로 구성할 수 있다.

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

각 User 객체가 DataTemplate을 통해 UI로 표현된다.

ControlTemplate과 헷갈리기 쉬운데 역할은 다르다.

ControlTemplate -> Control의 시각적 구조
DataTemplate -> Data를 표현하는 UI

ItemsControl

여러 데이터를 반복해서 UI로 표시할 때 ItemsControl 계열을 사용한다.

대표적인 Control은 다음과 같다.

ItemsControl -> ListBox -> ListView -> ComboBox

핵심 흐름은 다음과 같다.

Collection -> ItemsSource -> Item -> DataTemplate -> UI

WPF에서 목록 화면을 만들 때 자주 사용하게 되는 구조다.


Converter

ViewModel의 데이터와 UI에서 필요한 데이터 타입이 다를 때 IValueConverter를 사용할 수 있다.

예를 들어

bool -> Visibility

같은 변환이다.

ViewModel에는 bool IsVisible이 있지만 UI에서는 Visibility.Visible 또는 Visibility.Collapsed가 필요할 수 있다.

이때 다음과 같은 흐름을 사용한다.

ViewModel Value -> Converter -> UI Value

Converter는 단순한 표현 변환에 사용하는 것이 적절하다.

복잡한 비즈니스 로직을 Converter에 넣기 시작하면 View와 로직의 책임이 다시 섞이게 된다.


Routed Event

WPF에는 일반적인 C# Event와 다른 Routed Event가 있다.

UI Tree가 다음과 같은 구조라고 하자.

Window -> Grid -> Button

Button에서 발생한 이벤트는 부모 방향으로 전달될 수 있다.

Button -> Grid -> Window

이것이 Bubbling이다.

반대로 상위에서 하위로 전달되는 Tunneling도 있다.

Window -> Grid -> Button

따라서 WPF에서는 이벤트가 발생한 객체와 이벤트를 처리하는 객체가 다를 수 있다.


Layout

WPF의 UI 배치는 Layout System을 통해 처리된다.

대표적인 Panel은 다음과 같다.

Grid -> 행 / 열 기반 배치
StackPanel -> 순차 배치
DockPanel -> 가장자리 기준 배치
WrapPanel -> 공간에 따른 줄바꿈 배치
Canvas -> 좌표 기반 배치

WPF Layout의 기본 흐름은 다음과 같다.

Measure -> Arrange -> Render

부모가 자식에게 사용할 수 있는 공간을 전달하고 자식의 크기를 계산한 뒤 최종 위치와 크기를 배치한다.


Measure와 Arrange

WPF Layout 문제를 이해하려면 MeasureArrange를 알아둘 필요가 있다.

전체 흐름은 다음과 같다.

Parent -> Measure -> Child Size 계산 -> Arrange -> Child 위치 / 크기 결정 -> Render

복잡한 UI에서 Layout 계산이 과도하게 발생하면 성능에 영향을 줄 수 있다.

따라서 WPF 성능 문제를 볼 때는 단순히 Control 개수만 확인할 것이 아니라 Layout 비용도 확인해야 한다.


Dispatcher

WPF의 UI 객체는 기본적으로 UI Thread에서 접근해야 한다.

백그라운드 Thread에서 UI를 직접 변경하면 문제가 발생할 수 있다.

Worker Thread -> Dispatcher -> UI Thread -> UI Update

Dispatcher는 특정 작업을 UI Thread에서 실행할 수 있도록 연결한다.

다만 MVVM 구조에서는 UI를 직접 변경하기보다 ViewModel의 상태를 변경하고 Binding을 통해 UI를 갱신하는 방식이 더 자연스럽다.

Background Task -> ViewModel State 변경 -> PropertyChanged -> Binding -> UI Update

비동기 처리

WPF는 UI Thread 기반의 데스크톱 애플리케이션이기 때문에 UI Thread를 오래 점유하면 화면이 멈춘다.

잘못된 구조는 다음과 같다.

UI Thread -> Heavy Work -> 완료 대기 -> UI 멈춤

네트워크 요청이나 파일 I/O처럼 기다리는 작업은 async/await를 사용해 UI Thread를 장시간 점유하지 않도록 구성할 수 있다.

UI -> ViewModel -> Service -> await -> Result -> PropertyChanged -> UI Update

중요한 것은 async 키워드를 붙이는 것이 아니다.

UI Thread를 오래 점유하지 않는 구조를 만드는 것이 핵심이다.


Dependency Injection

프로젝트가 커지면 ViewModel과 Service 사이에 의존성이 증가한다.

ViewModel -> UserService -> ApiClient

객체를 클래스 내부에서 직접 생성하면 결합도가 높아진다.

Dependency Injection을 사용하면 객체의 생성과 의존성 연결을 외부에서 관리할 수 있다.

DI Container -> ApiClient -> UserService -> ViewModel

ViewModel은 생성자를 통해 필요한 Service를 전달받는다.

public MainViewModel(UserService userService)
{
    _userService = userService;
}

이 구조는 테스트에서 실제 Service 대신 Mock이나 Fake 객체를 전달하기도 쉽다.


WPF의 렌더링

WPF는 Windows의 그래픽 시스템을 이용해 UI를 렌더링한다.

개발자가 내부 렌더링 과정을 직접 제어할 필요는 없지만 기본적인 흐름은 알아두면 좋다.

XAML / UI Object -> Layout -> Render -> Composition -> 화면

Layout은 다시

Measure -> Arrange

과정을 거친다.

따라서 복잡한 UI를 만들 때는 UI 구조 자체뿐만 아니라 Layout과 Rendering 비용도 고려해야 한다.


WPF 프로젝트 구조

실제 프로젝트에서는 역할에 따라 코드를 분리할 수 있다.

View -> UI
ViewModel -> 화면 상태 / 사용자 동작
Model -> 데이터 / 도메인
Service -> 애플리케이션 기능 / 외부 시스템 연동
Repository -> 데이터 접근
Resources -> Style / Theme / Shared Resource

전체적인 구조를 하나로 묶으면 다음과 같다.

View -> ViewModel -> Service -> Repository -> Database / API

프로젝트 규모가 커지면 기능 단위로 분리할 수도 있다.

Application -> Feature -> View -> ViewModel -> Service -> Data

폴더 구조 자체보다 중요한 것은 각 계층의 책임을 명확하게 유지하는 것이다.


WPF에서 자주 발생하는 문제

Binding이 동작하지 않는다

다음 흐름을 확인한다.

DataContext -> Binding Path -> Property -> PropertyChanged -> UI

이 중 하나가 잘못되어 있을 가능성이 높다.

UI가 갱신되지 않는다

Property 변경 -> PropertyChanged -> Binding -> UI Update

이 흐름이 정상적으로 연결되어 있는지 확인한다.

UI가 멈춘다

UI Thread -> 오래 걸리는 작업 -> UI 응답 지연

구조인지 확인한다.

Style이 적용되지 않는다

Resource Scope -> Resource Key -> Resource 참조 -> Style -> Control

순서로 확인한다.

목록 UI가 이상하다

ItemsSource -> Item -> DataTemplate -> Binding -> UI

구조를 확인한다.


WPF 전체 흐름

지금까지의 내용을 하나로 묶으면 WPF 애플리케이션은 다음과 같은 흐름으로 이해할 수 있다.

XAML -> View -> DataContext -> Binding -> ViewModel -> Service -> Repository -> Data

사용자의 입력은 반대로 들어온다.

User Input -> View -> Command / Event -> ViewModel -> Business Logic -> Service -> Data

데이터가 변경되면 다시 UI로 전달된다.

Data -> ViewModel -> PropertyChanged -> Binding -> View -> UI Update

UI가 생성되고 화면에 표시되는 과정은 다음과 같다.

XAML -> UI Object -> UI Tree -> Measure -> Arrange -> Render -> 화면

WPF의 Property System은 별도의 축으로 연결된다.

DependencyProperty -> Binding / Style / Animation / Trigger / Property Inheritance

Resource와 Style은 UI를 공통화한다.

Resource -> Style -> Control

데이터를 UI로 표현하는 과정은 다음과 같다.

Collection -> ItemsSource -> DataTemplate -> UI

사용자 동작과 애플리케이션 로직은 다음과 같이 연결된다.

User Input -> Command -> ViewModel -> Service -> Data

정리

WPF는 단순히 Windows 화면을 만드는 UI 프레임워크가 아니다.

XAML을 통해 UI를 선언하고 UI Tree를 구성한 뒤 DataContext와 Binding을 통해 ViewModel과 연결한다.

ViewModel은 화면의 상태와 사용자 동작을 관리하고 Service와 Repository를 통해 실제 애플리케이션 로직과 데이터를 연결한다.

전체적인 구조는 다음과 같다.

XAML -> View -> DataContext -> Binding -> ViewModel -> Service -> Data

사용자의 입력은

User Input -> View -> Command / Event -> ViewModel -> Logic

으로 들어간다.

상태가 변경되면

ViewModel -> PropertyChanged -> Binding -> View -> UI Update

로 다시 화면에 반영된다.

WPF의 주요 기능도 이 구조 안에서 보면 역할이 명확해진다.

DependencyProperty -> WPF Property System
DataContext -> Binding의 기준 객체
Binding -> UI와 데이터 연결
INotifyPropertyChanged -> 상태 변경 알림
Command -> 사용자 동작과 ViewModel 연결
Resource -> 공유 객체
Style -> Control 공통 속성
DataTemplate -> 데이터를 UI로 표현
ControlTemplate -> Control의 시각적 구조 변경
Routed Event -> UI Tree 기반 이벤트 전달
Dispatcher -> UI Thread 접근

WPF를 공부할 때 모든 Control과 XAML 문법을 외우는 것보다 먼저 UI Tree, DataContext, Binding, DependencyProperty, MVVM의 관계를 이해하는 것이 중요하다.

결국 WPF 개발의 핵심은 다음 흐름으로 압축할 수 있다.

UI -> Binding -> State -> Logic -> Data

그리고 데이터가 변경되면 다시

Data -> State -> Binding -> UI

로 돌아온다.

WPF의 수많은 기능은 결국 이 흐름을 구성하고 확장하기 위한 도구다.