다음을 통해 공유


ASP.NET Core Blazor 렌더링 성능 모범 사례

비고

이 기사는 최신 버전이 아닙니다. 현재 릴리스에 대해서는 본 기사의 .NET 9 버전을 참조하십시오.

경고

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조하세요. 현재 릴리스에 대해서는 본 기사의 .NET 9 버전을 참조하십시오.

중요합니다

이 정보는 사전 출시 제품과 관련이 있으며, 상업적으로 출시되기 전에 상당히 수정될 수 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적이거나 묵시적인 보증도 하지 않습니다.

현재 릴리스에 대해서는 본 기사의 .NET 9 버전을 참조하십시오.

렌더링 속도를 최적화하여 렌더링 워크로드를 최소화하고 UI 응답성을 향상합니다. 그러면 UI 렌더링 속도가 ‘10배 이상 향상’될 수 있습니다.

불필요한 구성 요소 하위 트리 렌더링 방지

자식 구성 요소 하위 트리의 렌더링을 이벤트가 발생할 때 건너뛰면 부모 구성 요소의 렌더링 비용을 대다수 줄일 수 있습니다. 렌더링 비용이 특히 많이 들고 UI 지연을 발생시키는 하위 트리의 다시 렌더링을 건너뛰는 부분만 신경쓰면 됩니다.

런타임에 구성 요소는 계층 구조에 있습니다. 루트 구성 요소(로드된 첫 번째 구성 요소)에는 자식 구성 요소가 있습니다. 차례대로 루트의 자식은 또 다른 자식 구성 요소를 가지고 있습니다. 사용자의 단추 선택과 같은 이벤트가 발생하는 경우 다음 프로세스에 따라 다시 렌더링할 구성 요소를 결정합니다.

  1. 이벤트가 이벤트 처리기를 렌더링한 구성 요소에 디스패치됩니다. 이벤트 처리기를 실행한 후 구성 요소가 다시 렌더링됩니다.
  2. 구성 요소가 다시 렌더링되면 매개 변수 값의 새 복사본을 각 자식 구성 요소에 제공합니다.
  3. 새 매개 변수 값 집합을 받은 Blazor 후 구성 요소를 다시 렌더링할지 여부를 결정합니다. 기본 동작으로 ShouldRendertrue을 반환하면 구성 요소가 다시 렌더링됩니다. 이는 재정의되지 않는 한 적용되며, 예를 들어 매개변수 값이 변경 가능한 개체일 때 변경되었을 수 있습니다.

위 시퀀스의 마지막 두 단계는 구성 요소 계층 구조 아래까지 반복적으로 계속됩니다. 대부분의 경우 전체 하위 트리는 다시 렌더링됩니다. 상위 수준 구성 요소 아래의 모든 구성 요소는 다시 렌더링되어야 하므로 상위 수준 구성 요소를 대상으로 하는 이벤트로 인해 다시 렌더링 비용이 증가할 수 있습니다.

