Blazor JS Interop 연동: IJSRuntime 핵심 가이드

JS Interop 연동
📚 시작하기 전에

JS Interop 연동

IJSRuntime을 통해 C#과 JavaScript를 상호 호출하는 Interop 패턴을 익힙니다.

📌 학습 목표

  • IJSRuntime이 무엇인지, 의존성 주입(의존성 주입(DI))으로 어떻게 사용하는지 설명할 수 있습니다.
  • C# 코드에서 JavaScript 함수를 InvokeVoidAsync / InvokeAsync<T>로 호출할 수 있습니다.
  • JavaScript에서 DotNet.invokeMethodAsync를 통해 C# 정적 메서드를 호출할 수 있습니다.
  • DotNetObjectReference를 활용하여 인스턴스 메서드를 JavaScript에서 역방향 호출할 수 있습니다.
  • JS 격리 모듈(.razor.js)과 메모리 관리(Dispose) 방법을 이해합니다.

📝 개념 설명

JS Interop이란?

Blazor는 WebAssembly 또는 Server 방식으로 동작하지만, 브라우저 환경에서는 JavaScript가 필요한 순간이 자주 발생합니다. JS Interop(JavaScript Interoperability)은 C#과 JavaScript가 서로를 호출할 수 있게 해 주는 Blazor의 핵심 기능입니다.

 

다음과 같은 상황에서 JS Interop이 필요합니다:

  • 브라우저 내장 API 사용 (alert, localStorage, clipboard 등)
  • 기존 JavaScript 라이브러리(Chart.js, Leaflet 등) 연동
  • DOM 요소에 직접 접근해야 하는 경우
  • 브라우저 창 크기·스크롤 위치 등 브라우저 상태 감지

IJSRuntime 인터페이스

IJSRuntime은 Blazor가 제공하는 인터페이스로, C# 코드에서 JavaScript 함수를 호출하는 진입점 역할을 합니다. @inject 지시어로 컴포넌트에 주입하거나 생성자 주입으로 서비스에서 사용합니다.

@inject IJSRuntime JS

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("alert", "페이지가 로드되었습니다!");
        }
    }
}

C#에서 JavaScript 호출 — InvokeVoidAsync / InvokeAsync<T>

IJSRuntime의 두 가지 핵심 메서드입니다:

  • InvokeVoidAsync(함수명, 인수…) — 반환값이 없는 JS 함수 호출
  • InvokeAsync<T>(함수명, 인수…) — T 타입 반환값이 있는 JS 함수 호출
// 반환값 없음 — console.log 호출
await JS.InvokeVoidAsync("console.log", "Blazor에서 로그 출력");

// 반환값 있음 — localStorage에서 string 읽기
string? value = await JS.InvokeAsync<string>("localStorage.getItem", "myKey");

// 반환값 있음 — confirm 다이얼로그 (bool 반환)
bool confirmed = await JS.InvokeAsync<bool>("confirm", "계속하시겠습니까?");

커스텀 JavaScript 함수 등록

직접 작성한 JS 함수를 Blazor에서 호출하려면 wwwroot/index.html(Blazor WASM) 또는 Pages/_Host.cshtml(Blazor Server)에 스크립트를 등록합니다.

<!-- wwwroot/index.html -->
<script>
  window.myApp = {
    showMessage: function(message) {
      alert(message);
    },
    getWindowWidth: function() {
      return window.innerWidth;
    },
    setLocalStorage: function(key, value) {
      localStorage.setItem(key, value);
    },
    getLocalStorage: function(key) {
      return localStorage.getItem(key);
    }
  };
</script>
// C# 컴포넌트에서 커스텀 함수 호출
await JS.InvokeVoidAsync("myApp.showMessage", "안녕하세요!");

int width = await JS.InvokeAsync<int>("myApp.getWindowWidth");

await JS.InvokeVoidAsync("myApp.setLocalStorage", "username", "홍길동");
string? name = await JS.InvokeAsync<string>("myApp.getLocalStorage", "username");

