C# 코드로 UI 라이브러리를 다시 만들기 시작했습니다

안녕하세요.

오랜만에 하나의 프로젝트를 잡고 꾸준히 진행하고 있어, 그 과정을 기록해보려고 합니다.

프로젝트의 이름은 Nuri입니다.

Nuri는 WPF, WinForms, MAUI, WinUI 3, Avalonia, Uno Platform, OpenSilver, MewUI와 같은 새로운 UI 프레임워크를 만드는 프로젝트는 아닙니다.

화면을 그리고, 윈도우를 생성하고, GPU나 Skia를 통해 실제 결과물을 표현하는 역할은 기존 프레임워크가 그대로 담당합니다.

Nuri는 그 위에서 UI를 작성하고 상태를 관리하는 새로운 방식을 제공하는 별도의 UI 라이브러리입니다.

개발자는 Nuri의 방식으로 UI를 선언하고, Nuri는 이를 각 프레임워크가 이해할 수 있는 실제 컨트롤로 변환합니다.

즉, 기존 프레임워크를 대체하는 것이 아니라 기존 렌더링 환경을 활용하면서 UI를 구성하는 방식을 새롭게 만들어보는 프로젝트라고 생각하시면 됩니다.

예전부터 C# 코드만으로 UI를 구성하는 방식을 직접 만들어보고 싶었습니다.

현재의 Nuri를 시작하기 전에도 비슷한 아이디어를 검증하기 위해 Delta라는 프로젝트를 개발한 적이 있습니다.

Delta는 WPF에서 MVU 패턴과 useState 형태의 상태 관리, Fluent 방식의 선언형 UI를 구현해보기 위한 초기 실험 프로젝트였습니다.

이를 통해 C# 코드만으로 UI를 선언하고, 상태 변경에 따라 변경된 부분만 실제 컨트롤에 반영하는 방식이 가능한지 확인해볼 수 있었습니다.

하지만 상태 변경, 화면 갱신, 컴포넌트 생명주기, 렌더링 성능까지 본격적으로 다루기 시작하면서 구조적인 한계에 도달했습니다.

결국 혼자 구현하고 검증할 수 있는 범위를 넘어섰다고 느꼈고, 더 나아가지 못한 채 멈춰 있었습니다.

현재의 Nuri는 Delta를 그대로 이어서 확장한 프로젝트는 아닙니다.

Delta를 개발하며 얻은 경험과 당시 해결하지 못했던 문제들을 바탕으로 구조를 다시 설계하고, 상태 관리와 렌더링, 개발 도구까지 새롭게 구현하고 있습니다.

그러다 최근 Codex를 본격적으로 도입하면서, 예전에 막혀 있던 아이디어를 다시 꺼내볼 수 있겠다는 생각이 들었습니다.

그동안 C#과 UI 개발을 하며 쌓아온 경험을 바탕으로 방향과 구조를 설계하고, Codex와 함께 구현과 검증을 반복하며 하나의 라이브러리로 구체화하고 있습니다.

아직 완성된 프로젝트는 아닙니다.

이 글에서는 완성된 결과물만 소개하기보다 Nuri를 만들면서 어떤 문제를 만났고, 어떤 판단을 했으며, 구조가 어떻게 변해가는지를 개발 로그 형식으로 남겨보려고 합니다.

6 Likes

Nuri의 기본 기능: 함수 안에서 UI와 상태를 함께 작성하기

Nuri는 React의 함수형 컴포넌트와 Hooks에서 아이디어를 가져와 C# 방식으로 구현하고 있습니다.

하나의 컴포넌트 안에서 UI와 해당 UI가 사용하는 상태를 함께 작성할 수 있습니다.

public class CounterComponent : Component
{
    public override IElement Render()
    {
        var (count, setCount) = useState (0);
        return Grid (
                Text (count.ToString ())
                    .Row(0),
                Button ("증가", () => setCount (count + 1))
                    .Row(1)
            ).Rows(Auto, Pixels(50));
    }
}

기존 방식처럼 간단한 상태 하나를 추가하기 위해 ViewModel에 속성을 만들고, 변경 알림과 Command를 연결하지 않아도 됩니다.

해당 UI에서만 사용하는 상태라면 함수 내부에 바로 선언할 수 있고, 필요 없어졌을 때도 쉽게 제거할 수 있습니다.

이 구조의 가장 큰 장점은 UI와 상태의 관계를 한곳에서 확인할 수 있다는 점입니다.