특정 하위 트리에 대한 렌더링 반복을 방지하려면 다음 방법 중 하나를 사용하세요.

  • 자식 구성 요소 매개 변수가 변경할 수 없는 특정 형식인지 확인합니다†( 예: string, intboolDateTime. 변경 내용을 검색하기 위한 기본 제공 논리는 변경할 수 없는 매개 변수 값이 변경되지 않은 경우 자동으로 다시 렌더링을 건너뜁니다. <Customer CustomerId="item.CustomerId" />를 사용하여 자식 구성 요소를 렌더링하는 경우(여기서 CustomerIdint 형식임) Customer 구성 요소는 item.CustomerId가 변경되는 경우에만 다시 렌더링됩니다.
  • 재정의 ShouldRender, 반환 false:
    • 매개 변수가 기본이 아닌 형식이거나 지원되지 않는 변경할 수 없는 형식인 경우† 복잡한 사용자 지정 모델 형식 또는 RenderFragment 값과 같이 매개 변수 값이 변경되지 않은 경우
    • 매개 변수 값 변경에 관계없이 초기 렌더링 이후 변경되지 않는 UI 전용 구성 요소를 작성하는 경우입니다.

† 자세한 내용은 '의 참조 원본(Blazor)에서 ChangeDetection.cs변경 검색 논리를 참조하세요.

비고

문서 링크는 .NET 참조 소스를 가리키며, 일반적으로 저장소의 기본 브랜치를 로드합니다. 이는 .NET의 다음 릴리스를 위한 현재 개발 상태를 나타냅니다. 특정 릴리스를 위한 태그를 선택하려면 Switch branches or tags 드롭다운 목록을 사용하세요. 자세한 내용은 ASP.NET Core 소스 코드(dotnet/AspNetCore.Docs #26205)의 버전 태그를 선택하는 방법을 참조하세요.

다음 항공편 검색 도구 예제에서는 프라이빗 필드를 사용하여 변경 내용을 검색하는 데 필요한 정보를 추적합니다. 이전 인바운드 항공편 식별자(prevInboundFlightId)와 이전 아웃바운드 항공편 식별자(prevOutboundFlightId)는 잠재적인 다음 구성 요소 업데이트에 대한 정보를 추적합니다. OnParametersSet에서 구성 요소의 매개 변수를 설정할 때 항공편 식별자 중 하나가 변경되면 shouldRendertrue로 설정되어 있기 때문에 구성 요소가 다시 렌더링됩니다. 항공편 식별자를 확인한 후 shouldRenderfalse로 평가되면 비용이 많이 드는 다시 렌더링을 수행하지 않아도 됩니다.

@code {
    private int prevInboundFlightId = 0;
    private int prevOutboundFlightId = 0;
    private bool shouldRender;

    [Parameter]
    public FlightInfo? InboundFlight { get; set; }

    [Parameter]
    public FlightInfo? OutboundFlight { get; set; }

    protected override void OnParametersSet()
    {
        shouldRender = InboundFlight?.FlightId != prevInboundFlightId
            || OutboundFlight?.FlightId != prevOutboundFlightId;

        prevInboundFlightId = InboundFlight?.FlightId ?? 0;
        prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
    }

    protected override bool ShouldRender() => shouldRender;
}

이벤트 처리기도 shouldRendertrue로 설정할 수 있습니다. 일반적으로 대부분의 구성 요소에 대해 개별 이벤트 처리기 수준에서 다시 렌더링을 결정할 필요가 없습니다.

자세한 내용은 다음 리소스를 참조하세요.

가상화

수천 개 항목을 포함하는 목록 또는 그리드와 같은 많은 양의 UI를 루프 내에서 렌더링할 때 렌더링 작업량이 많으면 UI 렌더링이 지연될 수 있습니다. 사용자가 스크롤하지 않고 한 번에 적은 수의 요소만 볼 수 있다면 현재 표시되지 않는 요소를 렌더링하는 데 불필요하게 시간이 소요될 수 있습니다.

Blazor는 임의로 커질 수 있는 목록의 모양 및 스크롤 동작을 만들지만 현재 스크롤 뷰포트에 있는 목록 항목만 렌더링하는 Virtualize<TItem> 구성 요소를 제공합니다. 예를 들어 구성 요소는 100,000개 항목이 포함된 목록을 렌더링할 수 있지만 표시되는 20개 항목의 렌더링 비용만 지급합니다.

자세한 내용은 ASP.NET Core Razor 구성 요소 유효성 검사를 참조하세요.

간단하고 최적화된 구성 요소 만들기

대부분의 구성 요소가 UI에서 반복되지 않고 자주 다시 렌더링되지 않기 때문에 대부분의 Razor 구성 요소에 적극적인 최적화 작업이 필요하지 않습니다. 예를 들어 @page 지시문을 사용하는 라우팅 가능한 구성 요소와 상위 수준의 UI(예: 대화 상자 또는 양식)를 렌더링하는 데 사용되는 구성 요소는 한 번에 하나만 표시되고 사용자 제스처에 대한 응답으로만 다시 렌더링될 가능성이 많습니다. 일반적으로 이러한 구성 요소의 렌더링 워크로드는 많지 않으므로 렌더링 성능을 걱정하지 않고도 원하는 프레임워크 기능의 조합을 자유롭게 사용할 수 있습니다.

그러나 구성 요소가 대규모로 반복되고 종종 UI 성능이 저하되는 일반적인 시나리오가 있습니다.

  • 입력 또는 레이블과 같은 수백 개의 개별 요소가 포함된 대규모 중첩된 양식
  • 수백 개의 행 또는 수천 개의 셀이 있는 그리드
  • 수백만 개의 데이터 포인트를 가진 산점도

각 요소, 셀 또는 데이터 포인트를 개별 구성 요소 인스턴스로 모델링하는 경우 렌더링 성능이 중요한 경우가 많습니다. 이 섹션에서는 UI가 빠르고 뛰어난 응답성을 유지하도록 해당 구성 요소를 간단하게 만드는 방법에 관한 지침을 제공합니다.

수천 개의 구성 요소 인스턴스 방지

각 구성 요소는 부모 및 자식과 독립적으로 렌더링할 수 있는 분리된 섬입니다. UI를 구성 요소 계층 구조로 분할하는 방법을 선택하면 UI 렌더링의 세분성을 제어하게 됩니다. 그러면 성능이 향상되거나 저하될 수 있습니다.

UI를 별도의 구성 요소로 분할하면 이벤트가 발생할 때 다시 렌더링되는 UI 부분을 줄일 수 있습니다. 각 행에 단추가 있는 행이 많은 테이블에서 전체 페이지나 테이블 대신 자식 구성 요소를 사용하면 단일 행만 다시 렌더링되도록 할 수 있습니다. 그러나 각 구성 요소에서 독립적인 상태 및 렌더링 수명 주기를 처리하려면 추가 메모리 및 CPU 오버헤드가 필요합니다.

ASP.NET Core 제품 단위 엔지니어가 수행한 테스트에서는 구성 요소 인스턴스당 약 0.06ms의 렌더링 오버헤드가 Blazor WebAssembly 앱에서 확인되었습니다. 테스트 앱은 세 개의 매개 변수를 허용하는 간단한 구성 요소를 렌더링했습니다. 내부적으로 대부분 오버헤드는 사전에서 구성 요소별 상태를 검색하고 매개 변수를 전달 및 수신하기 때문에 발생합니다. 곱하기를 통해 2,000개 추가 구성 요소 인스턴스를 추가하면 렌더링 시간이 0.12초 늘어나고 사용자에게 UI가 느리게 느껴지는 것을 알 수 있습니다.

구성 요소를 더 가볍게 만들어 더 많이 사용할 수 있습니다. 그러나 너무 많은 구성 요소가 렌더링되지 않도록 하는 것이 보다 효과적인 기법입니다. 다음 섹션에서는 사용할 수 있는 두 가지 접근 방식을 설명합니다.

메모리 관리에 대한 자세한 내용은 배포된 ASP.NET Core 서버 쪽 Blazor 앱에서 메모리 관리를 참조하세요.

자식 구성 요소를 부모 내에 인라인으로 삽입하기: 다음과 같이 루프에서 자식 구성 요소를 렌더링하는 부모 구성 요소의 한 부분을 고려합니다.

<div class="chat">
    @foreach (var message in messages)
    {
        <ChatMessageDisplay Message="message" />
    }
</div>

ChatMessageDisplay.razor:

<div class="chat-message">
    <span class="author">@Message.Author</span>
    <span class="text">@Message.Text</span>
</div>

@code {
    [Parameter]
    public ChatMessage? Message { get; set; }
}

위의 예제는 수천 개의 메시지가 한 번에 표시되지 않는 한 제대로 작동합니다. 한 번에 수천 개의 메시지를 표시하려면 별도의 구성 요소를 분리하지 ‘않는’ 것이 좋습니다.ChatMessageDisplay 대신 자식 구성 요소를 부모에 포함시킵니다. 다음 접근 방법은 너무 많은 자식 구성 요소를 렌더링할 때 발생하는 구성 요소별 오버헤드를 피하기 위해 각 자식 구성 요소의 마크업을 개별적으로 다시 렌더링할 수 있는 기능이 손실됩니다.

<div class="chat">
    @foreach (var message in messages)
    {
        <div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>
    }
</div>

코드에서 재사용 가능한 RenderFragments 정의: 렌더링 논리를 재사용하기 위해 순수하게 자식 구성 요소를 추출할 수 있습니다. 이 경우 추가 구성 요소를 구현하지 않고도 재사용 가능한 렌더링 논리를 만들 수 있습니다. 구성 요소의 @code 블록에서 RenderFragment를 정의합니다. 필요할 때마다 모든 위치에서 조각을 렌더링합니다.

@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}

여러 구성 요소에서 코드를 재사용할 수 있도록 RenderTreeBuilder 하려면 다음을 RenderFragmentpublic 선언합니다 static.

public static RenderFragment SayHello = @<h1>Hello!</h1>;

위의 예제에서 SayHello는 관련 없는 구성 요소에서 호출할 수 있습니다. 이 기법은 구성 요소별 오버헤드 없이 렌더링되는 재사용 가능한 태그 코드 조각의 라이브러리를 빌드하는 데 유용합니다.

RenderFragment 대리자는 매개 변수를 허용할 수 있습니다. 다음 구성 요소는 메시지(message)를 RenderFragment 대리자에 전달합니다.

<div class="chat">
    @foreach (var message in messages)
    {
        @ChatMessageDisplay(message)
    }
</div>

@code {
    private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
        @<div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>;
}

위의 방법은 구성 요소별 오버헤드 없이 렌더링 논리를 다시 사용합니다. 그러나 이 방법은 UI의 하위 트리를 독립적으로 새로 고치는 것을 허용하지 않으며, 구성 요소 경계가 없기 때문에 부모가 렌더링할 때 UI의 하위 트리 렌더링을 건너뛰는 기능도 없습니다. RenderFragment 대리자 할당은 Razor 구성 요소 파일(.razor)에서만 지원됩니다.

다음 예제의 TitleTemplate과 같이 필드 이니셜라이저가 참조할 수 없는 비정적 필드, 메서드 또는 속성의 경우, RenderFragment의 필드 대신 속성을 사용합니다.

protected RenderFragment DisplayTitle =>
    @<div>
        @TitleTemplate
    </div>;

너무 많은 매개 변수를 수신하지 않음

구성 요소가 너무 자주(예: 수백 또는 수천 번) 반복되는 경우 각 매개 변수를 전달하고 받는 오버헤드가 누적될 수 있습니다.

너무 많은 매개 변수가 성능을 크게 제한하는 경우는 드물지만 한 가지 요인이 될 수 있습니다. TableCell 그리드 내에서 4,000번 렌더링하는 구성 요소의 경우 구성 요소에 전달된 각 매개 변수는 총 렌더링 비용에 약 15ms를 추가합니다. 10개의 매개 변수를 전달하려면 약 150ms가 필요하며 UI 렌더링 지연이 발생합니다.

매개 변수 로드를 줄이려면 여러 매개 변수를 사용자 지정 클래스에 번들로 묶습니다. 예를 들어 테이블 셀 구성 요소는 공용 개체를 허용할 수 있습니다. 다음 예제에서 Data는 모든 셀에서 서로 다르지만 Options는 모든 셀 인스턴스에서 공통적입니다.

@typeparam TItem

...

@code {
    [Parameter]
    public TItem? Data { get; set; }
    
    [Parameter]
    public GridOptions? Options { get; set; }
}

그러나 기본 매개 변수를 클래스에 묶는 것이 항상 장점은 아닙니다. 매개 변수 수를 줄일 수 있지만 변경 검색 및 렌더링이 동작하는 방식에도 영향을 줍니다. 기본이 아닌 매개 변수를 전달하면 항상 다시 렌더링이 트리거됩니다. Blazor 임의의 개체에 내부적으로 변경 가능한 상태가 있는지 여부를 알 수 없는 반면 기본 매개 변수를 전달하면 해당 값이 실제로 변경된 경우에만 다시 렌더링이 트리거됩니다.

또한, 앞서 보여준 예제처럼 테이블 셀 구성 요소를 사용하지 않는 것이 개선이 될 수 있으며, 대신 그 논리를 부모 구성 요소에 인라인으로 포함시키는 것을 고려하십시오.

비고

성능 향상을 위해 여러 가지 방법을 사용할 수 있는 경우 최상의 결과를 얻을 수 있는 방법을 결정하려면 일반적으로 벤치마킹이 필요할 수 있습니다.

제네릭 형식 매개 변수(@typeparam)에 대한 자세한 내용은 다음 리소스를 참조하세요.

연계 매개 변수가 고정되어 있는지 확인

CascadingValue 구성 요소에는 선택적 IsFixed 매개 변수가 있습니다.

  • IsFixedfalse(기본값)이면 연계된 값의 모든 수신자가 변경 알림을 받도록 구독을 설정합니다. 각 [CascadingParameter]는 구독 추적으로 인하여 일반 보다 비용이 상당히 더 많이 듭니다.
  • IsFixedtrue(예: <CascadingValue Value="someValue" IsFixed="true">)이면 수신자는 초기 값을 수신하지만 업데이트를 수신하도록 구독을 설정하지 않습니다. 각 [CascadingParameter]는 간단하며 일반 [Parameter] 보다 비용이 더 많이 들지 않습니다.

IsFixedtrue로 설정하면 연계된 값을 수신하는 다른 구성 요소가 많은 경우 성능이 향상됩니다. 가능하면 연계된 값에서 IsFixedtrue로 설정합니다. 제공된 값이 시간이 지남에 따라 변경되지 않는 경우 IsFixedtrue로 설정할 수 있습니다.

구성 요소가 this을 계단식 값으로 전달하는 경우, 구성 요소의 생명 주기 동안 IsFixed이 변경되지 않기 때문에 truethis로 설정할 수도 있습니다.

<CascadingValue Value="this" IsFixed="true">
    <SomeOtherComponents>
</CascadingValue>

자세한 내용은 ASP.NET Core Blazor 연계 값 및 매개 변수를 참조하세요.

CaptureUnmatchedValues를 사용하여 특성 스플래팅 방지

구성 요소는 CaptureUnmatchedValues 플래그를 사용하여 “일치하지 않는” 매개 변수 값을 수신하도록 선택할 수 있습니다.

<div @attributes="OtherAttributes">...</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? OtherAttributes { get; set; }
}

