ASP.NET Core Blazor 동기화 컨텍스트
참고
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
중요
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
Blazor는 동기화 컨텍스트(SynchronizationContext)를 사용하여 단일 논리적 실행 스레드를 적용합니다. 구성 요소의 수명 주기 메서드 및 Blazor에서 발생하는 이벤트 콜백은 동기화 컨텍스트에서 실행됩니다.
Blazor'의 서버 쪽 동기화 컨텍스트는 단일 스레드 환경이 단일 스레드인 브라우저의 WebAssembly 모델과 밀접하게 일치하도록 단일 스레드 환경을 에뮬레이트하려고 시도합니다. 이 에뮬레이션은 개별 회로로만 범위가 지정됩니다. 즉, 서로 다른 두 회로가 병렬로 실행될 수 있습니다. 회로 내의 지정된 특정 시점에서 작업은 정확히 하나의 스레드에서 수행되므로 단일 논리 스레드의 인상을 생성합니다. 두 작업이 동일한 회로 내에서 동시에 실행되지 않습니다.
단일 논리 실행 스레드가 단일 비동기 제어 흐름을 의미하지는 않습니다. 구성 요소는 불완전한 Task를 기다리는 모든 지점에서 재진입 가능합니다. 수명 주기 메서드 또는 구성 요소 삭제 메서드 비동기 제어 흐름이 완료될 Task 대기한 후 다시 시작하기 전에 호출할 수 있습니다. 따라서 잠재적으로 불완전한 Task를 기다리기 전에, 구성 요소가 유효한 상태인지 반드시 확인해야 합니다. 특히 구성 요소는 OnInitializedAsync 또는 OnParametersSetAsync 반환할 때 렌더링에 유효한 상태인지 확인해야 합니다. 이러한 메서드 중 하나가 불완전한 Task반환하는 경우 완료하는 메서드의 일부가 동기적으로 구성 요소를 렌더링에 유효한 상태로 유지해야 합니다.
재진입 컴포넌트의 또 다른 의미는 메서드가 ComponentBase.InvokeAsync에 전달하여 메서드가 반환될 때까지 Task을 연기할 수 없다는 것입니다.
ComponentBase.InvokeAsync을 호출하면 다음 await
연산자에 도달할 때까지 Task을 연기할 수 있습니다.
구성 요소는 IDisposable 또는 IAsyncDisposable을 구현하여, 구성 요소가 삭제될 때 취소되는 CancellationTokenSource의 CancellationToken를 사용하여 비동기 메서드를 호출할 수 있습니다. 그러나 이것은 실제로 시나리오에 따라 달라집니다. 구성 요소 작성자가 올바른 동작인지 여부를 결정해야 합니다. 예를 들어 저장 단추를 선택할 때 데이터베이스에 일부 로컬 데이터를 유지하는 SaveButton
구성 요소를 구현하는 경우 구성 요소 작성자는 사용자가 단추를 선택하고 다른 페이지로 빠르게 이동하여 비동기 저장이 완료되기 전에 구성 요소를 삭제할 수 있는 경우 변경 내용을 삭제하려고 할 수 있습니다.
일회용 구성 요소는 구성 요소가 CancellationToken을 수신하지 않는 Task를 대기한 후 삭제 여부를 확인할 수 있습니다. 불완전한 Task은 삭제된 구성 요소의 가비지 수집을 방지할 수도 있습니다.
ComponentBase는 Task의 취소로 인한 예외를 무시합니다 (더 정확하게는 대기 중인 Task이(가) 취소된 경우 모든 예외를 무시합니다), 따라서 구성 요소 메서드는 TaskCanceledException를 처리할 필요가 없습니다. OperationCanceledException.
ComponentBase 파생된 구성 요소에 대해 유효한 상태를 구성하는 항목을 개념화하지 않고 자체적으로 IDisposable 또는 IAsyncDisposable구현하지 않으므로 이전 지침을 따를 수 없습니다. OnInitializedAsync CancellationToken 사용하지 않는 불완전한 Task 반환하고 구성 요소가 Task 완료되기 전에 삭제되는 경우 ComponentBase 여전히 OnParametersSet 호출하고 OnParametersSetAsync기다립니다. 삭제 가능한 구성 요소가 CancellationToken사용하지 않는 경우 OnParametersSet 및 OnParametersSetAsync 구성 요소가 삭제되었는지 확인해야 합니다.
스레드 차단 호출 방지
일반적으로 구성 요소에서 다음 메서드를 호출하지 마세요. 다음 메서드는 실행 스레드를 차단하므로 기본 Task가 완료될 때까지 앱이 작업을 다시 시작하지 못하게 차단합니다.
참고
이 섹션에서 설명한 스레드 차단 메서드를 사용하는 Blazor 설명서 예제에서는 권장 코딩 지침이 아니라 데모용으로만 메서드를 사용합니다. 예를 들어 몇 가지 구성 요소 코드 데모에서는 Thread.Sleep을 호출하여 장기 실행 프로세스를 시뮬레이션합니다.
외부에서 구성 요소 메서드를 호출하여 상태 업데이트
외부 이벤트(예: 타이머 또는 다른 알림)를 기준으로 구성 요소를 업데이트해야 하는 경우 InvokeAsync
의 동기화 컨텍스트에 코드 실행을 디스패치하는 Blazor 메서드를 사용합니다. 예를 들어 수신 대기하는 구성 요소에 업데이트된 상태를 알릴 수 있는 다음 ‘알림 서비스’를 살펴보세요.
Update
메서드는 앱의 어디에서나 호출할 수 있습니다.
TimerService.cs
:
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new Timer();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
NotifierService.cs
:
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
서비스를 등록합니다.
클라이언트 쪽 개발의 경우 클라이언트 쪽
Program
파일에 서비스를 싱글톤으로 등록합니다.builder.Services.AddSingleton<NotifierService>(); builder.Services.AddSingleton<TimerService>();
서버 쪽 개발의 경우 서버
Program
파일에서 범위로 서비스를 등록합니다.builder.Services.AddScoped<NotifierService>(); builder.Services.AddScoped<TimerService>();
NotifierService
를 사용하여 구성 요소를 업데이트합니다.
Notifications.razor
:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
public void Dispose() => Notifier.Notify -= OnNotify;
}
Notifications.razor
:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
public void Dispose() => Notifier.Notify -= OnNotify;
}
ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key != null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
앞의 예에서:
- 타이머는 Blazor의 동기화 컨텍스트 외부에서
_ = Task.Run(Timer.Start)
로 시작됩니다. -
NotifierService
는 구성 요소의 메서드를OnNotify
호출합니다.InvokeAsync
는 올바른 컨텍스트로 전환하고 재렌더링을 큐에 넣는 데 사용됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요. - 구성 요소는 IDisposable을 구현합니다.
OnNotify
대리자는 구성 요소가 삭제될 때 프레임워크에서 호출되는Dispose
메서드에서 구독 취소됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 삭제참조하세요.
-
NotifierService
는OnNotify
의 동기화 컨텍스트 외부에서 구성 요소의 Blazor 메서드를 호출합니다.InvokeAsync
는 올바른 컨텍스트로 전환하고 다시 렌더링을 큐에 넣기 위해 사용됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요. - 구성 요소는 IDisposable을 구현합니다.
OnNotify
대리자는 구성 요소가 삭제될 때 프레임워크에서 호출되는Dispose
메서드에서 구독 취소됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 삭제참조하세요.
중요
Razor 구성 요소가 백그라운드 스레드에서 트리거되는 이벤트를 정의하는 경우, 처리기가 등록될 때 실행 컨텍스트(ExecutionContext)를 캡처하고 복원해야 할 수 있습니다. 자세한 정보는 InvokeAsync(StateHasChanged)
를 호출하면 페이지가 기본 문화권으로 대체됩니다(dotnet/aspnetcore #28521)를 참조하세요.
백그라운드 TimerService
에서 잡은 예외를 구성 요소로 전송하여 일반 수명 주기 이벤트 예외처럼 처리하려면, Razor 구성 요소의 수명 주기 외부에서 잡은 예외를 다루는 섹션을 참조하세요.
구성 요소의 수명 주기 외부에서 포착된 Razor 예외 처리
Razor 구성 요소의 수명 주기 호출 스택 외부에서 throw된 예외를 처리하기 위해 ComponentBase.DispatchExceptionAsync를 사용합니다. 이렇게 하면 구성 요소의 코드가 수명 주기 메서드 예외인 것처럼 예외를 처리할 수 있습니다. 그 후 Blazor오류 경계와 같은 오류 처리 메커니즘이 예외를 처리할 수 있습니다.
참고
Razor 구성 요소 파일(.razor
)에서는 ComponentBase로부터 상속되는 ComponentBase.DispatchExceptionAsync가 사용됩니다. 구성 요소를 implement IComponent directly할 때는 RenderHandle.DispatchExceptionAsync를 사용합니다.
구성 요소 라이프사이클 외부에서 잡힌 예외를 Razor 처리하기 위해, 예외를 DispatchExceptionAsync에 전달하고 결과를 대기합니다.
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
이전 접근 방식의 일반적인 시나리오는 구성 요소가 비동기 작업을 시작했지만 대기하지 않는 경우입니다. 이는 메서드가 실행(시작)되고 메서드의 결과가 잊혀(버려짐)지기 때문에 종종 "fire and forget" 패턴이라고도 합니다. 작업이 실패하는 경우 구성 요소가 다음 목표 중 하나로 오류를 구성 요소 수명 주기 예외로 처리하도록 할 수 있습니다.
- 예를 들어 오류 경계를 트리거하려면 구성 요소를 오류 상태로 전환합니다.
- 오류 경계가 없으면 회로를 종료합니다.
- 수명 주기 예외에 대해 발생하는 동일한 로깅을 트리거합니다.
다음 예제에서 사용자는 보고서 보내기 단추를 선택하여 보고서를 보내는 백그라운드 메서드 ReportSender.SendAsync
를 트리거합니다. 대부분의 경우 구성 요소는 비동기 호출을 기다리고 Task 작업이 완료되었음을 나타내도록 UI를 업데이트합니다. 다음 예제에서는 SendReport
메서드가 Task를 기다리지 않으며, 사용자에게 결과를 보고하지 않습니다. 구성 요소가 SendReport
에서 Task을 의도적으로 삭제하기 때문에 비동기 오류는 일반 수명 주기 호출 스택 외부에서 발생하며, 따라서 Blazor에서 볼 수 없습니다.
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = ReportSender.SendAsync();
}
}
수명 주기 메서드 예외와 같은 오류를 처리하려면 다음 예제와 같이 예외를 구성 요소로 DispatchExceptionAsync명시적으로 다시 디스패치합니다.
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = SendReportAsync();
}
private async Task SendReportAsync()
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
}
}
대체 방법은 다음을 활용합니다.Task.Run
private void SendReport()
{
_ = Task.Run(async () =>
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
작업 데모의 경우 상태를 업데이트하기 위해 외부적으로 Invoke 구성 요소 메서드에서 타이머 알림 예제를 구현합니다.
Blazor 앱에서 타이머 알림 예제에서 다음 파일을 추가하고 섹션에서 설명하는 대로 파일에 서비스를 Program
등록합니다.
TimerService.cs
NotifierService.cs
Notifications.razor
이 예제에서는 구성 요소의 수명 주기 외부에서 타이머를 Razor 사용합니다. 여기서 처리되지 않은 예외는 일반적으로 오류 경계Blazor오류 처리 메커니즘에 의해 처리되지 않습니다.
먼저 코드를 TimerService.cs
변경하여 구성 요소의 수명 주기 외부에서 인위적인 예외를 만듭니다. 루프while
에서 TimerService.cs
, elapsedCount
가 두에 도달하면 예외를 throw합니다.
if (elapsedCount == 2)
{
throw new Exception("I threw an exception! Somebody help me!");
}
앱의 기본 레이아웃에 오류 경계를 배치합니다.
<article>...</article>
마크업을 다음 마크업으로 바꿉니다.
MainLayout.razor
의 경우
<article class="content px-4">
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="alert alert-danger" role="alert">
Oh, dear! Oh, my! - George Takei
</p>
</ErrorContent>
</ErrorBoundary>
</article>
오류 경계가 정적 구성 요소에만 적용되는 경우 Blazor Web App에서 경계는 정적 서버 측 렌더링(정적 SSR) 단계 중에만 활성화됩니다. 구성 요소 계층 구조 아래의 구성 요소가 대화형이기 때문에 경계가 활성화되지 않습니다. 구성 요소 계층 구조에서 MainLayout
구성 요소 및 나머지 구성 요소에 대해 광범위하게 대화형 작업을 사용하도록 설정하려면 App
구성 요소(Components/App.razor
)의 HeadOutlet
및 Routes
구성 요소 인스턴스에 대해 대화형 렌더링을 사용하도록 설정합니다. 다음 예제에서는 대화형 서버(InteractiveServer
) 렌더링 모드를 채택합니다.
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
이 시점에서 앱을 실행하면, 경과된 값이 2에 도달할 때 예외가 발생합니다. 그러나 UI는 변경되지 않습니다. 오류 경계에 오류 내용이 표시되지 않습니다.
타이머 서비스의 예외를 다시 구성 요소로 디스패치하기 위해 Notifications
구성 요소에 다음과 같은 변경 내용이 적용됩니다.
- 문에서 타이머
try-catch
를 시작합니다.try-catch
블록의catch
절에서 예외는 Exception를 DispatchExceptionAsync에 전달하고 결과를 대기하여 다시 구성 요소로 디스패치됩니다. -
StartTimer
메서드에서, Action 대리자의 Task.Run에서 비동기 타이머 서비스를 시작하고 반환된 Task를 의도적으로 무시합니다.
StartTimer
구성 요소의 Notifications
메서드(Notifications.razor
):
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
타이머 서비스가 실행되어 카운트가 두에 도달하면 예외가 Razor 구성 요소로 디스패치되고, 이로 인해 오류 경계가 트리거되어 MainLayout
구성 요소에 있는 <ErrorBoundary>
의 오류 내용이 표시됩니다.
이런! 세상에! - 조지 테이크
ASP.NET Core