화면을 수정할 때 여러 파일을 오가기보다, 하나의 컴포넌트 안에서 상태와 동작을 함께 살펴볼 수 있습니다.

Nuri는 이런 방식으로 작은 컴포넌트를 빠르게 작성하고, 필요해졌을 때 더 작은 단위로 분리하는 개발 흐름을 목표로 하고 있습니다.

3 Likes

dotnet watch 를 이용한 빠른 UI 수정

Nuri는 UI를 C# 코드로 작성하기 때문에 dotnet watch 를 활용할 수 있습니다.

애플리케이션을 실행한 상태에서 텍스트, 여백, 배치, 색상과 같은 UI 코드를 수정하면 매번 프로그램을 완전히 종료하고 다시 실행하는 과정을 줄일 수 있습니다.

특히 상태값도 컴포넌트 함수 내부에 작성되기 때문에, 간단한 상태를 추가하거나 제거하는 작업 역시 비교적 부담 없이 진행할 수 있습니다.

public override IElement Render()
{
    var (count, setCount) = useState(0);

    return Div(
        Text($"Count: {count}"),
        Button("증가", () => setCount(count + 1))
    );
}

예를 들어 버튼 하나를 추가하거나, 상태값을 하나 더 선언하거나, 화면 구성을 조금 변경하는 작업을 실행 중인 화면을 확인하면서 반복할 수 있습니다.

Nuri는 dotnet watch 를 통해 UI 코드를 수정하고 바로 결과를 확인하는 빠른 개발 흐름을 목표로 하고 있습니다.

2026-07-10-134725_uzFpsna7

2 Likes

DevTools 지원

Nuri에는 실행 중인 UI의 내부 구조를 확인할 수 있는 별도의 DevTools도 추가했습니다.

UI에 문제가 생겼을 때 단순히 결과 화면만 보는 것이 아니라, 현재 선택한 컨트롤이 화면 구조상 어느 위치에 있는지 확인하고 싶었습니다.

또한 해당 컨트롤이 어떤 컴포넌트에서 만들어졌는지, 그 컴포넌트가 가지고 있는 Hook의 상태값은 무엇인지도 함께 살펴볼 수 있으면 좋겠다고 생각했습니다.

데스크톱 애플리케이션에서는 디버그 로그를 확인하는 과정도 다소 불편할 때가 있어, 실행 중 발생하는 로그 역시 DevTools 안에서 한곳에 모아 확인할 수 있도록 만들고 있습니다.

생각해보면 브라우저에서 개발자 도구를 열어 DOM 구조와 상태, 로그를 자연스럽게 확인하던 웹 개발 환경이 조금 부러웠던 것 같습니다.

Nuri의 DevTools는 그런 경험을 데스크톱 UI 개발에서도 제공해보려는 시도입니다.

2026-07-10-140148_mwLvOdSU

4 Likes

Preview 기능

dotnet watch를 이용하면 애플리케이션을 실행한 상태에서 코드를 수정하고 결과를 빠르게 확인할 수 있습니다.

하지만 사용하다 보니 한 가지 아쉬움이 생겼습니다.

“꼭 애플리케이션을 실행해야만 할까?”

XAML 디자이너처럼 코드 에디터에서 UI를 작성하면서, 실행하지 않은 상태에서도 바로 화면을 확인할 수 있으면 좋겠다는 생각이 들었습니다.

그 고민 끝에 Nuri에도 Preview 기능을 추가해보았습니다.

Visual Studio의 Tool Window에서 원하는 컴포넌트를 미리 렌더링하고, 코드를 수정하면서 결과를 확인할 수 있도록 하는 기능입니다.

미리 보기 화면에서 특정 UI 요소를 선택하면 해당 요소가 어느 코드에서 작성되었는지도 확인할 수 있도록 연결하고 있습니다.

아직 XAML 디자이너처럼 완성된 형태는 아니지만, 애플리케이션 전체를 실행하지 않고도 컴포넌트 단위로 UI를 확인하고 수정할 수 있는 개발 환경을 목표로 하고 있습니다.

2026-07-10-140706_mgSP7Ryt

4 Likes

그럼 디자인 테마를 적용하려면 Native 연동을 해야겠네?

Nuri가 기존 UI 프레임워크의 컨트롤을 그대로 사용한다면, 디자인 테마는 어떻게 적용해야 할지도 고민하게 되었습니다.