이 접근 방식을 사용하면 임의 추가 특성을 요소에 전달할 수 있습니다. 그러나 이 접근 방식에서는 렌더러가 다음을 충족해야 하기 때문에 비용이 많이 듭니다.

  • 제공된 모든 매개 변수를 알려진 매개 변수 세트에 대조하여 사전을 빌드합니다.
  • 동일한 특성의 얼마나 많은 복사본이 서로 덮어쓰는지 추적합니다.

자주 반복되지 않는 구성 요소와 같이 구성 요소 렌더링 성능이 중요하지 않은 경우 CaptureUnmatchedValues를 사용합니다. 큰 목록의 각 항목이나 그리드의 셀과 같이 대규모로 렌더링되는 구성 요소의 경우 특성 스플래팅을 방지합니다.

자세한 내용은 ASP.NET Core Blazor 특성 스플래팅 및 임의 매개 변수를 참조하세요.

수동으로 SetParametersAsync 구현

구성 요소별 렌더링 오버헤드의 주요 측면 중 하나는 들어오는 매개 변수 값을 [Parameter] 속성에 기록하는 것입니다. 렌더러는 리플렉션을 사용하여 매개 변수 값을 작성하므로 대규모 성능이 저하됩니다.

일부 극단적인 경우에는 리플렉션을 피하고 자체 매개 변수 설정 논리를 수동으로 구현하려고 할 수 있습니다. 여기에 해당할 수 있는 경우는 다음과 같습니다.

  • 구성 요소가 너무 자주 렌더링됩니다(예: UI에 수백 또는 수천 개의 구성 요소 복사본이 있는 경우).
  • 구성 요소가 많은 매개 변수를 허용합니다.
  • 매개 변수를 수신하는 오버헤드가 UI 응답성에 관찰 가능한 영향을 미치는 것을 알게 된 경우.

