Share via


ASP.NET Core Blazor 앱에서 오류 처리

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

이 문서에서는 Blazor가 처리되지 않은 예외를 관리하는 방법과 오류를 감지 및 처리하는 앱을 개발하는 방법을 설명합니다

개발 중에 발생한 자세한 오류

Blazor 앱이 개발 중에 올바르게 작동하지 않는 경우 앱에서 자세한 오류 정보를 수신하면 문제를 해결하고 수정하는 데 도움이 됩니다. 오류가 발생하면 Blazor 앱의 화면 아래쪽에 연한 노란색 막대가 표시됩니다.

  • 개발 중에 이 막대를 누르면 예외를 볼 수 있는 브라우저 콘솔로 연결됩니다.
  • 프로덕션에서 이 막대는 오류가 발생했음을 알려 주고 브라우저를 새로 고치도록 권장합니다.

이 오류 처리 환경의 UI는 Blazor 프로젝트 템플릿의 일부입니다. 프로젝트 템플릿의 Blazor 모든 버전이 이 특성을 사용하여 data-nosnippet 오류 UI의 내용을 캐시하지 않도록 브라우저에 신호를 보낼 수는 없지만 모든 버전의 Blazor 설명서에서 특성을 적용합니다.

Blazor 웹앱에서 구성 요소의 환경을 사용자 지정합니다MainLayout. 환경 태그 도우미(예: <environment include="Production">...</environment>)는 구성 요소에서 Razor 지원되지 않으므로 다음 예제에서는 다양한 환경에 대한 오류 메시지를 구성하기 위해 삽입 IHostEnvironment 합니다.

맨 위에는 다음이 있습니다 MainLayout.razor.

@inject IHostEnvironment HostEnvironment

오류 UI 태그를 Blazor 만들거나 수정합니다.

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Blazor Server 앱에서 파일의 환경을 사용자 지정합니다Pages/_Host.cshtml. 다음 예제에서는 환경 태그 도우미사용하여 다양한 환경에 대한 오류 메시지를 구성합니다.

Blazor Server 앱에서 파일의 환경을 사용자 지정합니다Pages/_Layout.cshtml. 다음 예제에서는 환경 태그 도우미사용하여 다양한 환경에 대한 오류 메시지를 구성합니다.

Blazor Server 앱에서 파일의 환경을 사용자 지정합니다Pages/_Host.cshtml. 다음 예제에서는 환경 태그 도우미사용하여 다양한 환경에 대한 오류 메시지를 구성합니다.

오류 UI 태그를 Blazor 만들거나 수정합니다.

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Blazor WebAssembly 앱에서 wwwroot/index.html 파일의 환경을 사용자 지정합니다.

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

blazor-error-ui 앱의 display: none 자동 생성 스타일시트에 CSS 클래스의 스타일 blazor-error-ui 이 있기 때문에 요소가 일반적으로 숨겨집니다. 오류가 발생하면 프레임워크가 display: block을 요소에 적용합니다.

blazor-error-ui 이 요소는 일반적으로 폴더의 display: none 사이트 스타일시트에 wwwroot/css CSS 클래스 스타일 blazor-error-ui 이 있기 때문에 숨겨집니다. 오류가 발생하면 프레임워크가 display: block을 요소에 적용합니다.

자세한 회로 오류

이 섹션은 회로를 Blazor 통해 작동하는 Web Apps에 적용됩니다.

이 섹션은 Blazor Server 앱에 적용됩니다.

클라이언트 쪽 오류는 호출 스택을 포함하지 않으며 오류의 원인에 대한 세부 정보를 제공하지 않지만 서버 로그에는 이러한 정보가 포함되어 있습니다. 개발 단계에서는 자세한 오류를 사용하도록 설정하여 클라이언트에서 중요한 회로 오류 정보를 볼 수 있도록 설정할 수 있습니다.

CircuitOptions.DetailedErrorstrue로 설정합니다. 자세한 내용과 예제는 ASP.NET Core BlazorSignalR 지침을 참조하세요.

설정 CircuitOptions.DetailedErrors 의 대안은 구성 키를 true 앱의 Development 환경 설정 파일(appsettings.Development.json)로 설정하는 DetailedErrors 것입니다. 또한 자세한 SignalR 로깅을 위해 SignalR 서버 쪽 로깅(Microsoft.AspNetCore.SignalR)을 디버그 또는 추적으로 설정할 수 있습니다.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

구성 키는 DetailedErrors 환경 서버 또는 로컬 시스템에서 값 trueStagingDevelopment/이 있는 환경 변수를 사용하도록 ASPNETCORE_DETAILEDERRORS 설정할 true 수도 있습니다.

Warning

인터넷의 클라이언트에 오류 정보를 노출하는 것을 항상 피하세요. 보안상 위험할 수 있습니다.

구성 요소 서버 쪽 렌더링에 대한 Razor 자세한 오류

