Blazor와 React의 조합 — JavaScript Interop

C# 프로그래밍 언어 하나로 프론트엔드를 쓸 수 있다는 사실에 많은 사람들의 주목을 받고, 매우 뜨거운 환영을 받고 있는 Blazor가 약 두 달간의 개발을 거쳐 이 글을 쓰는 시점인 2018년 9월 26일에 0.6.0 Preview 1을 오랫만에 업데이트했습니다.

새 소식을 들으니 지난번 0.5.1 버전을 사용해보면서 들었던 궁금증이 다시 떠올랐습니다.

Blazor에서는 오로지 C#만을 사용해서 프론트엔드 코딩을 해야 할까?

Blazor는 컴포넌트 내부에서 별도의 <script> 태그를 사용하는 것을 권장하지 않고 있고, 실제로 모든 코드를 C#으로만 작성하도록 강제하고 있습니다. 우리가 아는 대다수의 프론트엔드 코드는 ECMA Script를 기반으로 작동하는데 비해 너무 동떨어진 면이 있고, 이것이 아직 Blazor가 Experimental Framework 라는 것을 잘 보여주는 부분이기도 합니다.

앞으로 어떻게 개선이 될지는 모르겠습니다. 하지만 큰 결정을 새로 하지 않는 이상 현재의 구도에는 변화가 없을 것 같다는 예상은 어느정도 해볼 수 있습니다. 그래서인지 JavaScript Interop에 관한 여지를 미리부터 넣어두고 있는듯 합니다. 오늘은 이 부분을 어떻게 활용할 수 있는지에 대한 인사이트를 공유하려고 합니다.

React를 같이 사용하기 위한 준비

앞에서 이야기한대로 Razor는 cshtml 컴포넌트에 다른 <script> 태그를 포함하는 것을 권장하지 않습니다. 실제로, Visual Studio에서는 다음과 같은 빌드 경고를 미리 표시해줍니다.

script 태그를 사용하려면 index.html 로 옮기라고 합니다.

그래서 이 지침에 따라 솔루션 탐색기의 wwwroot 폴더 아래의 index.html 파일을 아래와 같이 수정하여 React 라이브러리에 대한 참조 <head> 태그에 추가했습니다. 그리고 scripts/reacttest.js 파일을 새로 만들어서 같이 추가했습니다.

<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<script src="scripts/reacttest.js"></script>

저는 Web Pack을 사용하진 않습니다. 그래서 편의상 unpkg.com 에서 미리 제공하는 캐시된 버전의 미리 빌드된 React 라이브러리를 참조하여 사용할 것입니다.

JavaScript Interop 코드 준비하기

scripts/reacttest.js 파일의 내용은 다음과 같습니다. window.reacttest 라는 프로퍼티에 함수를 대입하였고, target이 React가 그려낼 컴포넌트를 추가할 Root Element가 됩니다.

window.reacttest = function (target) {
class Greetings extends React.Component {
render() {
return React.createElement('h1', null, 'Greetings, ' + this.props.name + '!');
}
}
ReactDOM.render(
React.createElement(Greetings, { name: 'Chris' }),
target
);
}

빠르게 파악하신 분도 계시겠지만, React에 관한 코드는 온전히 scripts/reacttest.js 에 모두 담았습니다. index.html 파일을 거쳐서 추가해야 하는 점이 불편한 부분이고 앞으로 개선될 여지가 있을 것 같습니다만, 이렇게 추가되는 코드는 Blazor와는 무관하며, Blazor가 얼마든지 불러다 쓸 수 있습니다.

그러면 위의 코드를 Blazor 컴포넌트에서 불러다 쓰게 하려면 어떻게 해야 할까요? Extension 메서드를 하나 정의해보겠습니다.

public static Task ReactTest(this ElementRef elementRef)
{
return JSRuntime.Current.InvokeAsync<object>("window.reacttest", elementRef);
}

ElementRef 라는 타입은 Browser로부터 전달받은 DOM Element Object에 대한 불투명한 참조 (형식을 알 수 없는 참조)입니다. 뒤에서 다시 사용하겠지만, React가 컴포넌트를 그릴 Element를 Blazor에서 받아서 앞의 scripts/reacttest.js 측으로 전달하기 위하여 이 도우미 메서드를 사용하는 것입니다.

실제 Interop 코드 작성하고 테스트해보기

이제 Index.cshtml 파일을 열어서 React와 연결해보겠습니다.

<div ref="reacttest"></div>

위와 같이 ref 어트리뷰트에 이름을 지정합니다. id 어트리뷰트 대신 ref 어트리뷰트를 사용하여 Blazor에서 해당 DOM Element를 찾을 수 있게 해주는 것입니다.

그리고 ref 어트리뷰트 안에 지정했던 이름과 동일한 ElementRef 타입의 멤버 변수를 @functions 절 안에 아래와 같이 추가합니다.

@functions {
ElementRef reacttest;
}

그 다음, OnAfterRenderAsync 메서드를 재정의하여, reacttest 변수에 덧붙여지는 확장 메서드인 ReactTest (앞에서 만든 메서드)를 호출하는 코드를 추가합니다. Task 타입의 인스턴스를 반환하는 확정 메서드였으므로 await 키워드를 사용하여 호출하고, 재정의하는 메서드 자체는 async 키워드로 표기합니다.

@functions {
ElementRef reacttest;
  protected override async Task OnAfterRenderAsync()
{
await reacttest.ReactTest();
await base.OnAfterRenderAsync();
}
}

실행해보면 다음과 같이 React 컴포넌트가 div 태그 아래에 잘 렌더링된 것을 볼 수 있습니다.

Blazor와 React의 동시 사용 예시

이와 같은 방법으로 Blazor와 기존 JavaScript/ECMA Script 애플리케이션을 공존시킬 수 있다는 것을 확인할 수 있었습니다.

고민거리

Blazor는 React나 VueJS 등의 UI 프레임워크가 동작하는 기반과는 완전히 분리된 독립적인 웹 어셈블리 기반의 런타임을 사용합니다. 처음부터 완전히 새로 만드는 프론트엔드 UI에서 JavaScript 상호 운용성을 이용하여 얼마나 어떤 부분을 재사용하는 것이 좋을지에 대한 방향성을 잡는 것은 아마 무척 어려운 고민이 될 것 같습니다.

오늘 아티클은 Blazor를 메인으로 사용하는 상황에서 React를 상호운용성 기능으로 가져와 사용하는 방법을 보여드린 것이지만, 다른 형태의 상호운용 시나리오에 대한 피드백을 공유하고 토론해보는 것도 무척 유용할 것 같습니다.

아울러 Blazor에 관한 전문적인 내용을 공유하고 토론할 수 있는 Facebook 그룹을 열었습니다. https://www.facebook.com/groups/krblazor/ 에 방문해주셔서 토론을 발제해주신다면 매우 유익할 것 같습니다. :-)