극단적인 경우에는 구성 요소의 가상 SetParametersAsync 메서드를 재정의하고 자체 구성 요소별 논리를 구현할 수 있습니다. 다음 예제에서는 사전 조회를 의도적으로 방지합니다.

@code {
    [Parameter]
    public int MessageId { get; set; }

    [Parameter]
    public string? Text { get; set; }

    [Parameter]
    public EventCallback<string> TextChanged { get; set; }

    [Parameter]
    public Theme CurrentTheme { get; set; }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(MessageId):
                    MessageId = (int)parameter.Value;
                    break;
                case nameof(Text):
                    Text = (string)parameter.Value;
                    break;
                case nameof(TextChanged):
                    TextChanged = (EventCallback<string>)parameter.Value;
                    break;
                case nameof(CurrentTheme):
                    CurrentTheme = (Theme)parameter.Value;
                    break;
                default:
                    throw new ArgumentException($"Unknown parameter: {parameter.Name}");
            }
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }
}

이전 코드에서 기본 클래스 SetParametersAsync 를 반환하면 매개 변수를 다시 할당하지 않고 일반 수명 주기 메서드가 실행됩니다.

위의 코드에서 알 수 있듯이 SetParametersAsync를 재정의하고 사용자 지정 논리를 제공하는 것은 복잡하고 힘들기 때문에 일반적으로 이 접근 방식을 권장하지 않습니다. 극단적인 경우 렌더링 성능이 20~25%까지 향상될 수 있지만 이 접근 방식은 이 섹션의 앞부분에 나열된 극단적인 시나리오에서만 사용하는 것이 좋습니다.

