ASP.NET Core Razor 구성 요소 렌더링

참고 항목

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

Important

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

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

이 문서에서는 렌더링할 구성 요소를 수동으로 트리거하기 위해 StateHasChanged를 호출하는 시기를 포함하여 ASP.NET Core Blazor 앱의 Razor 구성 요소 렌더링을 설명합니다.

ComponentBase의 렌더링 규칙

구성 요소는 부모 구성 요소에 의해 구성 요소 계층 구조에 처음 추가될 때 ‘렌더링해야 합니다’. 구성 요소는 이 시점에만 렌더링해야 합니다. 구성 요소는 자체 논리 및 규칙에 따라 다른 시간에 ‘렌더링할 수 있습니다’.

기본적으로 Razor 구성 요소는 다음 시간에 다시 렌더링을 트리거하는 논리를 포함하는 ComponentBase 기본 클래스에서 상속됩니다.

ComponentBase에서 상속된 구성 요소는 다음 중 하나에 해당하는 경우 매개 변수 업데이트로 인해 다시 렌더링을 건너뜁니다.

  • 모든 매개 변수는 알려진 형식 집합† 또는 이전 매개 변수 집합이 설정된 이후로 변경되지 않은 모든 기본 형식에서 가져온 것입니다.

    †Blazor 프레임워크는 기본 제공 규칙 집합 및 변경 검색에 대한 명시적 매개 변수 형식 검사를 사용합니다. 이러한 규칙 및 형식은 언제든지 변경될 수 있습니다. 자세한 내용은 ASP.NET Core 참조 소스의 ChangeDetection API를 참조하세요.

    참고 항목

    .NET 참조 원본의 설명서 링크는 일반적으로 다음 릴리스의 .NET을 위한 현재 개발을 나타내는 리포지토리의 기본 분기를 로드합니다. 특정 릴리스를 위한 태그를 선택하려면 Switch branches or tags(분기 또는 태그 전환) 드롭다운 목록을 사용합니다. 자세한 내용은 ASP.NET Core 소스 코드(dotnet/AspNetCore.Docs #26205)의 버전 태그를 선택하는 방법을 참조하세요.

  • 구성 요소 메서드의 재정의 ShouldRender 가 반환됩니다 false (기본 ComponentBase 구현은 항상 반환true됨).

렌더링 흐름 제어

대부분의 경우 ComponentBase 규칙은 이벤트가 발생한 후 구성 요소 다시 렌더링의 올바른 하위 세트를 생성합니다. 개발자는 일반적으로 프레임워크에 다시 렌더링할 구성 요소와 이 구성 요소를 다시 렌더링할 시기를 알리는 수동 논리를 제공할 필요가 없습니다. 프레임워크 규칙의 전반적인 효과는 이벤트를 수신하는 구성 요소가 스스로 다시 렌더링되어 매개 변수 값이 변경되었을 수 있는 하위 구성 요소의 다시 렌더링을 재귀적으로 트리거합니다.

프레임워크 규칙의 성능 영향과 렌더링에 맞게 앱의 구성 요소 계층 구조를 최적화하는 방법에 관한 자세한 내용은 ASP.NET Core Blazor 성능 모범 사례를 참조하세요.

스트리밍 렌더링

정적 서버 쪽 렌더링(정적 SSR) 또는 미리 렌더링을 사용하여 응답 스트림에서 콘텐츠 업데이트를 스트리밍하고 장기 실행 비동기 작업을 수행하는 구성 요소의 사용자 환경을 개선하여 완전히 렌더링합니다.

예를 들어 페이지가 로드되면 데이터를 렌더링하기 위해 장기 실행 데이터베이스 쿼리 또는 웹 API 호출을 만드는 구성 요소를 고려합니다. 일반적으로 서버 쪽 구성 요소 렌더링의 일부로 실행되는 비동기 작업은 렌더링된 응답을 보내기 전에 완료되어야 하므로 페이지 로드가 지연될 수 있습니다. 페이지를 렌더링하는 데 상당한 지연이 있으면 사용자 환경에 해를 끼칩니다. 사용자 환경을 개선하기 위해 스트리밍 렌더링은 처음에는 비동기 작업이 실행되는 동안 자리 표시자 콘텐츠로 전체 페이지를 신속하게 렌더링합니다. 작업이 완료되면 업데이트된 콘텐츠가 동일한 응답 연결에서 클라이언트로 전송되고 DOM에 패치됩니다.

스트리밍 렌더링을 사용하려면 서버에서 출력 버퍼링을 피해야 합니다. 응답 데이터는 데이터가 생성될 때 클라이언트로 전달되어야 합니다. 버퍼링을 적용하는 호스트의 경우 스트리밍 렌더링이 정상적으로 저하되고 스트리밍 렌더링 없이 페이지가 로드됩니다.

정적 서버 쪽 렌더링(정적 SSR) 또는 미리 렌더링을 사용할 때 콘텐츠 업데이트를 스트리밍하려면 구성 요소에 [StreamRendering(true)] 특성을 적용합니다. 스트리밍 업데이트로 인해 페이지의 콘텐츠가 이동될 수 있으므로 스트리밍 렌더링을 명시적으로 사용하도록 설정해야 합니다. 부모 구성 요소가 이 기능을 사용하는 경우 특성이 없는 구성 요소는 스트리밍 렌더링을 자동으로 채택합니다. 자식 구성 요소의 특성에 전달 false 하여 해당 지점에서 기능을 사용하지 않도록 설정하고 구성 요소 하위 트리를 더 아래로 내려갈 수 있습니다. 이 특성은 클래스 라이브러리에서 제공하는 구성 요소에 Razor 적용할 때 작동합니다.

다음 예제는 웹앱 프로젝트 템플릿에서 만든 앱의Blazor구성 요소를 기반으로 Weather 합니다. 날씨 데이터 검색을 Task.Delay 비동기적으로 시뮬레이트하는 호출입니다. 구성 요소는 비동기 지연이 완료되는 것을 기다리지 않고 처음에 자리 표시자 콘텐츠("Loading...")를 렌더링합니다. 비동기 지연이 완료되고 날씨 데이터 콘텐츠가 생성되면 콘텐츠가 응답으로 스트리밍되고 일기 예보 테이블에 패치됩니다.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

UI 새로 고침 중지(ShouldRender)

구성 요소를 렌더링할 때마다 ShouldRender가 호출됩니다. UI 새로 고침을 관리하려면 ShouldRender를 재정의합니다. 구현에서 true를 반환하는 경우 UI가 새로 고침됩니다.

ShouldRender를 재정의한 경우에도 처음에는 구성 요소가 항상 렌더링됩니다.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

ShouldRender 관련 성능 모범 사례에 대한 자세한 내용은 ASP.NET Core Blazor 성능 모범 사례를 참조하세요.

StateHasChanged를 호출하는 경우

StateHasChanged를 호출하여 언제든지 렌더링을 트리거할 수 있습니다. 그러나 불필요하게 StateHasChanged를 호출하지 않도록 주의해야 합니다. 해당 호출은 불필요한 렌더링 비용을 발생시키는 일반적인 실수입니다.

다음과 같은 경우 코드에서 StateHasChanged를 호출할 필요가 없습니다.

  • ComponentBase가 대부분 루틴 이벤트 처리기의 렌더링을 트리거한 후 동기적 또는 비동기적으로 이벤트를 정기적으로 처리하는 경우.
  • ComponentBase가 일반적인 수명 주기 이벤트의 렌더링을 트리거한 후 동기적 또는 비동기적 여부와 관계없이 OnInitialized 또는 OnParametersSetAsync와 같은 일반적인 수명 주기 논리를 구현하는 경우

그러나 이 문서의 다음 섹션에서 설명하는 사례에서는 StateHasChanged를 호출하는 것이 적합할 수 있습니다.

비동기 처리기는 여러 비동기 단계를 포함함

.NET에서 작업이 정의되는 방식으로 인해 Task의 수신자는 중간 비동기 상태가 아니라 최종 완료만 관찰할 수 있습니다. 따라서 ComponentBaseTask가 처음 반환될 경우와 Task가 마지막으로 완료될 경우에만 다시 렌더링을 트리거할 수 있습니다. IAsyncEnumerable<T>이(가) 일련의 중간 Task에서 데이터를 반환하는 경우와 같이 다른 중간 지점에서 구성 요소를 렌더링하는 방법을 프레임워크에서는 알 수 없습니다. 중간 지점에서 다시 렌더링하려면 해당 지점에서 StateHasChanged를 호출합니다.

메서드가 실행 될 때마다 IncrementCount 4 번 수를 업데이트 하는 다음 CounterState1 구성 요소를 고려 합니다.

  • 자동 렌더링은 currentCount의 처음과 마지막 증분 후에 수행됩니다.
  • currentCount가 증가하는 중간 처리 지점에서 프레임워크가 다시 렌더링을 자동으로 트리거하지 않는 경우 StateHasChanged를 호출하여 수동 렌더링을 트리거합니다.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Blazor 렌더링 및 이벤트 처리 시스템 외부에서 호출 수신

ComponentBase는 자체 수명 주기 메서드 및 Blazor 트리거 이벤트만 알 수 있습니다. ComponentBase는 코드에서 발생할 수 있는 다른 이벤트를 알 수 없습니다. 예를 들어, 사용자 지정 데이터 저장소에서 발생한 C# 이벤트는 Blazor에서 알 수 없습니다. 해당 이벤트가 다시 렌더링을 트리거하여 UI에 업데이트된 값을 표시하게 하려면 StateHasChanged를 호출합니다.

System.Timers.Timer를 사용하여 정기적으로 개수를 업데이트하고 StateHasChanged를 호출하여 UI를 업데이트하는 다음 CounterState2 구성 요소를 고려합니다.

  • OnTimerCallback은 Blazor 관리형 렌더링 흐름 또는 이벤트 알림 외부에서 실행됩니다. Blazor는 콜백에서 currentCount의 변경을 인식하지 못하므로 OnTimerCallbackStateHasChanged를 호출해야 합니다.
  • 구성 요소는 IDisposable을 구현합니다. 여기서 Timer는 프레임워크가 Dispose 메서드를 호출할 때 삭제됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 수명 주기를 참조하세요.

콜백은 Blazor의 동기화 컨텍스트 외부에서 호출되기 때문에 구성 요소는 ComponentBase.InvokeAsync에서 OnTimerCallback의 논리를 래핑하여 렌더러의 동기화 컨텍스트로 이동해야 합니다. 이는 다른 UI 프레임워크의 UI 스레드로 마샬링하는 것과 같습니다. StateHasChanged는 렌더러의 동기화 컨텍스트에서만 호출할 수 있으며 이외의 경우에는 예외를 throw합니다.

System.InvalidOperationException: '현재 스레드가 Dispatcher와 연결되어 있지 않습니다. InvokeAsync()를 사용하여 렌더링 또는 구성 요소 상태를 트리거할 때 Dispatcher를 실행 상태로 전환합니다.'

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

특정 이벤트가 다시 렌더링하는 하위 트리 외부에서 구성 요소를 렌더링하려면

UI에는 다음이 포함될 수 있습니다.

  1. 구성 요소로 이벤트 디스패치.
  2. 일부 상태 변경.
  3. 이벤트를 수신하는 구성 요소의 하위 항목이 아닌 완전히 다른 구성 요소 다시 렌더링.

해당 시나리오를 처리하는 한 가지 방법은 DI(종속성 삽입) 서비스와 같은 ‘상태 관리’ 클래스를 여러 구성 요소에 삽입하는 것입니다. 한 구성 요소가 상태 관리자에서 메서드를 호출하면 상태 관리자가 C# 이벤트를 발생시키고 독립 구성 요소가 이를 수신합니다.

상태를 관리하는 방법은 다음 리소스를 참조하세요.

상태 관리자 접근 방식의 경우, C# 이벤트는 Blazor 렌더링 파이프라인 외부에 있습니다. 상태 관리자의 이벤트에 대한 응답으로 다시 렌더링하려는 다른 구성 요소에서 StateHasChanged을 호출합니다.

상태 관리자 접근방식은 이전 섹션에서 System.Timers.Timer를 사용하는 이전 사례와 비슷합니다. 실행 호출 스택은 일반적으로 렌더러의 동기화 컨텍스트에 유지되므로 일반적으로 InvokeAsync를 호출할 필요가 없습니다. Task에서 ContinueWith를 호출하거나 ConfigureAwait(false)를 사용하여 Task를 대기하는 경우처럼 논리가 동기화 컨텍스트를 이스케이프하는 경우에만 InvokeAsync를 호출해야 합니다. 자세한 정보는 Blazor 렌더링 및 이벤트 처리 시스템 외부에서 호출 수신 섹션을 참조하세요.

Web Apps에 대한 Blazor WebAssembly 로드 진행률 표시기

웹앱 프로젝트 템플릿에서 만든 앱에는 로드 진행률 표시기가 Blazor 없습니다. .NET의 향후 릴리스를 위해 새로운 로드 진행률 표시기 기능이 계획되어 있습니다. 그 동안 앱은 사용자 지정 코드를 채택하여 로드 진행률 표시기를 만들 수 있습니다. 자세한 내용은 ASP.NET Core Blazor 시작을 참조하세요.