처음에는 Windows 11 스타일의 UI를 표현하려면 WinUI 3와 같은 Native UI를 별도로 연동하거나, Nuri에서 컨트롤별 스타일을 직접 만들어야 하지 않을까 생각했습니다.

하지만 현재 Nuri가 우선 지원하는 WPF에서는 .NET 9부터 Fluent 테마를 공식적으로 사용할 수 있었습니다.

기존 WPF의 Button, TextBox, CheckBox 같은 컨트롤은 그대로 사용하면서, Fluent 테마를 선언하면 Windows 11과 유사한 디자인이 기본 컨트롤에 적용됩니다.

따라서 Nuri에서 별도의 Windows 11 전용 컨트롤을 만들거나 Native UI와 직접 연동하지 않더라도, 애플리케이션에 Fluent 테마를 한 번 선언한 이후에는 별다른 조치 없이 Nuri로 생성된 컨트롤에도 동일한 테마가 적용되었습니다.

Nuri는 결국 실제 WPF 컨트롤을 생성하기 때문에, WPF에 적용된 테마와 스타일 체계를 그대로 활용할 수 있었습니다.

처음에는 디자인 시스템까지 Nuri에서 직접 다뤄야 한다고 생각했지만, 기존 프레임워크가 제공하는 기능을 그대로 활용한다는 Nuri의 방향과도 잘 맞는 해결 방법이었습니다.

4 Likes

현재 WPF만 하고 있는데, 다른 쪽은?

현재 Nuri는 WPF를 중심으로 개발하고 있습니다.

Avalonia의 경우 dotnet watch를 통한 핫 리로드가 동작하는 단계까지는 테스트해둔 상태입니다.

다만 Nuri가 새로운 프레임워크를 지원하려면, 해당 프레임워크의 기본 컨트롤을 Nuri 요소와 연결하는 매핑 작업이 필요합니다.

예를 들어 Grid, Button, RadioButton, TextBox 같은 Nuri 요소가 각 프레임워크에서 어떤 실제 컨트롤로 생성되고, 속성과 이벤트가 어떻게 연결되는지를 하나씩 구현해야 합니다.

각 프레임워크에는 수많은 컨트롤이 있지만, 이를 처음부터 모두 지원하는 것은 현실적으로 어렵습니다.

따라서 우선 공통적으로 많이 사용하는 기본 컨트롤을 중심으로 지원하고, 실제 사용 과정에서 필요한 컨트롤을 점차 추가하는 방향으로 진행하려고 합니다.

여러 프레임워크를 동시에 넓게 지원하기보다는, 먼저 WPF에서 렌더링과 상태 변경, Preview, DevTools 같은 기본 기능을 충분히 안정화하는 것이 우선이라고 판단했습니다.

WPF가 어느 정도 안정화된 이후에는 MewUI 프레임워크와의 연동을 다음 목표로 진행할 예정입니다.

6 Likes

기능 확장보다는 안정화

이번 주말에는 새로운 기능을 추가하기보다, 지금까지 구현한 기능의 안정성과 내부 구조를 개선하는 데 집중했습니다.

컴포넌트의 생명주기, Hook 상태 관리, 렌더링 갱신과 불필요한 메모리 할당 등 실제로 계속 사용했을 때 문제가 될 수 있는 부분들을 점검하고 다듬었습니다.

세부적인 변경 내용과 구조에 대한 설명은 프로젝트의 docs에 정리하고 있습니다.

useState 사용 방식 변경

안정화 과정에서 useState 의 상태 변경 방식도 수정했습니다.

기존에는 현재 렌더링 시점의 상태값을 직접 참조해 새로운 값을 전달했습니다.

var (count, setCount) = useState(0);

setCount(count + 1);
setCount(0);

현재는 이전 상태값을 전달받는 함수 형태로 상태를 변경합니다.

var (count, setCount) = useState(0);

setCount(x => x + 1);
setCount(_ => 0);

이 방식은 상태 변경이 연속해서 발생하더라도, 컴포넌트가 렌더링될 당시의 오래된 count 값을 참조하지 않고 실제 최신 상태를 기준으로 값을 계산할 수 있습니다.

단순히 값을 대입하는 것보다 문법은 조금 길어졌지만, 상태 갱신의 기준을 명확하게 만들고 예측하기 어려운 상태 변경 문제를 줄이기 위한 선택이었습니다.

2 Likes

Nuri v0.0.1을 배포했습니다