이벤트를 너무 빨리 트리거하지 않음

일부 브라우저 이벤트는 매우 빈번하게 일어납니다. 예를 들어 onmousemoveonscroll은 초당 수십 번 또는 수백 번 발사될 수 있습니다. 대부분의 경우 UI 업데이트는 이렇게 자주 수행할 필요가 없습니다. 이벤트가 너무 빨리 트리거되면 UI 응답성이 손상되거나 과도한 CPU 시간이 사용될 수 있습니다.

빠르게 발생하는 네이티브 이벤트를 사용하기보다는 JS 인터롭을 사용하여 더 적은 빈도로 발생하는 콜백을 등록하는 것을 고려해 보십시오. 예를 들어 다음 구성 요소는 마우스의 위치를 표시하지만 500ms마다 한 번만 업데이트됩니다.

@implements IDisposable
@inject IJSRuntime JS

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">
    Move mouse here
</div>

@code {
    private ElementReference mouseMoveElement;
    private DotNetObjectReference<MyComponent>? selfReference;
    private string message = "Move the mouse in the box";

    [JSInvokable]
    public void HandleMouseMove(int x, int y)
    {
        message = $"Mouse move at {x}, {y}";
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfReference = DotNetObjectReference.Create(this);
            var minInterval = 500;

            await JS.InvokeVoidAsync("onThrottledMouseMove", 
                mouseMoveElement, selfReference, minInterval);
        }
    }

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