이 섹션은 Web Apps에 Blazor 적용됩니다.

RazorComponentsServiceOptions.DetailedErrors 옵션을 사용하여 구성 요소 서버 쪽 렌더링에 대한 오류에 대한 Razor 자세한 정보 생성을 제어할 수 있습니다. 기본값은 false입니다.

다음 예제에서는 자세한 오류를 사용하도록 설정합니다.

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

Warning

환경에서 자세한 오류 Development 만 사용하도록 설정합니다. 자세한 오류에는 악의적인 사용자가 공격에 사용할 수 있는 앱에 대한 중요한 정보가 포함될 수 있습니다.

위의 예제에서는 반환IsDevelopment된 값을 기준으로 값을 DetailedErrors 설정하여 안전도를 제공합니다. 앱이 환경에 DetailedErrors 있는 Development 경우 .로 설정true됩니다. 이 방법은 환경의 퍼블릭 서버에서 Development 프로덕션 앱을 호스트할 수 있기 때문에 완벽하지 않습니다.

개발자 코드에서 처리되지 않은 예외 관리

오류가 발생한 후에도 앱을 계속하려면 앱에 오류 처리 논리가 있어야 합니다. 이 문서의 나중 섹션에서는 처리되지 않은 예외의 잠재적 원인을 설명합니다.

프로덕션 환경에서는 UI에서 프레임워크 예외 메시지 또는 스택 추적을 렌더링하지 않습니다. 예외 메시지 또는 스택 추적을 렌더링하면 다음이 수행될 수 있습니다.

  • 최종 사용자에게 중요한 정보를 공개합니다.
  • 악의적인 사용자가 앱, 서버 또는 네트워크의 보안을 손상시킬 수 있는 앱의 약점을 찾아내도록 합니다.

회로에 대한 처리되지 않은 예외

이 섹션은 회로를 통해 작동하는 서버 쪽 앱에 적용됩니다.

Razor 서버 대화형 작업을 사용하도록 설정된 구성 요소는 서버에서 상태 저장입니다. 사용자가 서버의 구성 요소와 상호 작용하는 동안 회로라고 하는 서버에 대한 연결을 기본. 회로는 활성 구성 요소 인스턴스와 다음과 같은 상태의 여러 다양한 측면을 포함합니다.

  • 구성 요소의 가장 최근에 렌더링된 출력
  • 클라이언트 쪽 이벤트에 의해 트리거될 수 있는 현재 이벤트 처리 대리자 세트

사용자가 여러 브라우저 탭에서 앱을 여는 경우 여러 개의 독립적인 회로를 만들게 됩니다.

Blazor는 발생하는 대부분의 처리되지 않은 예외를 회로에 치명적인 것으로 취급합니다. 처리되지 않은 예외로 인해 회로가 종료되는 경우 사용자는 페이지를 다시 로드하여 새 회로를 만드는 방식으로 앱과 계속 상호 작용할 수 있습니다. 다른 사용자 또는 다른 브라우저 탭을 위한 회로이면서 종료된 회로의 외부에 있는 회로는 영향을 받지 않습니다. 이 시나리오는 충돌하는 데스크톱 앱과 유사합니다. 작동을 중단한 앱을 다시 시작해야 하지만 다른 앱은 영향을 받지 않습니다.

다음과 같은 이유로 인해 처리되지 않은 예외가 발생하면 프레임워크가 회로를 종료합니다.

  • 처리되지 않은 예외는 종종 회로를 정의되지 않은 상태로 둡니다.
  • 처리되지 않은 예외가 발생한 후에는 앱의 일반 작업을 보장할 수 없습니다.
  • 회로가 정의되지 않은 상태로 계속되는 경우 앱에 보안 취약성이 나타날 수 있습니다.

전역 예외 처리

전역 예외 처리는 다음 섹션을 참조하세요.

오류 경계

오류 경계는 예외를 처리하는 편리한 방법을 제공합니다. ErrorBoundary 구성 요소는 다음과 같습니다.

  • 오류가 발생하지 않은 경우 자식 내용을 렌더링합니다.
  • 처리되지 않은 예외가 throw되는 경우 오류 UI를 렌더링합니다.

오류 경계를 정의하려면 ErrorBoundary 구성 요소를 사용하여 기존 내용을 래핑합니다. 앱은 계속 정상적으로 작동하지만, 오류 경계가 처리되지 않은 예외를 처리합니다.

<ErrorBoundary>
    ...
</ErrorBoundary>

전역 방식으로 오류 경계를 구현하려면 앱의 기본 레이아웃의 본문 콘텐츠 주위에 경계를 추가합니다.

MainLayout.razor의 경우

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