오늘 Nuri의 첫 번째 버전인 v0.0.1을 배포했습니다.

Nuri의 WPF 라이브러리는 NuGet을 통해 설치할 수 있습니다.

Visual Studio에서 컴포넌트의 UI를 미리 확인할 수 있는 Nuri Preview 확장도 Visual Studio Marketplace에 함께 배포했습니다.

아직은 첫 번째 배포인 만큼 완성된 버전이라기보다, 지금까지 만들어온 기능을 실제 프로젝트에서 설치하고 사용할 수 있는 형태로 공개했다는 데 의미를 두고 있습니다.

Nuri Preview 역시 초기 버전이기 때문에 지원되는 환경과 기능에 제한이 있을 수 있습니다.

앞으로 직접 사용하면서 발견되는 문제를 수정하고, WPF 렌더링과 상태 관리, Preview 개발 경험을 조금씩 안정화해나갈 예정입니다.

작게 시작한 프로젝트지만, 이제 저장소 안에서만 동작하는 코드를 넘어 다른 프로젝트에서도 직접 설치하고 테스트할 수 있는 첫 단계까지 오게 되었습니다.

4 Likes

왜 모든 컨트롤을 매핑하지 않는가

Nuri가 새로운 UI 프레임워크를 지원하려면 해당 프레임워크의 컨트롤을 Nuri 요소와 연결하는 매핑 작업이 필요합니다.

처음에는 각 프레임워크가 제공하는 컨트롤을 최대한 많이 매핑해야 한다고 생각했습니다.

하지만 실제로 UI를 구성해보면 생각보다 많은 컨트롤을 직접 매핑하지 않아도 됩니다.

예를 들어 Grid , Button , Text 와 같은 기본 요소만으로도 여러 요소를 조합해 TreeView와 유사한 컴포넌트를 만들 수 있습니다.

WPF를 사용해보신 분이라면 익숙하시겠지만, TreeViewDataGrid 같은 복합 컨트롤도 내부적으로는 더 작은 시각 요소와 레이아웃, 입력 동작이 조합되어 만들어집니다.

그 위에 템플릿과 스타일이 적용되면서 하나의 완성된 컨트롤처럼 동작하게 됩니다.

Nuri 역시 모든 프레임워크 컨트롤을 무조건 일대일로 매핑하기보다는, 기본 요소를 조합해 재사용 가능한 컴포넌트를 만드는 방향을 우선하고 있습니다.

물론 기본 컨트롤만으로 구현하기 어렵거나, 해당 프레임워크가 제공하는 고유 기능을 활용해야 하는 경우도 있습니다.

이런 경우에는 실제 필요성이 확인되었을 때 해당 컨트롤의 매핑을 추가하려고 합니다.

즉, 지원 가능한 컨트롤의 수를 늘리는 것보다 적은 수의 기본 요소만으로 얼마나 다양한 컴포넌트를 구성할 수 있는지를 더 중요하게 보고 있습니다.

source : Nuri.ExplorerTreeSample

4 Likes

데이터가 많아지면 어떻게 될까?

오늘 아침에 올린 글을 공유한 뒤, 상태가 변경될 때마다 재귀적으로 UI를 다시 계산하는 구조라면 데이터가 많아졌을 때 GC 부담으로 성능이 저하될 수 있지 않겠느냐는 피드백을 받았습니다.

저 역시 충분히 가능성이 있는 문제라고 생각했습니다.

Nuri가 가상 UI 트리를 사용한다고 해도, 실제 화면에 표시되는 모든 항목을 WPF 컨트롤로 생성한다면 데이터가 많아질수록 컨트롤 생성 비용과 메모리 할당량이 함께 증가할 수 있습니다.

그렇다고 기존 네이티브 프레임워크가 제공하는 기본 컨트롤을 전혀 사용하지 않는 방향으로 가는 것도 Nuri의 목적과는 맞지 않았습니다.

그래서 기존 WPF 컨트롤을 그대로 활용하면서도, 현재 화면에 필요한 항목만 생성하는 가상화 방식을 적용해보았습니다.

즉, 데이터 전체의 상태와 구조는 Nuri가 관리하지만 실제 네이티브 컨트롤은 화면에 보이는 범위만 만들어집니다.

아직 다양한 환경과 실제 애플리케이션을 기준으로 더 많은 검증이 필요하지만, 많은 데이터를 다루는 컴포넌트에서도 기존 WPF 컨트롤을 활용하면서 생성 비용과 메모리 사용량을 줄일 수 있는 방향은 확인할 수 있었습니다.