해당 JavaScript 코드는 마우스 이동에 대한 DOM 이벤트 수신기를 등록합니다. 이 예제에서 이벤트 수신기는 Lodash의 throttle 함수를 사용하여 호출 빈도를 제한합니다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
  function onThrottledMouseMove(elem, component, interval) {
    elem.addEventListener('mousemove', _.throttle(e => {
      component.invokeMethodAsync('HandleMouseMove', e.offsetX, e.offsetY);
    }, interval));
  }
</script>

상태를 변경하지 않고 이벤트를 처리한 후 다시 렌더링하는 것을 방지합니다.

구성 요소는 ComponentBase에서 상속되며, 구성 요소의 이벤트 처리기가 호출된 후 자동으로 StateHasChanged이 호출됩니다. 일부 경우에는 이벤트 처리기가 호출된 후 다시 렌더링을 트리거하는 것이 불필요하거나 바람직하지 않을 수 있습니다. 예를 들어 이벤트 처리기가 구성 요소 상태를 수정하지 못할 수 있습니다. 이러한 시나리오에서 앱은 IHandleEvent 인터페이스를 활용하여 Blazor의 이벤트 처리 동작을 제어할 수 있습니다.

비고

이 섹션의 접근 방식은 예외를 오류 경계로 전달하지 않습니다. 호출ComponentBase.DispatchExceptionAsync하여 오류 경계를 지원하는 자세한 내용 및 데모 코드는 AsNonRenderingEventHandler + ErrorBoundary = 예기치 않은 동작(dotnet/aspnetcore#54543)을 참조하세요.

모든 구성 요소의 이벤트 처리기가 다시 렌더링되는 것을 방지하려면 IHandleEvent를 구현하고 IHandleEvent.HandleEventAsync를 호출하지 않고 이벤트 처리기를 호출하는 StateHasChanged 작업을 제공하세요.

다음 예제에서는 구성 요소에 추가된 이벤트 처리기가 렌더링을 다시 트리거하지 않으므로 HandleSelect이 호출될 때 다시 렌더링되지 않습니다.

HandleSelect1.razor:

@page "/handle-select-1"
@using Microsoft.Extensions.Logging
@implements IHandleEvent
@inject ILogger<HandleSelect1> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleSelect">
    Select me (Avoids Rerender)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleSelect()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }

    Task IHandleEvent.HandleEventAsync(
        EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg);
}

