Avalonia: 크로스 플랫폼 UI 작업을 위한 오픈 소스 옵션 | David Eastman

본문을 DeepL로 기계 번역 후 확인 및 수정하였음


Avalonia는 크로스 플랫폼 UI 작업을 위한 오픈 소스 프레임워크입니다. 일반적으로 여러 기기에서 작동하는 .NET 앱을 빌드하는 데 사용됩니다.

.NET의 문제점은 항상 Microsoft가 Windows 이외의 시스템을 수용하는 데 다소 느리다는 것이었습니다. C#을 사용하지만 MacBook에서 실행되는 UI 라이브러리를 찾는 것은 생각보다 어렵습니다. 한 가지 해결책은 “.NET을 사용하여 모든 디바이스에 맞는 앱을 빌드”할 수 있도록 도와주는 Avalonia이며, 실제로 오픈 소스 크로스 플랫폼 프레임워크의 공백을 메워줍니다. 또한 이 도구를 사용하면 프레임워크 디자인 기술도 연마할 수 있습니다.

시작하기

이제 아키텍처를 살펴보면서 간단한 코드를 작성하여 Avalonia를 시작해 보겠습니다. Visual Studio Code와 .NET이 설치되어 있다고 가정하겠습니다. 이전 여러 게시물에서 VS Code 사용에 대해 다룬 적이 있는데, 그 유연성 덕분에 다양한 프로젝트에 뛰어들기에 이상적입니다(실제로 Avalonia 자체에서는 JetBrains의 .NET IDE인 Rider를 사용할 것을 권장합니다).

명령줄에서 닷넷을 사용하면 간단한 프로젝트를 빠르게 만들 수 있습니다. 먼저 Avalonia 템플릿을 설치합니다:

새 프로젝트 디렉터리에서는 MacOS에서도 작동하는 MVVM 템플릿을 사용합니다:

그런 다음 항상 그렇듯이 폴더에 Visual Studio Code가 열립니다:

Avalonia 확장 프로그램을 검색하세요. 첫 번째 확장 프로그램을 원하실 겁니다:

Program.cs 파일을 선택하고 화살표를 클릭하여 “이 파일과 연결된 프로그램 실행”을 클릭하면 Avalonia가 즉시 창을 생성합니다:

괜찮습니다. 이제 기본 사항을 살펴본 다음 좀 더 흥미로운 작업을 해보겠습니다.

Windows 프레젠테이션 파운데이션(또는 WPF)에 익숙하신 분이라면 확장 가능한 애플리케이션 마크업 언어(XAML)를 보셨을 것이고 Avalonia는 자체 브랜드인 “axaml” 파일 확장자를 사용합니다. 또한 “코드 뒤에 있는” 파일을 표시하기 위해 C# 확장자를 추가한다는 사실도 알 수 있습니다. 조금 지저분하더라도 이 모든 것이 작동합니다. 네, 바로 XML입니다.

다행히도 몇 가지 흥미로운 파일이 있습니다. 창은 “MainWindow.axaml”에 정의되어 있으며, 제목이 정의되어 있는 것을 볼 수 있습니다:

또 하나 흥미로운 점은 TextBlock 정의입니다.

<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

화면 중앙에 위치하는 것 외에도, 앞서 살펴본 것처럼 데이터 바인딩을 도입했는데, 이는 생각보다 약간 까다로운 문제입니다. 분명히 Greeting이라는 용어가 변수에 바인딩되어 있다는 것을 알 수 있지만 어디에 바인딩되어 있을까요?

상용구 파일 중 하나인 “App.axaml.cs”를 살펴보면 시작 시 설정되는 DataContext라는 항목이 있음을 알 수 있습니다:

...
desktop.MainWindow = new MainWindow { 
  DataContext = new MainWindowViewModel(), 
};
...

이는 프레임워크가 바인딩을 시작할 때 이 새 모델 클래스를 사용한다는 것을 나타냅니다. 그리고 “MainWindowViewModel.cs”를 살펴보면 대략 우리가 예상했던 것을 찾을 수 있습니다:

namespace avaloniaui.ViewModels; 
 
public partial class MainWindowViewModel : ViewModelBase { 
  public string Greeting { get; } = "Welcome to Avalonia!"; 
}

따라서 TextBlock은 클래스 변수에 바인딩되었습니다. IDE는 주로 빌드 후에 이를 협상하는 데 도움이 됩니다.

자, 이제 어려운 부분으로 넘어가서 약간의 UI를 살펴봅시다. 간단한 카테고리 항목을 선택하고 다른 목록을 예제로 채울 수 있는 목록 상자를 만들어 보겠습니다. 그래서 약간의 UI 디자인을 살펴보고 몇 가지 이벤트를 다뤄보겠습니다.

MainWindow.axaml 파일로 돌아가서 단독 텍스트 블록을 이것으로 대체합니다:

<StackPanel Orientation="Horizontal">       
  <TextBlock Text="{Binding Greeting}" FontSize="20" FontWeight="Bold" Margin="20 10"/>
  <StackPanel Margin="20 25">
    <TextBlock Margin="0 5" Background="LightBlue" DockPanel.Dock="Top">Choose a category</TextBlock>
    <ListBox x:Name="category" SelectionChanged="CategoryChanged" Margin="1" SelectionMode="Single,AlwaysSelected">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding}"/>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </StackPanel>
 
  <StackPanel Margin="20 25">
    <TextBlock Margin="0 5" Background="LightBlue" DockPanel.Dock="Top">Examples</TextBlock>
    <ListBox x:Name="resultlist">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding}"/>
          </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
  </StackPanel>
</StackPanel>

보시죠. 태그만 보면 전체 컨테이너로 StackPanel을 사용하고 있으며 내부에는 두 개의 StackPanel이 있습니다. 바깥쪽 StackPanel을 가로로 정의했으므로 안쪽 StackPanel은 서로 나란히 배치됩니다. 두 스택 모두에 ListBox를 정의합니다. 한 가지 동작의 차이점은 첫 번째의 SelectionMode가 항상 무언가를 선택하도록 강제한다는 것입니다.

데이터 관점에서 첫 번째 목록 상자의 이름을 “category”로 지정하고 두 번째 목록 상자의 이름을 나중에 관련성이 있는 “resultList”으로 지정했습니다. 그리고 각 스택 내의 텍스트 블록에 바인딩이 있는 것을 볼 수 있습니다. 그래서 우리는 그 안에 우리만의 문자열을 넣을 것입니다. 물론 지금은 데이터가 없습니다.

이벤트 측면에서는 첫 번째 스택에서 “SelectionChanged”를 감지하고 있습니다. 이에 대응해야 합니다. 실제로 이를 실행하면 Avalonia에서 “SelectionChanged 속성에 적합한 setter 또는 adder를 찾을 수 없습니다.”라는 메시지가 표시됩니다.

이제 이벤트를 수정해 보겠습니다. MainWindow 클래스 안에 메서드를 추가합니다:

using System; 
... 
public void CategoryChanged(object source, SelectionChangedEventArgs args) { 
  if (source is ListBox listBox) { 
   Console.WriteLine($"Category changed to {listBox.SelectedItem}"); 
  } 
}

이제 프레임워크를 다시 실행할 수 있으며 스택 열 텍스트 제목도 가져올 수 있습니다:

데이터를 추가해 보겠습니다. 실제로는 MainWindow 클래스에서 작업할 수 있습니다. 몇 가지 데이터를 추가하고 이를 사용하여 첫 번째 ListBox를 채우고 카테고리 ListBox의 ItemSource로 표시하겠습니다:

using System.Collections.Generic;
...
 
private Dictionary<string, List<string>> catgeoryDict = new Dictionary<string, List<string>>()
{
    {"Trees", new List<string>(){"Larch", "Oak", "Pine", "Willow"}},
    {"Birds", new List<string>(){"Eagle", "Hawk", "Owl", "Raven"}},
    {"Mammals", new List<string>(){ "Bear", "Deer", "Fox", "Wolf"}},
    {"Snakes", new List<string>(){"Cobra", "Python", "Rattlesnake", "Viper"}},
    {"Insects", new List<string>(){"Ant", "Bee", "Fly", "Wasp"}}
};
public MainWindow()
{
    InitializeComponent();
    category.ItemsSource = new List<string>(catgeoryDict.Keys);
}

이렇게 하면 멋진 카테고리 목록 상자가 생깁니다. 이제 절반은 끝났습니다.

이제 사용자가 선택 항목을 누르면 데이터의 예시로 올바른 목록을 채워서 카테고리 변경에 응답하기만 하면 됩니다. 이벤트 응답 메서드에 한 줄만 추가하면 됩니다:

public void CategoryChanged(object source, SelectionChangedEventArgs args)
{
  if (source is ListBox listBox)
  {
    Console.WriteLine($"Category changed to {listBox.SelectedItem}");
    resultlist.ItemsSource = catgeoryDict[(string)listBox.SelectedItem].ToList();
  }
}

이 경우 SelectedItem은 문자열이므로 이를 카테고리 사전의 인덱스로 사용하여 예제를 찾을 수 있습니다. 이제 끝났습니다. 카테고리를 선택하고 그 결과를 다음 목록 상자에 넣으면 됩니다:

마무리

그다지 고통스럽지는 않았지만 그 이상의 작업을 하려면 ViewModel을 올바르게 사용해야 했습니다. 데이터를 표시하는 데 더 강력한 컴포넌트인 DataGrid를 사용해 보았지만 훨씬 더 까다로웠습니다.

간결하게 설명하기 위해 이 간단한 예제에서도 설명하지 않은 부분이 상당히 많으며, 다른 곳에서 가져온 디자인 부담이 상당히 많은 것처럼 느껴집니다. 그럼에도 불구하고 크로스 플랫폼 UI 작업을 위한 오픈 소스 옵션으로서 그 목적을 성공적으로 수행하고 있습니다.

2개의 좋아요