source : Nuri.VirtualExplorerTreeSample

4 Likes

Transform 기반 애니메이션 지원

기존 Nuri에서는 Margin, Opacity처럼 비교적 단순한 속성 애니메이션만 지원했습니다.

이번에는 화면 요소의 이동과 크기 변화를 표현할 수 있도록 Transform 기반 애니메이션을 추가했습니다.

새롭게 지원하는 항목은 다음과 같습니다.

  • Translation
  • TranslationX
  • TranslationY
  • Scale
  • ScaleX
  • ScaleY

이를 통해 요소를 좌우 또는 상하로 이동시키거나, 전체 크기 또는 특정 축을 기준으로 확대·축소하는 애니메이션을 적용할 수 있게 되었습니다.

기존에는 위치를 변경하기 위해 Margin 값을 조정해야 했다면, 이제는 레이아웃 자체에 영향을 주지 않는 Transform 기반의 이동 효과도 표현할 수 있습니다.

단순한 속성 변화에서 조금 더 자연스러운 UI 전환과 인터랙션을 만들 수 있는 기반을 추가한 셈입니다.

1 Like

MultiWindow 지원

Nuri에 여러 개의 Window를 동시에 사용할 수 있는 MultiWindow 기능을 추가했습니다.

샘플에서는 각 Window가 자신만의 개별 State를 가지는 경우와, 여러 Window가 하나의 공용 State를 함께 사용하는 경우를 모두 확인할 수 있도록 구성했습니다.

개별 State를 사용하면 각 Window가 서로 영향을 주지 않고 독립적으로 동작합니다.

반대로 공용 State를 사용하면 한 Window에서 변경한 값이 다른 Window에도 함께 반영됩니다.

처음 구현했을 때는 MainWindow를 닫아도 이미 열려 있던 SubWindow들이 함께 종료되지 않는 문제가 있었습니다.

애플리케이션의 종료 기준과 Window 간의 소유 관계가 명확하지 않았기 때문입니다.

이 부분을 조정해 MainWindow가 종료될 때 남아 있는 SubWindow들도 함께 정리되도록 동작을 보완했습니다.

이번 작업을 통해 Nuri에서도 단일 화면뿐 아니라 여러 Window가 상태를 공유하거나 독립적으로 동작하는 데스크톱 애플리케이션 구성을 지원할 수 있게 되었습니다.

2 Likes

WPF에 없는 웹스러운 정렬을 추가해봤습니다

Nuri를 만들면서 기존 WPF의 레이아웃 방식만으로는 조금 아쉽다고 느낀 부분이 있었습니다.

WPF에는 StackPanel, Grid, DockPanel처럼 강력한 레이아웃 컨테이너가 있지만, 웹의 Flexbox처럼 자식 요소를 간단하게 분배하고 정렬하는 표현은 다소 부족합니다.

예를 들어 다음과 같은 배치를 자주 사용하게 됩니다.

배치
Start 앞쪽에 붙임
Center 가운데 모음
End 뒤쪽에 붙임
SpaceBetween 자식 사이만 동일하게 배치하고 양 끝에는 여백을 두지 않음
SpaceAround 각 자식 주변의 공간을 동일하게 배치
SpaceEvenly 양 끝을 포함한 모든 간격을 동일하게 배치

Nuri에서는 이를 다음과 같이 표현할 수 있도록 구현해보았습니다.

Div(DivTypes.Row, children)
    .Spacing(8)
    .JustifyContent(ContentDistribution.SpaceEvenly);

자주 사용하는 정렬 방식은 더 간단하게 작성할 수 있는 편의 DSL도 함께 제공합니다.

.SpaceBetween()
.SpaceAround()
.SpaceEvenly()

다만 여기서 말하는 ‘균등 정렬’은 두 가지 의미로 구분할 필요가 있습니다.

하나는 자식 사이의 간격을 균등하게 배치하는 것이고, 다른 하나는 자식들의 크기 자체를 동일하게 만드는 것입니다.

예를 들어 버튼 3개의 너비를 동일하게 채우고 싶다면 다음과 같이 작성할 수 있습니다.

Div(DivTypes.Row,
    Button("One").Grow(),
    Button("Two").Grow(),
    Button("Three").Grow()
).Spacing(8);

레이아웃 API는 다음과 같은 구성을 기준으로 정리하고 있습니다.