글로벌 방식으로 구성 요소 내에서 이벤트 처리기가 발생한 후의 다시 렌더링을 방지할 뿐만 아니라, 다음 유틸리티 메서드를 사용하여 단일 이벤트 처리기 후에도 다시 렌더링을 막을 수 있습니다.

EventUtil 앱에 다음 Blazor 클래스를 추가합니다. EventUtil 클래스의 가장 위에 있는 정적 작업과 함수는 Blazor가 이벤트를 처리할 때 사용하는 여러 인수 조합 및 반환 유형을 처리하는 처리기를 제공합니다.

EventUtil.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

public static class EventUtil
{
    public static Action AsNonRenderingEventHandler(Action callback)
        => new SyncReceiver(callback).Invoke;
    public static Action<TValue> AsNonRenderingEventHandler<TValue>(
            Action<TValue> callback)
        => new SyncReceiver<TValue>(callback).Invoke;
    public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
        => new AsyncReceiver(callback).Invoke;
    public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
            Func<TValue, Task> callback)
        => new AsyncReceiver<TValue>(callback).Invoke;

    private record SyncReceiver(Action callback) 
        : ReceiverBase { public void Invoke() => callback(); }
    private record SyncReceiver<T>(Action<T> callback) 
        : ReceiverBase { public void Invoke(T arg) => callback(arg); }
    private record AsyncReceiver(Func<Task> callback) 
        : ReceiverBase { public Task Invoke() => callback(); }
    private record AsyncReceiver<T>(Func<T, Task> callback) 
        : ReceiverBase { public Task Invoke(T arg) => callback(arg); }

    private record ReceiverBase : IHandleEvent
    {
        public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => 
            item.InvokeAsync(arg);
    }
}

