이 Post 는 WPF 에서 UI 스레드에 안전하게 접근하는 방법을 알아보고 Invoke 와 BeginInvoke 의 차이를 알아봅니다.
WPF 로 개발을 할 때 스레드에서 UI 에 접근할려고 하면 다음과 같은 오류를 만납니다.

외부에서 생성된 스레드에서 UI 스레드를 제어하려고 하면 발생하는 오류 입니다.
해결책은 다양한 방식이 있지만 대부분 Dispatcher 를 사용하게 됩니다.
창 전체 혹은 UserControl 전체에 대해서 제어를 할려면 보통 해당 창에서 this.Dispatcher 를 사용합니다.
특정 컨트롤만 제어 하기 위해선 controlName.Dispatcher 를 사용하면 됩니다.
혹은 접두 없이 DIspatcher 를 사용하기도 하는데 이는 현재 스레드를 뜻합니다. 즉 this.Dispatcher 와 동일하다고 보시면 됩니다.
다음과 같이 정리하면 될 것 같습니다.
방식 | 동작 | 사용 권장 상황 |
---|---|---|
this.Dispatcher | 현재 Window 가 속한 UI 스레드의 Dispatcher 를 사용합니다. | 현재 창의 UI 스레드에서 작업을 예약할 때 |
textBox.Dispatcher | TextBox 가 생성된 스레드의 Dispatcher 를 사용합니다. | 특정 컨트롤의 스레드에 작업을 전달할 때 |
Dispatcher | 현재 스레드의 Dispatcher 를 사용합니다. | 위험: 백그라운드 스레드에서 호출 시 실패 |
Application.Current.Dispatcher | 메인 UI 스레드의 Dispatcher 를 명시적으로 사용합니다. | 컨트롤 없이 UI 스레드에 안전하게 접근할 때 |
Dispatcher 이후에는 Invoke 혹은 BeginInvoke 를 사용하게 되는데요, 그 차이는 다음과 같습니다.
테스트를 위한 간단한 코드를 작성 했습니다.
1. MainWindow.xaml
<Window x:Class="BlogDemo00.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:BlogDemo00"
mc:Ignorable="d"
Title="MainWindow" Height="80" Width="200">
<Grid>
<TextBlock x:Name="txtMain" Text=""
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="32"
Foreground="DarkBlue"
FontWeight="Bold"/>
</Grid>
</Window>
2. MainWIndow.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;
namespace BlogDemo00
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task task = new Task(worker);
task.Start();
Console.WriteLine("작업이 시작되었습니다. UI는 계속 응답합니다.");
}
private void worker()
{
bool? IsSync = null; // 동기 실행 여부
Console.WriteLine($"IsSync: {IsSync}");
for (int i = 0; i < 3; i++)
{
if (IsSync == null)
{
txtMain.Text = $"Count: {i}";
System.Threading.Thread.Sleep(10);
Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
}
else if (IsSync.Value == true)
{
// 동기 실행: UI 스레드에서 실행
Dispatcher.Invoke(() =>
{
txtMain.Text = $"Count: {i}";
System.Threading.Thread.Sleep(10);
Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
});
}
else if (IsSync.Value == false)
{
// 비동기 실행: UI 스레드에서 실행
Dispatcher.BeginInvoke(new Action(() =>
{
txtMain.Text = $"Count: {i}";
System.Threading.Thread.Sleep(10);
Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
}));
}
// 1초 대기
Console.WriteLine($"Count: {i} - 백그라운드 스레드에서 실행됨");
System.Threading.Thread.Sleep(10);
}
Console.WriteLine("작업이 완료되었습니다. UI는 계속 응답합니다.");
}
}
}
코드를 보시면 아시겠지만 Invoke 는 동기, BeginInvoke 는 비동기로 내부 코드를 실행합니다.
실행 결과 확인 하겠습니다.
1. Invoke (동기)

예상 하셨듯이 정상적으로 실행 됩니다.
2. BeginInvoke (비동기)

위에 코드를 보면 아시겠지만 i < 3 이기 때문에 “Count: 3” 이 원래 대로라면 나올 수가 없습니다.
또한 for 문을 다 돌고 난 후에 출력한 “작업이 완료되었습니다…..” 이후에 “Count: 3” 이 찍혔네요.
즉, 마지막 구분은 for 문을 다 돌고 난 후에 진입하여 실행이 되었단 말입니다.
이와 같이 BeginInvoke 는 비동기로 동작 하기 때문에 사용할 때는 순서가 필요 없는 작업일 때만 써야 하는 주의가 필요 합니다.