오류 경계가 정적 구성 요소에만 적용되는 Web Apps에서는 Blazor 정적 MainLayout 서버 쪽 렌더링(정적 SSR) 단계 중에만 경계가 활성화됩니다. 구성 요소 계층 구조 아래의 구성 요소가 대화형이기 때문에 경계가 활성화되지 않습니다. 구성 요소 계층 구조에서 구성 요소 및 나머지 구성 요소에 대해 MainLayout 광범위하게 대화형 작업을 사용하도록 설정하려면 구성 요소(Components/App.razor)의 구성 요소 인스턴스 및 Routes 구성 요소 인스턴스에 App 대해 HeadOutlet 대화형 렌더링을 사용하도록 설정합니다. 다음 예제에서는 대화형 서버(InteractiveServer) 렌더링 모드를 채택합니다.

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

구성 요소에서 전체 앱에서 Routes 서버 대화형 작업을 사용하도록 설정하지 않으려면 구성 요소 계층 구조 아래로 오류 경계를 더 배치합니다. 예를 들어 앱의 기본 레이아웃이 아니라 대화형 작업을 사용하도록 설정하는 개별 구성 요소에서 태그 주위에 오류 경계를 배치합니다. 유의해야 할 중요한 개념은 오류 경계가 배치되는 모든 위치에 있다는 것입니다.

  • 오류 경계가 대화형이 아닌 경우 정적 렌더링 중에 서버에서만 활성화할 수 있습니다. 예를 들어 구성 요소 수명 주기 메서드에서 오류가 throw되면 경계가 활성화할 수 있습니다.
  • 오류 경계가 대화형인 경우 래핑하는 대화형 서버 렌더링 구성 요소에 대해 활성화할 수 있습니다.

개수가 5개를 넘어 증가하는 경우 Counter 구성 요소에서 예외를 throw하는 다음 예제를 살펴보세요.

Counter.razor의 경우

private void IncrementCount()
{
    currentCount++;

    if (currentCount > 5)
    {
        throw new InvalidOperationException("Current count is too big!");
    }
}

5개가 넘는 currentCount에 대해 처리되지 않은 예외가 throw된 경우:

  • 오류는 정상적으로 기록됩니다(System.InvalidOperationException: Current count is too big!).
  • 오류 경계에서 예외가 처리됩니다.
  • 오류 UI는 다음 기본 오류 메시지와 함께 오류 경계에 의해 렌더링됩니다. An error has occurred.

기본적으로 ErrorBoundary 구성 요소는 오류 내용에 대해 blazor-error-boundary CSS 클래스를 사용하여 빈 <div> 요소를 렌더링합니다. 기본 UI의 색, 텍스트, 아이콘은 wwwroot 폴더에 있는 앱 스타일시트의 CSS를 사용하여 정의되므로 오류 UI를 자유롭게 사용자 지정할 수 있습니다.

속성을 설정하여 기본 오류 콘텐츠를 변경합니다.ErrorContent

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

오류 경계는 이전 예제의 레이아웃에 정의되어 있으므로 오류가 발생한 후 사용자가 탐색하는 페이지에 관계없이 오류 UI가 표시됩니다. 대부분 시나리오에서는 오류 경계 범위를 좁게 지정하는 것이 좋습니다. 오류 경계의 범위를 광범위하게 지정하는 경우 오류 경계의 Recover 메서드를 호출하여 후속 페이지 탐색 이벤트에서 오류 없는 상태로 다시 설정할 수 있습니다.

MainLayout.razor의 경우

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

복구가 오류를 다시 throw하는 구성 요소를 다시 생성하는 무한 루프를 방지하려면 렌더링 논리에서 호출 Recover 하지 마세요. 다음 경우에만 호출 Recover 합니다.

  • 사용자는 단추를 선택하여 프로시저를 다시 시도하거나 사용자가 새 구성 요소로 이동할 때 UI 제스처를 수행합니다.
  • 추가 논리도 예외를 지웁니다. 구성 요소가 다시 렌더링되면 오류가 다시 발생하지 않습니다.

대체 전역 예외 처리

오류 경계(ErrorBoundary)를 사용하는 대신 사용자 지정 오류 구성 요소를 자식 구성 요소에 CascadingValue로 전달할 수 있습니다. 삽입된 서비스 또는 사용자 지정 로거 구현을 사용하는 것에 비해 구성 요소를 사용하는 것의 이점은 연계된 구성 요소가 오류 발생 시 콘텐츠를 렌더링하고 CSS 스타일을 적용할 수 있다는 것입니다.

다음 Error 구성 요소 예제는 단지 오류를 로깅하지만 구성 요소의 메서드는 앱에서 요구하는 방식으로 오류를 처리할 수 있으며 여기에는 여러 오류 처리 메서드를 사용하는 것도 포함됩니다.

Error.razor:

@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if ProcessError directly participates in 
        // rendering. If ProcessError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

참고 항목

RenderFragment에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소를 참조하세요.