호출될 때 렌더링을 트리거하지 않는 이벤트 처리기를 호출하려면 EventUtil.AsNonRenderingEventHandler를 호출하세요.

다음 예제에서

  • HandleClick1를 호출하는 첫 번째 단추를 선택하면 렌더링이 다시 트리거됩니다.
  • HandleClick2를 호출하는 두 번째 단추를 선택하면 렌더링이 다시 트리거되지 않습니다.
  • HandleClick3를 호출하는 세 번째 버튼을 선택해도 렌더링이 다시 수행되지 않고 이벤트 인수(MouseEventArgs)가 사용됩니다.

HandleSelect2.razor:

@page "/handle-select-2"
@using Microsoft.Extensions.Logging
@inject ILogger<HandleSelect2> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleClick1">
    Select me (Rerenders)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler(HandleClick2)">
    Select me (Avoids Rerender)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(HandleClick3)">
    Select me (Avoids Rerender and uses <code>MouseEventArgs</code>)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleClick1()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler triggers a rerender.");
    }

    private void HandleClick2()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }
    
    private void HandleClick3(MouseEventArgs args)
    {
        dt = DateTime.Now;

        Logger.LogInformation(
            "This event handler doesn't trigger a rerender. " +
            "Mouse coordinates: {ScreenX}:{ScreenY}", 
            args.ScreenX, args.ScreenY);
    }
}

IHandleEvent 인터페이스를 구현하는 것 외에 이 문서에 설명된 다른 모범 사례를 활용하면 이벤트가 처리된 후 원치 않는 렌더링을 줄일 수 있습니다. 예를 들어 대상 구성 요소의 하위 구성 요소에서 ShouldRender을 대체하여 다시 렌더링하는 것을 제어할 수 있습니다.

반복되는 여러 요소 또는 구성 요소에 대한 대리자 다시 만들기 방지

루프 내 요소 또는 구성 요소에 대한 Blazor의 람다 식 대리자 재구현은 성능 저하를 초래할 수 있습니다.

이벤트 처리 문서에 나오는 다음 구성 요소는 단추 집합을 렌더링합니다. 각 단추는 해당 이벤트에 대리자를 @onclick 할당합니다. 렌더링할 단추가 많지 않으면 괜찮습니다.

EventHandlerExample5.razor:

@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}

위의 방법을 사용하여 많은 수의 단추를 렌더링하는 경우 렌더링 속도가 저하되고 사용자 환경에 문제가 발생합니다. 클릭 이벤트에 대한 콜백을 사용하여 많은 단추를 렌더링하기 위해, 다음 예제에서는 각 단추의 @onclick 대리자를 Action에 할당하는 단추 개체 컬렉션을 사용합니다. 다음 방법을 사용하면 Blazor에서는 단추가 렌더링될 때마다 모든 단추 대리자를 다시 빌드하지 않아도 됩니다.

LambdaEventPerformance.razor:

@page "/lambda-event-performance"

<h1>@heading</h1>

@foreach (var button in Buttons)
{
    <p>
        <button @key="button.Id" @onclick="button.Action">
            Button #@button.Id
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private List<Button> Buttons { get; set; } = new();

    protected override void OnInitialized()
    {
        for (var i = 0; i < 100; i++)
        {
            var button = new Button();

            button.Id = Guid.NewGuid().ToString();

            button.Action = (e) =>
            {
                UpdateHeading(button, e);
            };

            Buttons.Add(button);
        }
    }

    private void UpdateHeading(Button button, MouseEventArgs e)
    {
        heading = $"Selected #{button.Id} at {e.ClientX}:{e.ClientY}";
    }

    private class Button
    {
        public string? Id { get; set; }
        public Action<MouseEventArgs> Action { get; set; } = e => { };
    }
}