JavaScript에서 C# 호출 — 정적 메서드

JavaScript에서 C# 메서드를 역방향으로 호출하려면 해당 메서드에 [JSInvokable] 특성을 적용합니다.

1단계 — C# 정적 메서드 정의:
public static class MyInteropMethods
{
    [JSInvokable]
    public static string GetGreeting(string name)
    {
        return $"안녕하세요, {name}님!";
    }
}
2단계 — JavaScript에서 호출:
// DotNet.invokeMethodAsync(어셈블리명, 메서드명, 인수)
async function callCSharp() {
  const greeting = await DotNet.invokeMethodAsync(
    'MyBlazorApp',    // .csproj 어셈블리 이름
    'GetGreeting',    // [JSInvokable] 메서드 이름
    '홍길동'          // 인수
  );
  console.log(greeting); // 안녕하세요, 홍길동님!
}
💡 어셈블리 이름 확인: .csproj 파일의 <AssemblyName> 태그 값을 사용합니다. 명시되지 않은 경우 프로젝트 이름이 기본 어셈블리 이름입니다.

JavaScript에서 C# 호출 — 인스턴스 메서드

정적 메서드가 아닌 컴포넌트 인스턴스 메서드를 호출하려면 DotNetObjectReference를 사용합니다. C#에서 객체 참조를 JS에 전달하고, JS는 그 참조를 통해 인스턴스 메서드를 호출합니다.

C# 컴포넌트 (.razor):
@implements IDisposable
@inject IJSRuntime JS

<button @onclick='RegisterCallback'>콜백 등록</button>
<p>수신 데이터: @_received</p>

@code {
    private DotNetObjectReference<MyComponent>? _objRef;
    private string _received = string.Empty;

    private async Task RegisterCallback()
    {
        _objRef = DotNetObjectReference.Create(this);
        await JS.InvokeVoidAsync("myApp.registerDotNet", _objRef);
    }

    [JSInvokable]
    public void OnJsCallback(string data)
    {
        _received = data;
        StateHasChanged();
    }

    public void Dispose()
    {
        _objRef?.Dispose();
    }
}
JavaScript:
window.myApp = {
  registerDotNet: function(dotNetRef) {
    // 2초 후 C# 인스턴스 메서드 역호출
    setTimeout(() => {
      dotNetRef.invokeMethodAsync('OnJsCallback', '타이머 완료!');
    }, 2000);
  }
};

 

 

🔄 JS Interop 양방향 호출 흐름

 

C# 컴포넌트
→ InvokeAsync →
IJSRuntime
→ 브라우저 →
JS 함수 실행
JS 함수
→ invokeMethodAsync →
DotNet / DotNetObjectRef
→ [JSInvokable] →
C# 메서드

JavaScript 격리 모듈 (JS Isolation)

.NET 5 이후 지원되는 JavaScript 격리(JS Isolation) 기능을 사용하면 컴포넌트와 같은 폴더에 .razor.js 파일을 두어 전역 네임스페이스 오염 없이 모듈 단위로 관리할 수 있습니다.

// Counter.razor.js — ES 모듈 방식
export function showAlert(message) {
  alert(message);
}

export function getLocalItem(key) {
  return localStorage.getItem(key);
}
// Counter.razor
@inject IJSRuntime JS
@implements IAsyncDisposable

@code {
    private IJSObjectReference? _module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./Components/Counter.razor.js");
        }
    }

    private async Task UseModule()
    {
        if (_module is null) return;
        await _module.InvokeVoidAsync("showAlert", "모듈 호출 성공!");
        string? val = await _module.InvokeAsync<string>("getLocalItem", "myKey");
    }

    public async ValueTask DisposeAsync()
    {
        if (_module is not null)
            await _module.DisposeAsync();
    }
}

💡 예제 & 실습

실습 1: 클립보드 복사 기능 구현

텍스트를 클립보드에 복사하는 실용적인 예제입니다. JS의 navigator.clipboard.writeText API를 C#에서 호출합니다.