.JustifyContent(...) // 주축 배치
.AlignItems(...)     // 반대축 배치
.Spacing(8)          // 자식 사이 간격
public enum ContentDistribution
{
    Start,
    Center,
    End,
    SpaceBetween,
    SpaceAround,
    SpaceEvenly
}

public enum ItemAlignment
{
    Start,
    Center,
    End,
    Stretch
}

WPF의 기존 레이아웃 기능을 버리는 것이 아니라, 그 위에 웹 개발에서 익숙한 정렬 개념을 추가하는 방식입니다.

Grid도 빠질 수 없었습니다

Flexbox와 유사한 정렬 기능만 추가하고 끝내기에는 WPF의 핵심 레이아웃인 Grid가 조금 섭섭할 것 같았습니다.

기존 WPF의 Grid에서는 행과 열 사이의 간격을 표현하기 위해 여백을 직접 조정하는 경우가 많습니다.

Nuri에서는 이를 조금 더 간단하게 작성할 수 있도록 행과 열 간격을 위한 API를 추가했습니다.

.ColumnsSpacing(5)
.RowSpacing(5)
.Spacing(5)

각각 다음과 같은 의미를 가집니다.

  • ColumnsSpacing(5)는 열과 열 사이의 간격을 설정합니다.
  • RowSpacing(5)는 행과 행 사이의 간격을 설정합니다.
  • Spacing(5)는 행과 열의 간격을 모두 설정합니다.

또한 Grid에 자식 요소를 하나씩 배치하면서 매번 RowColumn을 직접 지정하는 과정도 줄이고 싶었습니다.

Grid(
    Text("a"),
    Text("b"),
    Text("c"),
    Text("d")
)
.Columns("*,*")
.AutoFlow();

위 코드는 두 개의 Column을 기준으로 자식 요소를 순서대로 배치합니다.

정해진 Column 수를 초과하면 AutoFlow()가 자동으로 새로운 Row(Auto)를 생성해 다음 줄에 이어서 배치합니다.

개념적으로는 다음과 같은 Grid 구성을 자동으로 만들어주는 방식입니다.

.Columns("*,*")
.Rows("Auto")

자식이 더 추가되면 필요한 만큼 Row도 계속 생성됩니다.

작은 카드 목록이나 설정 화면처럼 열의 수만 정해두고 요소를 순서대로 배치하고 싶은 경우, 각 요소마다 RowColumn을 직접 지정하지 않아도 됩니다.

Nuri에서는 이런 식으로 기존 WPF Grid의 강력한 기능은 그대로 활용하면서, 반복적으로 작성해야 했던 배치 코드를 조금 더 단순하게 줄여보려고 합니다.

VS Code에도 Preview를 넣어봤습니다

Visual Studio에 이어 VS Code에서도 Nuri Preview를 사용할 수 있도록 기능을 추가해봤습니다.

구현 과정에서는 microsoft.reactor.ui의 구조를 참고하면서, Nuri에 필요한 기능을 추가하고 적용하고 있습니다.

최근 VS Code에서 동작하는 Preview 관련 기능을 보면서 Nuri에도 비슷한 기능이 있으면 좋겠다는 생각이 들었습니다.

Visual Studio Preview만큼 드라마틱한 경험은 아니더라도, 스냅샷 형태로 현재 UI를 에디터 안에서 확인할 수 있다면 없는 것보다는 낫지 않을까 싶었습니다.

처음부터 완성된 형태를 목표로 하기보다는, 일단 기능을 추가해두면 언젠가 더 확장할 수 있겠다는 생각으로 시작했습니다.

구조 자체는 비교적 단순합니다.

현재 열려 있는 C# 파일을 기준으로 해당 프로젝트의 .csproj를 찾고, 별도의 Nuri.WPF.PreviewHost.exe를 실행합니다.

Preview Host에서 렌더링한 WPF 화면을 이미지 프레임으로 전달받아 VS Code의 Webview 안에 지속적으로 표시하는 방식입니다.

하지만 첫 번째 구현에서는 화면 갱신이 상당히 버벅이는 문제가 있었습니다.

WPF 화면을 이미지로 캡처하고, 이를 인코딩해 지속적으로 전달하다 보니 이미지 크기와 전송량이 생각보다 큰 영향을 주었습니다.