구성 요소에서 Routes 구성 요소(<Router>...</Router>)를 Router 구성 요소로 래핑합니다Error. 그러면 Error 구성 요소는 Error 구성 요소가 CascadingParameter로 수신되는 앱의 구성 요소로 아래로 연계될 수 있습니다.

Routes.razor의 경우

<Error>
    <Router ...>
        ...
    </Router>
</Error>

구성 요소에서 App 구성 요소(<Router>...</Router>)를 Router 구성 요소로 래핑합니다Error. 그러면 Error 구성 요소는 Error 구성 요소가 CascadingParameter로 수신되는 앱의 구성 요소로 아래로 연계될 수 있습니다.

App.razor의 경우

<Error>
    <Router ...>
        ...
    </Router>
</Error>

구성 요소에서 오류를 처리하려면:

  • @code 블록에서 Error 구성 요소를 CascadingParameter로 지정합니다. Blazor 프로젝트 템플릿을 기준으로 하는 앱의 예제 Counter 구성 요소에서 다음 Error 속성을 추가합니다.

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • 적절한 예외 형식이 있는 catch 블록에서 오류 처리 메서드를 호출합니다. 예제 Error 구성 요소는 단일 ProcessError 메서드만 제공하지만, 오류 처리 구성 요소는 앱 전체에서 대체 오류 처리 요구 사항을 해결할 수 있도록 여러 오류 처리 메서드를 제공할 수 있습니다. 다음 Counter 구성 요소 예제에서는 개수가 5보다 크면 예외가 throw되고 트래핑됩니다.

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public Error? Error { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                Error?.ProcessError(ex);
            }
        }
    }
    

이전에 변경된 Counter 구성 요소와 이전 Error 구성 요소를 사용하면 브라우저의 개발자 도구 콘솔은 트래핑되고 기록된 오류를 나타냅니다.

fail: {COMPONENT NAMESPACE}.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

사용자 지정 오류 메시지 표시줄을 표시하거나 렌더링된 요소의 CSS 스타일을 변경하는 등 ProcessError 메서드가 렌더링에 직접 참여하는 경우, UI를 다시 렌더링하기 위해 ProcessErrors 메서드의 끝에서 StateHasChanged를 호출합니다.

이 섹션의 접근 방식은 문을 사용하여 오류를 try-catch 처리하므로 오류가 발생하고 회로가 다시 활성화되면 클라이언트와 서버 간의 앱 SignalR 연결이 끊어지지 않습니다기본. 처리되지 않은 다른 예외는 회로에 치명적입니다. 자세한 내용은 회로가 처리되지 않은 예외에 반응하는 방법에 대한 섹션을 참조하세요.

앱은 오류 처리 구성 요소를 연계 값으로 사용하여 중앙 집중식으로 오류를 처리할 수 있습니다.

다음 Error 구성 요소는 자신을 CascadingValue로서 자식 구성 요소에 전달합니다. 다음 예제는 단지 오류를 로깅하지만 구성 요소의 메서드는 앱에서 요구하는 방식으로 오류를 처리할 수 있으며 여기에는 여러 오류 처리 메서드를 사용하는 것도 포함됩니다. 삽입된 서비스 또는 사용자 지정 로거 구현을 사용하는 것에 비해 구성 요소를 사용하는 것의 이점은 연계된 구성 요소가 오류 발생 시 콘텐츠를 렌더링하고 CSS 스타일을 적용할 수 있다는 것입니다.

Error.razor:

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

참고 항목

RenderFragment에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소를 참조하세요.

App 구성 요소에서 Router 구성 요소를 Error 구성 요소로 래핑합니다. 그러면 Error 구성 요소는 Error 구성 요소가 CascadingParameter로 수신되는 앱의 구성 요소로 아래로 연계될 수 있습니다.

App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

구성 요소에서 오류를 처리하려면:

  • @code 블록에서 Error 구성 요소를 CascadingParameter로 지정합니다.

    [CascadingParameter]
    public Error Error { get; set; }
    
  • 적절한 예외 형식이 있는 catch 블록에서 오류 처리 메서드를 호출합니다. 예제 Error 구성 요소는 단일 ProcessError 메서드만 제공하지만, 오류 처리 구성 요소는 앱 전체에서 대체 오류 처리 요구 사항을 해결할 수 있도록 여러 오류 처리 메서드를 제공할 수 있습니다.

    try
    {
        ...
    }
    catch (Exception ex)
    {
        Error.ProcessError(ex);
    }
    

위 예제의 Error 구성 요소 및 ProcessError 메서드를 사용하여 브라우저의 개발자 도구 콘솔은 트랩되고 로깅된 오류를 나타냅니다.

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException 메시지: 개체의 인스턴스에 대해 개체 참조가 설정되지 않았습니다.

사용자 지정 오류 메시지 표시줄을 표시하거나 렌더링된 요소의 CSS 스타일을 변경하는 등 ProcessError 메서드가 렌더링에 직접 참여하는 경우, UI를 다시 렌더링하기 위해 ProcessErrors 메서드의 끝에서 StateHasChanged를 호출합니다.