// wwwroot/js/app.js
window.copyToClipboard = async function(text) {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch (err) {
    console.error('클립보드 복사 실패:', err);
    return false;
  }
};
@* CopyButton.razor *@
@inject IJSRuntime JS

<div>
  <input @bind='_text' placeholder='복사할 텍스트 입력' />
  <button @onclick='CopyText'>복사하기</button>
  <span>@_status</span>
</div>

@code {
    private string _text = string.Empty;
    private string _status = string.Empty;

    private async Task CopyText()
    {
        bool success = await JS.InvokeAsync<bool>("copyToClipboard", _text);
        _status = success ? "✅ 복사 완료!" : "❌ 복사 실패";
        StateHasChanged();
    }
}

실습 2: 스크롤 위치 제어

페이지 최상단으로 스크롤하거나 특정 요소 위치로 부드럽게 이동하는 기능입니다.

// JS 함수 등록
window.scrollUtils = {
  scrollToTop: function() {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  },
  scrollToElement: function(elementId) {
    const el = document.getElementById(elementId);
    if (el) el.scrollIntoView({ behavior: 'smooth' });
  }
};
// C# 컴포넌트
@inject IJSRuntime JS

<button @onclick='GoToTop'>맨 위로</button>

@code {
    private async Task GoToTop()
        => await JS.InvokeVoidAsync("scrollUtils.scrollToTop");

    private async Task GoToSection(string id)
        => await JS.InvokeVoidAsync("scrollUtils.scrollToElement", id);
}

실습 3: 브라우저 창 크기 감지 + C# 콜백

JS에서 resize 이벤트를 감지하여 C# 메서드를 역방향으로 호출하는 예제입니다. 이 패턴은 실시간 반응형 UI에서 자주 활용됩니다.

// JS
window.resizeTracker = {
  register: function(dotNetRef) {
    window.addEventListener('resize', () => {
      dotNetRef.invokeMethodAsync(
        'OnWindowResized',
        window.innerWidth,
        window.innerHeight
      );
    });
  }
};
// C# 컴포넌트
@implements IDisposable
@inject IJSRuntime JS

<p>창 크기: @_width × @_height px</p>

@code {
    private int _width;
    private int _height;
    private DotNetObjectReference<ResizePage>? _objRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _objRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("resizeTracker.register", _objRef);
        }
    }

    [JSInvokable]
    public void OnWindowResized(int width, int height)
    {
        _width = width;
        _height = height;
        StateHasChanged();
    }

    public void Dispose() => _objRef?.Dispose();
}

 

 

📌 JS Interop 핵심 API 비교

 

🔇
InvokeVoidAsync
반환값 없는 JS 호출. alert, console.log, localStorage.setItem 등에 사용
📨
InvokeAsync<T>
반환값 있는 JS 호출. confirm(bool), getItem(string), innerWidth(int) 등
🏷️
[JSInvokable]
JS → C# 역호출 허용 특성. 정적 메서드는 DotNet.invokeMethodAsync, 인스턴스 메서드는 DotNetObjectReference로 노출
📦
JS 격리 모듈
.razor.js 파일로 컴포넌트별 ES 모듈 캡슐화. IJSObjectReference로 참조하고 DisposeAsync로 해제

⚠️ 자주 틀리는 것 / 주의사항

1. OnInitializedAsync에서 JS 호출 금지

⚠️ 주의: JS Interop은 DOM이 렌더링된 이후에만 호출 가능합니다. OnInitializedAsyncOnParametersSetAsync에서 호출하면 DOM이 아직 없어 오류가 발생합니다. 반드시 OnAfterRenderAsync(firstRender == true)에서 호출하세요.
// ❌ 잘못된 예 — DOM 준비 전
protected override async Task OnInitializedAsync()
{
    await JS.InvokeVoidAsync("someFunction"); // 오류 발생!
}

// ✅ 올바른 예 — DOM 준비 후
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JS.InvokeVoidAsync("someFunction");
    }
}

2. DotNetObjectReference 메모리 누수

