UI는 컬렉션의 추가/삭제를 즉시 반영해야 합니다. C#의 ObservableCollection<T>는 변경 알림(INotifyCollectionChanged)을 제공하여 WPF/WinUI/MAUI 바인딩에서 자동으로 UI를 갱신합니다. List<T>는 알림이 없어 바인딩에 적합하지 않습니다.
1. ObservableCollection이 필요한 이유
ObservableCollection은 Add/Remove/Move/Replace/Reset 등의 변경을 INotifyCollectionChanged로 알립니다. UI 프레임워크는 이 이벤트를 구독해 화면을 업데이트합니다.
2. WPF 기본 바인딩 예제
ViewModel에 ObservableCollection을 노출하고 XAML에서 ItemsSource로 바인딩합니다.
using System.Collections.ObjectModel;
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set { if (_name != value) { _name = value; OnPropertyChanged(nameof(Name)); } }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public class MainViewModel
{
public ObservableCollection<Person> People { get; } = new()
{
new Person { Name = "Alice" },
new Person { Name = "Bob" }
};
public void AddPerson(string name) => People.Add(new Person { Name = name });
public void RemovePerson(Person p)
{
if (p != null) People.Remove(p);
}
}
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Demo">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<StackPanel Margin="12">
<ListBox ItemsSource="{Binding People}" DisplayMemberPath="Name" x:Name="PeopleList" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBox x:Name="NameBox" Width="140" />
<Button Content="추가" Margin="8,0,0,0" Click="Add_Click" />
</StackPanel>
</StackPanel>
</Window>
// code-behind
private void Add_Click(object sender, System.Windows.RoutedEventArgs e)
{
var vm = (MainViewModel)DataContext;
vm.AddPerson(NameBox.Text);
}
People에 Add/Remove를 호출하면 ListBox가 자동으로 갱신됩니다.
3. 아이템 속성 변경 반영
컬렉션 내부 아이템의 속성 변경은 INotifyPropertyChanged로 반영합니다. 위 Person처럼 속성 변경 시 OnPropertyChanged를 호출해야 UI가 갱신됩니다.
4. 스레드/비동기 업데이트
ObservableCollection은 UI 스레드에서 수정해야 합니다. 백그라운드 작업에서 추가하려면 Dispatcher를 사용합니다.
using System.Threading.Tasks;
using System.Windows;
Task.Run(() =>
{
var item = new Person { Name = "Eve" };
Application.Current.Dispatcher.Invoke(() =>
{
// UI 스레드에서 수정
((MainViewModel)Application.Current.MainWindow.DataContext)
.People.Add(item);
});
});
WPF .NET 4.5+에서는 EnableCollectionSynchronization로 스레드 간 동기화를 허용할 수 있습니다.
using System.Windows.Data;
public class MainViewModel
{
private readonly object _lock = new();
public ObservableCollection<Person> People { get; } = new();
public MainViewModel()
{
BindingOperations.EnableCollectionSynchronization(People, _lock);
}
public void AddFromAnyThread(Person p)
{
lock (_lock)
{
People.Add(p);
}
}
}
WinUI/MAUI도 개념은 동일하며, 주로 메인 스레드 디스패처(Dispatcher/MainThread)를 사용합니다.
5. 대량 변경 최적화(AddRange)
많은 항목을 한 번에 추가하면 항목마다 이벤트가 발생해 느려집니다. Reset 한 번만 발생시키는 RangeObservableCollection을 사용합니다.
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using System.ComponentModel;
public class RangeObservableCollection<T> : ObservableCollection<T>
{
private bool _suppress;
public void AddRange(IEnumerable<T> items)
{
if (items == null) return;
_suppress = true;
foreach (var item in items) Items.Add(item);
_suppress = false;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppress) base.OnCollectionChanged(e);
}
}
UI는 전체 리프레시를 수행하므로, 가상화가 있는 컨트롤을 쓰면 대량 변경에도 부하가 줄어듭니다.
6. 읽기 전용으로 노출
외부 수정 차단을 위해 내부 컬렉션을 감싸 ReadOnlyObservableCollection으로 노출합니다.
private readonly ObservableCollection<Person> _people = new();
public ReadOnlyObservableCollection<Person> People { get; }
public MainViewModel()
{
People = new ReadOnlyObservableCollection<Person>(_people);
}
public void AddPerson(string name) => _people.Add(new Person { Name = name });
7. 정렬/필터링과 CollectionView
CollectionViewSource를 사용하면 UI 갱신은 유지하면서 정렬/필터를 적용할 수 있습니다.
using System.ComponentModel;
using System.Windows.Data;
var view = CollectionViewSource.GetDefaultView(viewModel.People);
using (view.DeferRefresh())
{
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(nameof(Person.Name), ListSortDirection.Ascending));
view.Filter = o => ((Person)o).Name.StartsWith("A", StringComparison.CurrentCultureIgnoreCase);
}
8. 체크리스트
- List<T> 대신 ObservableCollection<T>를 바인딩에 사용합니다.
- 아이템 클래스는 INotifyPropertyChanged를 구현합니다.
- 백그라운드 스레드에서 수정 시 Dispatcher 또는 EnableCollectionSynchronization을 사용합니다.
- 대량 변경은 AddRange(Reset) 패턴으로 최적화합니다.
- 외부에는 ReadOnlyObservableCollection으로 노출해 캡슐화를 지킵니다.
- 정렬/필터는 CollectionView로 처리합니다.
이 패턴을 지키면 UI 바인딩이 깔끔하고, 성능도 안정적으로 유지됩니다.
'C#' 카테고리의 다른 글
| C# IComparable과 IComparer를 이용한 정렬 로직 최적화 (0) | 2026.05.28 |
|---|---|
| C# ThreadStatic vs AsyncLocal 차이와 활용 (0) | 2026.05.28 |
| C# KeyedCollection<T>로 키 기반 컬렉션 설계 (0) | 2026.05.27 |
| C# 정적 메서드와 인스턴스 메서드 호출 성능 비교 (0) | 2026.05.27 |
| C# Stopwatch 대신 BenchmarkTimer 만들기 (0) | 2026.05.27 |