이 섹션의 접근 방식은 try-catch 문을 사용하여 오류를 처리하기 때문에 오류가 발생하고 회로가 활성 상태를 유지할 경우 클라이언트와 서버 간의 Blazor 앱 SignalR 연결이 끊어지지 않습니다. 처리되지 않은 예외는 회로에 치명적입니다. 자세한 내용은 회로가 처리되지 않은 예외에 반응하는 방법에 대한 섹션을 참조하세요.

영구 공급자가 있는 오류 로깅

처리되지 않은 예외가 발생하는 경우 예외가 서비스 컨테이너에 구성된 ILogger 인스턴스에 로깅됩니다. 기본적으로 Blazor 앱은 콘솔 로깅 공급자의 콘솔 출력에 로깅합니다. 로그 크기 및 로그 회전을 관리하는 공급자를 사용하여 서버의 위치(또는 클라이언트 쪽 앱의 백 엔드 웹 API)에 로깅하는 것이 좋습니다. 또는 앱이 Azure Application Insights(Azure Monitor) 같은 APM(애플리케이션 성능 관리) 서비스를 사용할 수 있습니다.

참고 항목

클라이언트 쪽 앱 및 Google Analytics에 대한 네이티브 프레임워크 지원을 지원하는 네이티브 BlazorApplication Insights 기능은 이러한 기술의 향후 릴리스에서 사용할 수 있습니다. 자세한 내용은 Blazor WASM 클라이언트 쪽 App Insights 지원(microsoft/ApplicationInsights-dotnet #2143)웹 분석 및 진단(커뮤니티 구현에 대한 링크 포함)(dotnet/aspnetcore #5461)을 참조하세요. 그 동안 클라이언트 쪽 앱은 interop과 함께 JS Application Insights JavaScript SDK를 사용하여 클라이언트 쪽 앱에서 Application Insights에 직접 오류를 기록할 수 있습니다.

회로를 Blazor 통해 작동하는 앱에서 개발하는 동안 앱은 일반적으로 디버깅을 지원하기 위해 브라우저 콘솔에 예외의 전체 세부 정보를 보냅니다. 프로덕션 환경에서는 자세한 오류가 클라이언트에 전송되지 않지만, 예외의 전체 세부 정보가 서버에 로깅됩니다.

로깅할 인시던트 및 로깅된 인시던트의 심각도 수준을 결정해야 합니다. 악의적인 사용자가 의도적으로 오류를 트리거할 수 있습니다. 예를 들어, 제품 정보를 표시하는 구성 요소의 URL에서 알 수 없는 ProductId가 제공되는 오류의 경우 인시던트를 로깅하지 않도록 합니다. 모든 오류가 로깅이 필요한 인시던트로 취급되는 것은 아닙니다.

자세한 내용은 다음 문서를 참조하세요.

|웹 API 백 엔드 앱인 서버 쪽 Blazor 앱 및 기타 서버 쪽 ASP.NET Core 앱에 Blazor적용됩니다. 클라이언트 쪽 앱은 클라이언트의 오류 정보를 웹 API로 트래핑하고 보낼 수 있으며, 이 정보를 영구 로깅 공급자에 기록합니다.

처리되지 않은 예외가 발생하는 경우 예외가 서비스 컨테이너에 구성된 ILogger 인스턴스에 로깅됩니다. 기본적으로 Blazor 앱은 콘솔 로깅 공급자의 콘솔 출력에 로깅합니다. 로그 크기 관리 및 로그 회전과 함께 로깅 공급자를 사용하는 백 엔드 웹 API에 오류 정보를 보냄으로써 서버의 좀 더 영구적인 위치에 로깅하는 것이 좋습니다. 또는 백 엔드 웹 API 앱은 Azure Application Insights(Azure Monitor)† 같은 APM(애플리케이션 성능 관리) 서비스를 사용하여 클라이언트에서 수신하는 오류 정보를 로깅할 수 있습니다.

로깅할 인시던트 및 로깅된 인시던트의 심각도 수준을 결정해야 합니다. 악의적인 사용자가 의도적으로 오류를 트리거할 수 있습니다. 예를 들어, 제품 정보를 표시하는 구성 요소의 URL에서 알 수 없는 ProductId가 제공되는 오류의 경우 인시던트를 로깅하지 않도록 합니다. 모든 오류가 로깅이 필요한 인시던트로 취급되는 것은 아닙니다.

자세한 내용은 다음 문서를 참조하세요.

† Google Analytics에 대한 클라이언트 쪽 앱 및 네이티브 Blazor 프레임워크 지원을 지원하는Native Application Insights 기능은 이러한 기술의 향후 릴리스에서 사용할 수 있습니다. 자세한 내용은 Blazor WASM 클라이언트 쪽 App Insights 지원(microsoft/ApplicationInsights-dotnet #2143)웹 분석 및 진단(커뮤니티 구현에 대한 링크 포함)(dotnet/aspnetcore #5461)을 참조하세요. 그 동안 클라이언트 쪽 앱은 interop과 함께 JS Application Insights JavaScript SDK를 사용하여 클라이언트 쪽 앱에서 Application Insights에 직접 오류를 기록할 수 있습니다.

‡Blazor 앱용 웹 API 백 엔드 앱인 서버 쪽 ASP.NET Core 앱에 적용됩니다. 클라이언트 쪽 앱은 오류 정보를 트랩하고 웹 API로 보냅니다. 웹 API는 이 정보를 영구 로깅 공급자에 로깅합니다.

오류가 발생할 수 있는 위치

프레임워크 및 앱 코드는 다음 위치 중 하나에서 처리되지 않은 예외를 트리거할 수 있습니다. 자세한 내용은 이 문서의 아래 섹션을 참조하세요.

구성 요소 인스턴스화

Blazor가 구성 요소의 인스턴스를 만들 경우

  • 구성 요소의 생성자가 호출됩니다.
  • @inject 지시문 또는 [Inject] 특성을 통해 구성 요소 생성자에 제공된 DI 서비스의 생성자가 호출됩니다.

실행된 생성자 또는 [Inject] 속성의 setter에 오류가 발생하면 처리되지 않은 예외가 발생하고 프레임워크가 구성 요소를 인스턴스화하는 것이 중단됩니다. 앱이 회로를 통해 작동하는 경우 회로가 실패합니다. 생성자 논리에서 예외를 throw할 수 있는 경우 앱은 오류 처리 및 로깅과 함께 try-catch 문을 사용하여 예외를 트랩해야 합니다.

수명 주기 메서드

구성 요소의 수명 주기 동안 Blazor는 수명 주기 메서드를 호출합니다. 수명 주기 메서드가 동기적 또는 비동기적으로 예외를 throw하는 경우 이 예외는 회로에 치명적입니다. 구성 요소가 수명 주기 메서드의 오류를 처리하려면 오류 처리 논리를 추가합니다.

OnParametersSetAsync가 제품을 가져오는 메서드를 호출하는 다음 예제에서는 다음 작업이 수행됩니다.

  • ProductRepository.GetProductByIdAsync 메서드에서 throw된 예외는 try-catch 문에 의해 처리됩니다.
  • 블록이 catch 실행되는 경우:
    • loadFailed가 사용자에게 오류 메시지를 표시하는 데 사용되는 true로 설정됩니다.
    • 오류가 로깅됩니다.
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

렌더링 논리

Razor 구성 요소 파일(.razor)의 선언적 태그는 BuildRenderTree라는 C# 메서드로 컴파일됩니다. 구성 요소가 렌더링되면 BuildRenderTree가 실행되고, 렌더링된 구성 요소의 요소, 텍스트 및 자식 구성 요소를 설명하는 데이터 구조를 구축합니다.

렌더링 논리는 예외를 throw할 수 있습니다. 이 시나리오의 예는 @someObject.PropertyName이 평가되지만 @someObjectnull인 경우에 발생합니다. 회로를 통해 작동하는 앱의 경우 Blazor 렌더링 논리에 의해 throw된 처리되지 않은 예외는 앱의 회로에 치명적입니다.

렌더링 논리에서 NullReferenceException이 발생하지 않도록 하려면 해당 멤버에 액세스하기 전에 null 개체를 확인합니다. 다음 예제에서 person.Addressnull인 경우 person.Address 속성에 액세스되지 않습니다.

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

위의 코드는 personnull이 아니라고 가정합니다. 일반적으로 이 코드의 구조에 따르면 구성 요소가 렌더링될 때 개체가 확실히 존재하게 됩니다. 이러한 경우에는 렌더링 논리에서 null이 있는지 확인하지 않아도 됩니다. 이전 예제에서는 구성 요소가 인스턴스화될 때 person이 만들어지기 때문에 person이 존재한다고 보장할 수 있습니다. 다음 예제에서 이를 보여 줍니다.

@code {
    private Person person = new();

    ...
}

이벤트 처리기

클라이언트 쪽 코드는 다음을 사용하여 이벤트 처리기를 만들 때 C# 코드의 호출을 트리거합니다.

  • @onclick
  • @onchange
  • 기타 @on... 특성
  • @bind

이벤트 처리기 코드는 이러한 시나리오에서 처리되지 않은 예외를 throw할 수 있습니다.

앱에서 외부 이유로 인해 실패할 수 있는 코드를 호출하는 경우 오류 처리 및 로깅과 함께 try-catch 문을 사용하여 예외를 트랩합니다.

이벤트 처리기가 개발자 코드에 의해 트래핑되지 않고 처리되지 않은 예외를 throw하는 경우(예: 데이터베이스 쿼리 실패):

  • 프레임워크는 예외를 기록합니다.
  • 회로를 Blazor 통해 작동하는 앱에서 예외는 앱의 회로에 치명적입니다.

구성 요소 삭제

예를 들어, 사용자가 다른 페이지로 이동했으므로 UI에서 구성 요소가 제거될 수 있습니다. System.IDisposable을 구현하는 구성 요소가 UI에서 제거되면 프레임워크는 구성 요소의 Dispose 메서드를 호출합니다.

구성 요소의 Dispose 메서드가 회로를 통해 작동하는 앱에서 Blazor 처리되지 않은 예외를 throw하는 경우 예외는 앱의 회로에 치명적입니다.

삭제 논리에서 예외를 throw할 수 있는 경우 앱은 오류 처리 및 로깅이 포함된 try-catch 문을 사용하여 예외를 트랩해야 합니다.

구성 요소 삭제에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소 수명 주기를 참조하세요.

JavaScript interop

IJSRuntime은 Blazor 프레임워크에 의해 등록됩니다. IJSRuntime.InvokeAsync는 .NET 코드가 사용자의 브라우저에서 JS(JavaScript) 런타임에 대한 비동기 호출을 수행할 수 있도록 합니다.

다음 조건은 InvokeAsync의 오류 처리에 적용됩니다.

  • InvokeAsync에 대한 호출이 동기적으로 실패하면 .NET 예외가 발생합니다. 예를 들어, 제공된 인수를 직렬화 할 수 없기 때문에 InvokeAsync에 대한 호출이 실패할 수 있습니다. 개발자 코드는 예외를 catch해야 합니다. 이벤트 처리기 또는 구성 요소 수명 주기 메서드의 앱 코드가 회로를 통해 작동하는 앱에서 Blazor 예외를 처리하지 않는 경우 결과 예외는 앱의 회로에 치명적입니다.
  • InvokeAsync에 대한 호출이 비동기적으로 실패하면 .NET Task는 실패합니다. 예를 들어 JS 쪽 코드에서 예외를 throw하거나 완료된 Promiserejected로 반환하기 때문에 InvokeAsync에 대한 호출이 실패할 수 있습니다. 개발자 코드는 예외를 catch해야 합니다. await 연산자를 사용하는 경우 오류 처리 및 로깅을 사용하여 try-catch 문에 메서드 호출을 래핑하는 것이 좋습니다. 그렇지 않으면 회로를 Blazor 통해 작동하는 앱에서 실패한 코드로 인해 앱 회로에 치명적인 처리되지 않은 예외가 발생합니다.
  • 기본적으로 호출은 InvokeAsync 특정 기간 내에 완료되거나 호출 시간이 초과되어야 합니다. 기본 제한 시간은 1분입니다. 제한 시간은 네트워크 연결이 끊어진 코드 또는 완료 메시지를 다시 전송하지 않는 JS 코드를 보호합니다. 호출 시간이 초과되면 결과 System.Threading.TasksOperationCanceledException을 나타내며 실패합니다. 로깅을 사용하여 예외를 트랩하고 처리합니다.

마찬가지로 JS 코드는 [JSInvokable] 특성으로 표시되는 .NET 메서드에 대한 호출을 시작할 수 있습니다. 이러한 .NET 메서드에서 처리되지 않은 예외를 throw하는 경우 다음이 수행됩니다.

  • 회로를 Blazor 통해 작동하는 앱에서 예외는 앱 회로에 치명적인 것으로 취급되지 않습니다 .
  • JS 쪽 Promise가 거부됩니다.

메서드 호출의 .NET 쪽 또는 JS 쪽에서 오류 처리 코드를 사용하는 옵션이 있습니다.

자세한 내용은 다음 문서를 참조하세요.

미리 렌더링

Razor 구성 요소는 렌더링된 HTML 태그가 사용자의 초기 HTTP 요청의 일부로 반환되도록 기본적으로 미리 렌더링됩니다.

회로를 Blazor 통해 작동하는 앱에서 사전 렌더링은 다음에서 작동합니다.

  • 동일한 페이지의 일부인 미리 렌더링된 모든 구성 요소에 대해 새 회로 생성
  • 초기 HTML 생성
  • 사용자의 브라우저가 동일한 서버에 대해 SignalR 연결을 다시 설정할 때까지 회로를 disconnected로 처리. 연결이 설정되면 회로의 상호 작용이 다시 시작되고 구성 요소의 HTML 태그가 업데이트됩니다.

미리 렌더링된 클라이언트 쪽 구성 요소의 경우 사전 렌더링은 다음에서 작동합니다.

  • 서버에서 동일한 페이지의 일부인 미리 렌더링된 모든 구성 요소에 대해 초기 HTML 생성
  • 브라우저가 앱의 컴파일된 코드와 .NET 런타임(아직 로드되지 않은 경우)을 백그라운드에서 로드한 후 클라이언트에서 구성 요소를 대화형으로 만들기

사전 렌더링 동안 구성 요소가 처리되지 않은 예외를 throw하는 경우(예: 수명 주기 방법 동안 또는 렌더링 논리에서)

  • 회로를 Blazor 통해 작동하는 앱에서 예외는 회로에 치명적입니다. 미리 렌더링된 클라이언트 쪽 구성 요소의 경우 예외는 구성 요소 렌더링을 방지합니다.
  • ComponentTagHelper의 호출 스택에 대해 예외가 throw됩니다.

일반적인 경우에는 사전 렌더링에 실패하면 작업 구성 요소를 렌더링할 수 없기 때문에 구성 요소를 계속 빌드 및 렌더링하는 것은 적합하지 않습니다.

렌더링 중에 발생할 수 있는 오류를 허용하려면 예외를 throw할 수 있는 오류 처리 논리를 구성 요소 내부에 배치해야 합니다. 오류 처리 및 로깅과 함께 try-catch 문을 사용합니다. try-catch 문에 ComponentTagHelper를 래핑하는 대신, ComponentTagHelper에 의해 렌더링되는 구성 요소에 오류 처리 논리를 배치합니다.

고급 시나리오

재귀 렌더링

구성 요소는 재귀적으로 중첩될 수 있습니다. 이것은 재귀적 데이터 구조를 나타내는 데 유용합니다. 예를 들어, TreeNode 구성 요소는 각 노드의 자식에 대해 더 많은 TreeNode 구성 요소를 렌더링할 수 있습니다.

반복적으로 렌더링하는 경우 무한 재귀가 발생하는 코딩 패턴을 사용하지 않습니다.

  • 주기가 포함된 데이터 구조는 재귀적으로 렌더링하지 않도록 합니다. 예를 들어, 자식에 자기 자신이 포함된 트리 노드를 렌더링하지 않도록 합니다.
  • 주기가 포함된 레이아웃 체인을 만들지 않도록 합니다. 예를 들어, 레이아웃만 있는 레이아웃은 만들지 않도록 합니다.
  • 최종 사용자가 악의적인 데이터 입력 또는 JavaScript interop 호출을 통해 재귀 고정 항목(규칙)을 위반할 수 없도록 합니다.

렌더링 중 무한 루프:

  • 렌더링 프로세스가 계속 지속되도록 합니다.
  • 종료되지 않는 루프를 만드는 것과 같습니다.

이러한 시나리오 Blazor 에서는 실패하고 일반적으로 다음을 시도합니다.

  • 운영 체제에서 허용하는 만큼의 CPU 시간을 무제한으로 사용합니다.
  • 메모리를 무제한으로 사용합니다. 무제한 메모리를 사용하는 것은 종료되지 않는 루프가 반복될 때마다 컬렉션에 항목을 추가하는 시나리오와 동일합니다.

무한 재귀 패턴을 방지하려면 재귀 렌더링 코드에 적절한 중지 조건이 포함되어 있는지 확인합니다.

사용자 지정 렌더링 트리 논리

대부분의 Razor 구성 요소는 Razor 구성 요소 파일(.razor)로 구현되며 RenderTreeBuilder에서 작동하여 출력을 렌더링하는 논리를 생성하도록 프레임워크에 의해 컴파일됩니다. 그러나 개발자는 프로시저 C# 코드를 사용하여 RenderTreeBuilder 논리를 수동으로 구현할 수 있습니다. 자세한 내용은 ASP.NET Core Blazor 고급 시나리오(렌더링 트리 생성)를 참조하세요.

Warning

수동 렌더링 트리 작성기 논리를 사용하는 것은 일반적인 구성 요소 개발에는 권장되지 않는 안전하지 않은 고급 시나리오로 간주됩니다.

RenderTreeBuilder 코드를 작성하는 경우 개발자는 코드의 정확성을 보장해야 합니다. 예를 들어, 개발자는 다음을 확인해야 합니다.

  • OpenElementCloseElement에 대한 호출은 올바르게 균형이 조정됩니다.
  • 특성은 올바른 위치에만 추가됩니다.

잘못된 수동 렌더링 트리 작성기 논리로 인해 크래시, 앱 또는 서버 중단, 보안 취약성 등 정의되지 않은 임의의 동작이 발생할 수 있습니다.

어셈블리 코드 또는 MSIL(Microsoft Intermediate Language) 명령을 직접 작성하는 것과 동일한 수준의 복잡성과 동일한 수준의 ‘위험’에서 수동 렌더링 트리 작성기 논리를 고려하세요.

추가 리소스

클라이언트 쪽 Blazor 앱이 로깅에 사용하는 백 엔드 ASP.NET Core 웹 API 앱에 적용됩니다.