⚠️ 주의: DotNetObjectReference.Create(this)로 생성한 참조는 가비지 컬렉션(GC)가 자동으로 해제하지 않습니다. 컴포넌트가 소멸될 때 반드시 Dispose()를 호출해야 합니다.
// ✅ IDisposable 구현으로 참조 해제
public void Dispose()
{
    _objRef?.Dispose();
}

3. JS 모듈 null 체크 누락

IJSObjectReferenceOnAfterRenderAsync에서 초기화됩니다. 초기화 전에 사용하면 NullReferenceException이 발생하므로 항상 null 체크를 수행합니다.

// ✅ 모듈 사용 전 null 체크 필수
private async Task UseModule()
{
    if (_module is null) return;
    await _module.InvokeVoidAsync("myFunction");
}

4. JS 반환값 직렬화 타입 불일치

JavaScript에서 반환하는 값은 JSON 직렬화 가능한 타입이어야 합니다. 복잡한 객체는 C# 레코드/클래스로 역직렬화할 수 있으며, 프로퍼티 이름은 camelCase(JS) ↔ PascalCase(C#) 매핑이 자동으로 처리됩니다.

// JS에서 객체 반환
window.getUser = function() {
  return { name: '홍길동', age: 30 };
};

// C# 수신용 레코드 정의
public record UserInfo(string Name, int Age);

// C#에서 역직렬화
UserInfo user = await JS.InvokeAsync<UserInfo>("getUser");

5. Blazor Server — 연결 단절 예외 처리

⚠️ Blazor Server 한정: SignalR 연결이 끊기면 JS Interop 호출 시 JSDisconnectedException이 발생합니다. 장수 컴포넌트(채팅, 실시간 업데이트 등)는 반드시 예외를 처리하세요.
try
{
    await JS.InvokeVoidAsync("updateUI", data);
}
catch (JSDisconnectedException)
{
    // 연결 끊김 — 무시하거나 재연결 안내
}
catch (TaskCanceledException)
{
    // 렌더링 취소 — 무시
}

🎯 마무리

JS Interop은 Blazor 애플리케이션이 브라우저 환경과 자유롭게 소통할 수 있게 해 주는 핵심 기능입니다. IJSRuntime을 통한 C# → JS 호출과, [JSInvokable] + DotNetObjectReference를 통한 JS → C# 역방향 호출 패턴을 함께 이해하면 기존 JavaScript 생태계 전체를 Blazor와 통합할 수 있습니다.

 

실전에서는 전역 window에 함수를 등록하는 방식보다 .razor.js 격리 모듈을 권장합니다. 컴포넌트 단위로 스크립트를 캡슐화하면 네임스페이스 충돌 없이 유지보수성이 크게 향상됩니다. 또한 비동기 패턴과 Dispose 처리를 철저히 지키면 메모리 누수 없는 안정적인 앱을 구축할 수 있습니다.

✅ 핵심 정리
  • IJSRuntime을 의존성 주입(DI)로 주입하여 C#에서 JavaScript 함수를 비동기 호출합니다.
  • InvokeVoidAsync는 반환값 없는 호출, InvokeAsync<T>는 반환값 있는 호출에 사용합니다.
  • JS에서 C# 메서드를 호출하려면 [JSInvokable] 특성이 필요하며, 어셈블리 이름과 메서드 이름을 정확히 지정합니다.
  • 인스턴스 메서드는 DotNetObjectReference.Create(this)로 참조를 JS에 전달합니다.
  • JS Interop 초기화는 반드시 OnAfterRenderAsync(firstRender == true)에서 수행합니다.
  • DotNetObjectReference는 사용 후 Dispose()로 반드시 해제합니다 (메모리 누수 방지).
  • .razor.js 격리 모듈로 전역 네임스페이스 오염 없이 컴포넌트별 JS를 안전하게 관리합니다.

댓글 남기기

Wordpress Social Share Plugin powered by Ultimatelysocial
Copy link
URL has been copied successfully!
THREADS
RSS
error: 저작권 콘텐츠보호를 부탁드립니다.