Blazor Developer Tools v0.10: 프레임워크 수준 통합에 대한 심층 탐구
Source: Dev.to
소개
제가 처음 Blazor Developer Tools 를 출시했을 때 목표는 Blazor 개발자에게 React 개발자들이 수년간 누려온 것과 같은 컴포넌트‑검사 경험을 제공하는 것이었습니다. React DevTools 를 사용하면 컴포넌트 트리를 확인하고, 실시간으로 props 를 검사하며, 애플리케이션 구조를 이해할 수 있습니다. 그에 비해 Blazor 에는 동등한 기능이 없었습니다.
v0.9 릴리스는 동작하는 MVP였지만, 아키텍처에 근본적인 제한이 있었고 결국 한계에 부딪히게 되었습니다. 몇 주 동안 Blazor 소스 코드를 연구하고 여러 막다른 길을 탐색한 끝에, 이러한 문제를 프레임워크 수준에서 해결할 수 있는 완전히 새로운 v0.10 아키텍처를 설계하게 되었습니다.
이 글에서는 제가 배운 점, 고려했던 옵션들, 그리고 새로운 접근 방식이 왜 효과적인지에 대해 설명합니다.
v0.9 아키텍처
v0.9 아키텍처는 영리했지만 결국 우회책에 불과했습니다. 세 단계로 구성되었습니다.
- 빌드‑시점 변환 – MSBuild 작업이
.razor파일을 스캔해 그림자 복사본을 만들고, 컴포넌트 메타데이터를 담은 데이터 속성을 가진 보이지 않는<span>마커를 삽입했습니다. - 런타임 탐지 – 브라우저 확장 프로그램이 DOM에서 이러한 마커를 검색했습니다.
- 트리 재구성 – 확장 프로그램이 마커 위치를 기반으로 컴포넌트 계층 구조를 재구성했습니다.
<!-- injected marker example (empty in original) -->
v0.9 의 문제점
-
컴포넌트 라이브러리 충돌 – 일부 라이브러리(예: MudBlazor)는 자식 요소를 검증하고 예상치 못한 요소가 나타나면 예외를 발생시킵니다. 삽입된
<span>때문에 이러한 오류가 발생했습니다:<!-- example of problematic injected markup --> <span data-bdt-component-id="123" style="display:none;"></span> -
우회책 필요 – 사용자가 문제를 일으키는 컴포넌트를 제외할 수 있도록
SkipComponents설정을 추가했지만, 이는 패치일 뿐 근본적인 해결책이 아니었습니다. 사용자는 어떤 컴포넌트가 깨지는지 스스로 찾아내고 설정해야 했습니다. -
정적 메타데이터 –
<span>마커에 담긴 메타데이터는 빌드 시점에 캡처된 것이며, 실제 실행 중인 컴포넌트 인스턴스와 연결되지 않았습니다. 결과적으로:- 실시간 파라미터 값이 없음.
- 성능 지표(렌더 횟수, 타이밍) 없음.
- 컴포넌트 상태 없음 – 마커는 살아있는 Blazor 애플리케이션과 단절된 죽은 HTML이었습니다.
-
DOM 오염 – 보이지 않는 요소를 삽입하는 것은 옳지 않은 느낌을 주었습니다. CSS 선택자, 자동화 테스트, 혹은 특수 케이스에 영향을 줄 수 있었습니다.
원하는 기능
새로운 아키텍처는 본질적으로 DOM 요소 하나에 대해 두 가지 질문에 답할 수 있어야 했습니다.
- 어떤 컴포넌트가 이 요소를 렌더링했는가?
- 그 컴포넌트의 현재 파라미터 값은 무엇인가?
Blazor 는 이 두 질문에 답할 공개 API를 제공하지 않습니다. 컴포넌트 트리는 .NET 메모리 안에 존재하고, DOM 은 브라우저에 존재하며, 이 둘 사이에 공개된 다리 역할을 하는 것이 없습니다.
Blazor 렌더링 파이프라인
렌더링 흐름을 단순화한 모습:
┌─────────────────────────────────────────────────────────────────┐
│ .NET SIDE │
│ │
│ Component Instance │
│ │ │
│ ▼ │
│ BuildRenderTree() → RenderTreeFrames │
│ │ │
│ ▼ │
│ Renderer assigns componentId, diffs against previous tree │
│ │ │
│ ▼ │
│ RenderBatch (binary format) │
│ │
└─────────────────────────────────────────────────────────────────┘
│
│ SignalR (Server) or
│ Direct call (WebAssembly)
▼
┌─────────────────────────────────────────────────────────────────┐
│ BROWSER SIDE │
│ │
│ blazor.server.js / blazor.webassembly.js │
│ │ │
│ ▼ │
│ BrowserRenderer interprets RenderBatch │
│ │ │
│ ▼ │
│ DOM mutations applied │
│ │
└─────────────────────────────────────────────────────────────────┘
핵심 인사이트: Blazor 는 componentId(각 컴포넌트 인스턴스에 할당되는 정수)와 해당 DOM 위치 사이의 내부 매핑을 유지합니다. 이 매핑은 JavaScript 런타임의 BrowserRenderer 안에 존재하지만 private 입니다. 만약 컴포넌트 생성 시점을 가로채어 JavaScript 가 보는 것과 연결할 수 있다면, 필요한 다리를 만들 수 있습니다.
작동하지 않은 접근 방식
1. CSS Isolation 활용
Blazor 는 범위가 지정된 CSS 를 위해 요소에 빈 b-xxxxxxxxxx 속성을 추가합니다. 이 메커니즘을 재사용할 수 있을까 생각했습니다.
- 문제: Razor 컴파일 과정은 블랙 박스입니다. CSS‑isolation 속성은 컴파일러 자체가 추가하는 것이며, 확장 가능한 파이프라인이 없어 추가 속성을 삽입할 훅이 없습니다.
2. 커스텀 베이스 클래스 요구
모든 컴포넌트가 ComponentBase 대신 BdtComponentBase 를 상속하도록 강제합니다.
public class Counter : BdtComponentBase // instead of ComponentBase
{
// …
}
- 사용자 마찰: 모든 컴포넌트를 수정하도록 요구하는 것은 현실적이지 않습니다.
- 상속 충돌: 많은 프로젝트가 이미 커스텀 베이스 클래스나 라이브러리 제공 베이스를 사용하고 있습니다.
- 서드파티 컴포넌트: MudBlazor, Radzen 등 외부 컴포넌트를 수정할 수 없습니다.
3. 소스 제너레이터로 등록 코드 삽입
각 컴포넌트의 라이프사이클 동안(예: OnInitialized) 등록 코드를 자동 생성합니다.
// Generated code injected into component
protected override void OnInitialized()
{
BdtRegistry.Register(this);
base.OnInitialized();
}
- 기존 오버라이드 충돌: 사용자가 이미
OnInitialized를 오버라이드했다면 충돌이 발생합니다. 호출 순서를 어떻게 맞출지도 애매합니다. - Partial‑class 복잡성: Razor 컴포넌트는 이미 부분 클래스와 생성된 코드가 존재합니다. 추가로 생성된 라이프사이클 오버라이드를 넣으면 깨지기 쉬운 상호작용이 생깁니다.
- 예외 상황:
SetParametersAsync를 오버라이드하면서base를 호출하지 않거나,ComponentBase를 상속하지 않은 컴포넌트는 이 접근 방식을 무너뜨립니다.
4. 수정된 Blazor SDK (‘핵’ 옵션)
추적 계측을 포함한 커스텀 Blazor SDK 를 만들고 사용하도록 합니다.
- 유지 보수 부담: 새로운 Blazor 릴리스가 나올 때마다 커스텀 SDK 를 최신 버전과 병합해야 합니다.
- 사용자 마찰: 개발자는 비표준 SDK 를 참조해야 하므로 채택 장벽이 높아집니다.
- 신뢰 문제: 핵심 프레임워크 컴포넌트를 교체하는 것은 대부분 팀에게 설득하기 어렵습니다.
결론
v0.9 아키텍처는 컴포넌트‑검사가 가능함을 증명했지만, 정적 마커를 DOM 에 삽입하는 방식은 취약하고 제한적이었습니다. Blazor 내부의 componentId 매핑을 이해하고 프레임워크 수준에서 컴포넌트 생성을 가로챔으로써, v0.10 은 .NET 컴포넌트 인스턴스와 렌더링된 DOM 요소 사이에 견고하고 실시간인 다리를 구축합니다. 이를 통해 파라미터, 상태, 성능 지표를 실시간으로 검사할 수 있게 되며, DOM 오염이나 사용자 코드에 대한 침습적 변경 없이도 구현할 수 있게 되었습니다.