이를 개선하기 위해 몇 가지 조정을 진행했습니다.

  • 실제 WPF 창 크기 그대로 전송하지 않고, VS Code Webview에 표시되는 영역에 맞게 이미지 크기를 축소했습니다.
  • 일정 주기로 프레임을 요청하는 Polling 방식을 사용하되, 이전 프레임과 비교해 변화가 없으면 Webview 업데이트를 생략했습니다.
  • JPEG 품질을 80으로 제한해 이미지 품질보다는 실시간으로 변경 내용을 확인하는 데 초점을 맞췄습니다.

처음에는 단순히 FPS를 낮추는 방식도 고려했습니다.

하지만 FPS를 줄이면 화면이 갱신될 때마다 뚝뚝 끊기는 느낌이 강해졌습니다.

그래서 갱신 주기는 유지하면서, 실제 화면에 변화가 있을 때만 새로운 프레임을 전달하는 방식으로 변경했습니다.

아직 Visual Studio Preview처럼 UI 요소와 소스 코드를 직접 연결하는 수준은 아니지만, VS Code에서도 현재 작성 중인 UI를 별도의 애플리케이션 창 없이 확인할 수 있는 기반은 만들어졌습니다.

2 Likes

Nuri v0.2.0을 출시했습니다

그동안 개발 로그를 통해 하나씩 소개했던 가상화, 웹 스타일 레이아웃, Transform 애니메이션, Multi-Window, VS Code Preview 기능이 이번 v0.2.0에 포함되었습니다.

이번 버전은 새로운 기능을 단순히 추가하는 데서 끝나지 않고, 실제 배포본에서 사용할 수 있도록 Core와 WPF 렌더러의 구조, 컴포넌트 생명주기, 이벤트 정리, Preview 실행 환경을 함께 다듬은 버전입니다.

이번 버전에 포함된 기능

대용량 목록을 위한 가상화

많은 데이터를 다루는 화면에서도 실제로 보이는 항목만 WPF 컨트롤로 생성할 수 있도록 가상화 기능을 추가했습니다.

항목의 Key를 기준으로 추가, 제거, 이동, 업데이트를 처리하며, 대용량 목록의 렌더링 상태를 확인할 수 있는 진단 기능도 함께 추가했습니다.

레이아웃 기능 확장

이전에 소개했던 웹 스타일의 콘텐츠 분배와 Grid 자동 배치 기능이 이번 버전에 포함되었습니다.

Row, Column, Wrap, Grid 레이아웃을 확장했으며 다음과 같은 콘텐츠 정렬을 지원합니다.

  • Start
  • Center
  • End
  • SpaceBetween
  • SpaceAround
  • SpaceEvenly

Grid에서는 행과 열의 간격을 지정하고, 정해진 Column 수에 따라 자식 요소를 자동으로 배치할 수 있습니다.

Rows()Columns() 문법 역시 Pixel, Star, 가중치 Star, Auto 길이를 지원하도록 확장했습니다.

Transform 애니메이션

기존 OpacityMargin 중심의 애니메이션에서 확장해, 다음과 같은 Transform 기반 애니메이션을 지원합니다.

  • Translate
  • TranslateX
  • TranslateY
  • Scale
  • ScaleX
  • ScaleY

Multi-Window

여러 Window가 독립적인 State를 가지거나 하나의 공용 State를 공유할 수 있도록 Multi-Window 기능을 추가했습니다.

MainWindow가 종료될 때 남아 있는 SubWindow도 함께 정리되도록 Window 생명주기도 보완했습니다.

Visual Studio 및 VS Code Preview

Visual Studio와 VS Code Preview가 공통 PreviewHost를 사용하도록 구조를 정리했습니다.

VS Code에서는 렌더링된 WPF 화면을 이미지 프레임으로 전달받아 Webview에서 확인할 수 있는 실시간 Preview 기능을 제공합니다.

컴포넌트 검색, 빌드 오류 표시, 어셈블리 로딩과 코드 변경 후 새로고침 동작도 함께 개선했습니다.

안정성 개선

새로운 기능뿐 아니라 기존 기능에서 확인된 문제도 함께 수정했습니다.

  • 일부 컴포넌트가 부분 렌더링 후 정상적으로 갱신되지 않던 문제
  • State는 변경됐지만 실제 WPF 컨트롤에 반영되지 않던 문제
  • 가상화 항목의 제거 및 교체 과정에서 발생하던 생명주기 문제
  • 제거된 UI 요소의 이벤트 핸들러가 남아 있던 문제
  • Command Palette의 검색과 키보드 동작 문제
  • Visual Studio 확장 설치 과정에서 devenv /updateconfiguration이 실행되거나 멈추던 문제

샘플 추가

이번 기능들을 확인할 수 있도록 다음 샘플도 추가했습니다.

  • Explorer Tree
  • Virtual Explorer Tree
  • Animated Dashboard
  • Multi-Window

v0.2.0은 그동안 개발 로그로 소개했던 기능들을 실제 설치 가능한 배포본에 모으고, 그 과정에서 드러난 렌더링과 생명주기 문제를 함께 안정화한 버전입니다.

1 Like

다음 타깃을 다시 고민해봤습니다

앞선 글에서는 WPF 안정화 이후 MewUI 연동을 다음 목표로 진행할 예정이라고 이야기했습니다.

Avalonia도 dotnet watch를 통한 Hot Reload가 동작하는 단계까지는 테스트해봤지만, 당시 다음 대상으로 언급했던 것이 MewUI였기 때문에 자연스럽게 그쪽을 먼저 진행하려고 했습니다.

MewUI는 WPF와 비슷하게 UI 요소를 생성하고 그 상태를 유지하는 방식이어서, 지금까지 만든 Nuri의 구조를 다른 프레임워크에 적용해보기에도 적절하다고 생각했습니다.

그러던 중 Duxel이라는 라이브러리를 보게 되면서 계획을 다시 고민하게 되었습니다.

MewUI가 WPF와 같은 Retained Mode 계열이라면, Duxel은 화면을 매 프레임 다시 구성하고 그리는 방식에 가깝습니다.

비슷한 구조의 프레임워크를 하나 더 연결하는 것도 의미가 있지만, 이번에는 전혀 다른 렌더링 방식에 Nuri를 연결해보는 편이 더 새로운 도전이 될 것 같았습니다.

그래서 MewUI 연동을 포기한 것은 아니지만, 우선순위를 조정해 Duxel 연동을 먼저 시도해보려고 합니다.

Nuri의 컴포넌트와 State 구조가 컨트롤을 유지하는 환경뿐 아니라, 매 프레임 다시 그리는 환경에서도 동작할 수 있는지 확인해보는 실험이 될 것 같습니다.


Duxel은 0.2.6-preview 버전까지 .NET 10을 대상으로 하고 있었습니다.

현재 Nuri를 .NET 9 기반으로 개발하고 있는 입장에서는 바로 연동하기 어려운 부분이 있어, .NET 9도 함께 지원해주실 수 있는지 요청드렸습니다.

감사하게도 요청을 흔쾌히 받아주셨고, .NET 9 환경에서도 사용할 수 있도록 지원 범위를 확장해주셨습니다.

Nuri와 Duxel의 연동을 시도할 수 있도록 빠르게 대응해주신 @dimohy 님께 다시 한번 감사드립니다.

3 Likes

단순한 렌더러 연동이라고 생각했습니다

처음에는 Nuri의 요소를 Duxel 위젯과 연결하면 되는 비교적 단순한 작업이라고 생각했습니다.

하지만 Duxel에는 WPF처럼 유지되는 네이티브 레이아웃 트리가 없기 때문에, Nuri.DuxelPixel, Auto, Star의 크기와 각 요소의 실제 위치를 직접 계산해야 했습니다.

창 크기가 변경될 때는 타이틀바를 제외한 실제 콘텐츠 영역을 기준으로 고정 크기는 유지하고 Star 영역만 늘어나도록 처리했습니다. 사이즈 그립을 통한 창 크기 조절과 Scroll 위의 마우스 입력도 Nuri에서 연결하되, 키보드와 텍스트 입력, IME는 Duxel의 기존 입력 처리를 그대로 사용하도록 경계를 나눴습니다.

즉, 단순한 그리기 연결을 넘어 Nuri.Duxel이 레이아웃과 입력의 의미를 Duxel 환경에 맞게 구체화하는 역할까지 맡기 시작했습니다.

처음에는 렌더러 하나를 추가한다고 생각했지만, 실제로는 Nuri가 자체 레이아웃 구조로 확장될 가능성을 시험하는 작업이 되었습니다.

1 Like

매우 흥미롭네요. Duxel은 잘 아시겠지만 바로 표현하며 처리하는 형태라 WPF같은 느낌으로 뭔가를 구성할수 없다는 아쉬움이 있었는데 Nuri.Duxel이 그런 역할을 하게 될 것 같아 기대가 됩니다

1 Like