ASP.NET Core Razor 구성 요소 가상화

참고 항목

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

Important

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

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

이 문서에서는 ASP.NET Core Blazor 앱에서 구성 요소 가상화를 사용하는 방법을 설명합니다.

가상화

구성 요소와 함께 Virtualize<TItem> 프레임워크의 기본 제공 가상화 지원을 사용하여 Blazor 구성 요소 렌더링의 인식된 성능을 향상시킵니다. 가상화는 UI 렌더링을 현재 표시되는 부분으로만 제한하는 기술입니다. 예를 들어 가상화는 앱에서 긴 항목 목록을 렌더링해야 하고 지정된 시간에 항목의 하위 집합만 표시해야 하는 경우에 유용합니다.

Virtualize<TItem> 구성 요소를 사용하는 경우:

  • 루프에서 데이터 항목 세트를 렌더링하는 경우.
  • 스크롤로 인해 대부분 항목이 표시되지 않는 경우.
  • 렌더링된 항목의 크기가 동일한 경우.

사용자가 Virtualize<TItem> 구성 요소의 항목 목록에 있는 임의의 지점으로 스크롤하면, 구성 요소는 어떤 항목을 표시할지를 계산합니다. 보이지 않는 항목은 렌더링되지 않습니다.

가상화를 사용하지 않는 경우 일반적인 목록은 C# foreach 루프를 사용하여 목록의 각 항목을 렌더링할 수도 있습니다. 다음 예제에서

  • allFlights는 비행기 항공편의 컬렉션입니다.
  • FlightSummary 구성 요소는 각 항공편에 대한 세부 정보를 표시합니다.
  • @key 지시문 특성은 각 FlightSummary 구성 요소와 항공편의 FlightId에 의해 렌더링된 항공편의 관계를 유지합니다.
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

컬렉션에 수천 개의 항공편이 포함되어 있으면 항공편을 렌더링하는 데 시간이 오래 걸리고 사용자는 UI에서 큰 지연을 경험하게 됩니다. 대부분의 항공편은 <div> 요소의 높이를 벗어나므로 보이지 않습니다.

항공편의 전체 목록을 동시에 렌더링하는 대신 앞의 예제에 있는 foreach 루프를 Virtualize<TItem> 구성 요소로 바꿉니다.

  • allFlightsVirtualize<TItem>.Items에 대한 고정 항목 원본으로 지정합니다. 현재 표시되는 항공편만 Virtualize<TItem> 구성 요소에 의해 렌더링됩니다.

    제네릭이 아닌 컬렉션이 항목(예: 컬렉션)을 제공하는 경우 항목 공급자 대리자 섹션의 DataRow지침에 따라 항목을 제공합니다.

  • Context 매개 변수를 사용하여 각 항공편에 대한 컨텍스트를 지정합니다. 다음 예제에서 flight는 각 항공편 멤버에 대한 액세스를 제공하는 컨텍스트로 사용됩니다.

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>

Context 매개 변수로 컨텍스트를 지정하지 않은 경우, 각 항공편의 멤버에 액세스하려면 항목 콘텐츠 템플릿에서 context 값을 사용합니다.

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights">
        <FlightSummary @key="context.FlightId" Details="@context.Summary" />
    </Virtualize>
</div>

Virtualize<TItem> 구성 요소는 다음과 같습니다.

  • 컨테이너의 높이와 렌더링된 항목의 크기에 따라 렌더링할 항목의 수를 계산합니다.
  • 사용자가 스크롤하면 항목을 다시 계산하고 렌더링합니다.
  • 대신 사용되는 경우 ItemsProvider 오버스캔을 포함하여 현재 표시되는 지역에 해당하는 외부 API에서만 레코드 조각을 가져옵니다(항목 공급자 대리자 섹션 참조 Items ).

Virtualize<TItem> 구성 요소에 대한 항목 콘텐츠에는 다음이 포함될 수 있습니다.

  • 이전 예제처럼 일반 HTML 및 Razor 코드
  • 하나 이상의 Razor 구성 요소
  • HTML/Razor 및 Razor 구성 요소의 혼합

항목 공급자 대리자

일부 항목을 메모리로 로드하지 않으려는 경우 또는 컬렉션인 제네릭 ICollection<T>가 아닌 경우에는 항목 공급자 대리자 메서드를 요청 시 요청된 항목을 비동기적으로 검색하는 구성 요소의 Virtualize<TItem>.ItemsProvider 매개 변수로 지정할 수 있습니다. 다음 예제에서 LoadEmployees 메서드는 Virtualize<TItem> 구성 요소에 항목을 제공합니다.

<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <p>
        @employee.FirstName @employee.LastName has the 
        job title of @employee.JobTitle.
    </p>
</Virtualize>

항목 공급자는 ItemsProviderRequest를 수신하고 필요한 항목 수를 특정 시작 인덱스에서 시작하여 지정합니다. 그런 다음 항목 공급자는 데이터베이스나 다른 서비스에서 요청된 항목을 검색하여 총 항목 수와 함께 ItemsProviderResult<TItem>으로 반환합니다. 항목 공급자는 각 요청으로 항목을 검색하거나 캐시하여 쉽게 사용 가능하도록 할 수 있습니다.

Virtualize<TItem> 구성 요소는 매개 변수에서 한 항목 원본만 허용할 수 있으므로 항목 공급자를 동시에 사용하여 컬렉션을 Items에 할당하려 하지 마세요. 둘 다 할당되면 런타임에 구성 요소의 매개 변수가 설정될 때 InvalidOperationException이 throw됩니다.

다음 예제에서는 EmployeeService에서 직원을 로드합니다(표시되지 않음).

private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(
    ItemsProviderRequest request)
{
    var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
    var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, 
        numEmployees, request.CancellationToken);

    return new ItemsProviderResult<Employee>(employees, totalEmployees);
}

다음 예제에서 DataRow의 컬렉션은 제네릭이 아닌 컬렉션이므로, 가상화를 위해 항목 공급자 대리자를 사용합니다.

<Virtualize Context="row" ItemsProvider="GetRows">
    ...
</Virtualize>

@code{
    ...

    private ValueTask<ItemsProviderResult<DataRow>> GetRows(ItemsProviderRequest request)
    {
        return new(new ItemsProviderResult<DataRow>(
            dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
            dataTable.Rows.Count));
    }
}

Virtualize<TItem>.RefreshDataAsyncItemsProvider에서 데이터를 다시 요청하도록 구성 요소에 지시합니다. 이는 외부 데이터가 변경되는 경우에 유용합니다. Items를 사용하는 경우 일반적으로 RefreshDataAsync를 호출할 필요가 없습니다.

RefreshDataAsync는 다시 렌더링을 발생시키지 않으면서 Virtualize<TItem> 구성 요소의 데이터를 업데이트합니다. RefreshDataAsync가 Blazor 이벤트 처리기 또는 구성 요소 수명 주기 메서드에서 호출되는 경우 렌더는 이벤트 처리기 또는 수명 주기 메서드 끝에서 자동으로 트리거되므로 렌더링을 트리거하는 것이 필요하지 않습니다. RefreshDataAsync가 다음 ForecastUpdated 대리자와 같이 백그라운드 작업 또는 이벤트와 별도로 트리거되는 경우 StateHasChanged를 호출하여 백그라운드 작업 또는 이벤트가 끝날 때 UI를 업데이트합니다.

<Virtualize ... @ref="virtualizeComponent">
    ...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()
{
    WeatherForecastSource.ForecastUpdated += async () => 
    {
        await InvokeAsync(async () =>
        {
            await virtualizeComponent?.RefreshDataAsync();
            StateHasChanged();
        });
    });
}

앞의 예에서:

  • RefreshDataAsyncVirtualize<TItem> 구성 요소에 대한 새 데이터를 가져오기 위해 먼저 호출됩니다.
  • StateHasChanged는 구성 요소를 다시 렌더링하기 위해 호출됩니다.

자리 표시자

원격 데이터 소스에서 항목을 요청하면 시간이 걸릴 수 있으므로 항목 콘텐츠를 사용하여 자리 표시자를 렌더링하는 옵션이 제공됩니다.

  • Placeholder(<Placeholder>...</Placeholder>)를 사용하여 항목 데이터를 사용할 수 있을 때까지 콘텐츠를 표시합니다.
  • Virtualize<TItem>.ItemContent를 사용하여 목록의 항목 템플릿을 설정합니다.
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <ItemContent>
        <p>
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder>
        <p>
            Loading&hellip;
        </p>
    </Placeholder>
</Virtualize>

빈 콘텐츠

매개 변수를 EmptyContent 사용하여 구성 요소가 로드되고 비어 있거나 ItemsItemsProviderResult<TItem>.TotalItemCount 0일 때 콘텐츠를 제공합니다.

EmptyContent.razor:

@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="@stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= new();
}

OnInitialized 구성 요소 표시 문자열을 보려면 메서드 람다를 변경합니다.

protected override void OnInitialized() =>
    stringList ??= new() { "Here's a string!", "Here's another string!" };

항목 크기

각 항목의 높이(픽셀 단위)를 Virtualize<TItem>.ItemSize(기본값: 50)으로 설정할 수 있습니다. 다음 예에서는 각 항목의 높이를 기본값인 50픽셀에서 25픽셀로 변경합니다.

<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>

기본적으로 Virtualize<TItem> 구성 요소는 초기 렌더링이 수행된 ‘후’에 개별 항목의 렌더링 크기(높이)를 측정합니다. 정확한 초기 렌더링 성능을 지원하고 페이지 다시 로드에 적합한 스크롤 위치를 보장하려면 ItemSize를 사용하여 정확한 항목 크기를 미리 제공합니다. 기본 ItemSize를 사용하여 일부 항목이 현재 표시된 뷰 외부에서 렌더링되는 경우 두 번째 다시 렌더링이 트리거됩니다. 가상화된 목록에서 브라우저의 스크롤 위치를 올바르게 유지 관리하려면 초기 렌더링이 정확해야 합니다. 그렇지 않으면 사용자에게 잘못된 항목이 표시될 수 있습니다.

오버스캔 개수

Virtualize<TItem>.OverscanCount는 표시되는 지역 앞뒤에 렌더링되는 추가 항목 수를 결정합니다. 이 설정은 스크롤 중 렌더링 빈도를 줄이는 데 도움이 됩니다. 그러나 값이 클수록 페이지에서 더 많은 요소가 렌더링됩니다(기본값: 3). 다음 예제에서는 overscan count를 기본값인 3개 항목에서 4개 항목으로 변경합니다.

<Virtualize Context="employee" Items="employees" OverscanCount="4">
    ...
</Virtualize>

상태 변경

Virtualize<TItem> 구성 요소에 의해 렌더링된 항목을 변경하는 경우 StateHasChanged를 호출하여 강제로 구성 요소를 재평가하고 다시 렌더링합니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

키보드 스크롤 지원

사용자가 키보드를 사용하여 가상화된 콘텐츠를 스크롤할 수 있도록 하려면 가상화된 요소나 스크롤 컨테이너 자체가 포커스 가능해야 합니다. 이 단계를 수행하지 못하면 Chromium 기반 브라우저에서 키보드 스크롤이 작동하지 않습니다.

예를 들어 다음과 같이 스크롤 컨테이너에서 tabindex 특성을 사용할 수 있습니다.

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights">
        <div class="flight-info">...</div>
    </Virtualize>
</div>

tabindex-1, 0 또는 기타 값의 의미에 대한 자세한 내용은 tabindex(MDN 설명서)를 참조하세요.

고급 스타일 및 스크롤 검색

Virtualize<TItem> 구성 요소는 특정 요소 레이아웃 메커니즘을 지원하도록 설계되었습니다. 제대로 작동하는 요소 레이아웃을 이해하기 위해 다음에서는 Virtualize가 올바른 위치에 표시해야 하는 요소를 검색하는 방법을 설명합니다.

소스 코드는 다음과 같습니다.

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights" ItemSize="100">
        <div class="flight-info">Flight @context.Id</div>
    </Virtualize>
</div>

런타임에 Virtualize<TItem> 구성 요소는 다음과 유사한 DOM 구조를 렌더링합니다.

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div style="height:1100px"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div style="height:3400px"></div>
</div>

렌더링된 실제 행 수와 스페이서의 크기는 스타일 및 Items 컬렉션 크기에 따라 달라집니다. 그러나 콘텐츠 앞과 뒤에는 스페이서 div 요소가 삽입되어 있습니다. 이러한 요소는 다음 두 가지 용도로 사용됩니다.

  • 콘텐츠 앞뒤에 오프셋을 제공하여 현재 표시되는 항목을 스크롤 범위의 올바른 위치에 표시하고 스크롤 범위 자체가 모든 콘텐츠의 전체 크기를 나타내도록 하기 위해
  • 사용자가 현재 보이는 범위를 벗어나는지 검색하여 다른 콘텐츠를 렌더링해야 하는지 파악하기 위해

참고 항목

스페이서 HTML 요소 태그를 제어하는 방법에 대한 자세한 내용은 이 문서 뒷부분에 있는 스페이서 요소 태그 이름 제어 섹션을 참조하세요.

스페이서 요소는 내부적으로 교차 관찰자를 사용하여 보이게 될 때 알림을 받습니다. Virtualize는 이러한 이벤트를 수신하는 방법에 따라 달라집니다.

Virtualize는 다음과 같은 경우에 작동합니다.

  • 자리 표시자 콘텐츠를 포함하여 렌더링된 모든 콘텐츠 항목의 높이가 동일합니다. 이렇게 하면 먼저 모든 데이터 항목을 가져온 후 데이터를 DOM 요소로 렌더링하지 않고도 지정된 스크롤 위치에 해당하는 콘텐츠를 계산할 수 있습니다.

  • 스페이서와 콘텐츠 행은 모든 항목이 전체 가로 너비를 채우는 단일 세로 스택으로 렌더링됩니다. 이것이 일반적으로 기본값입니다. 일반적으로 div 요소를 사용하는 경우 Virtualize가 기본적으로 작동합니다. CSS를 사용하여 고급 레이아웃을 만드는 경우 다음 요구 사항에 유의하세요.

    • 스크롤 컨테이너 스타일을 사용하려면 다음 값이 필요합니다 display .
      • block(div에 대한 기본값)
      • table-row-group(tbody에 대한 기본값)
      • flex-directioncolumn으로 설정된 flex Virtualize<TItem> 구성 요소의 직계 자식이 Flex 규칙에서 축소되지 않습니다. 예를 들어 .mycontainer > div { flex-shrink: 0 }을 추가합니다.
    • 콘텐츠 행 스타일 지정에는 display 다음 값 중 하나가 필요합니다.
      • block(div에 대한 기본값)
      • table-row(tr에 대한 기본값)
    • CSS를 사용하여 스페이서 요소의 레이아웃을 방해하지 않도록 합니다. 기본적으로 스페이서 요소에는 blockdisplay 값이 있으며, 부모가 테이블 행 그룹이면 기본값은 table-row입니다. 테두리 또는 content 의사 요소를 포함하도록 하는 등의 방법으로 스페이서 요소 너비 또는 높이에 영향을 주지 않습니다.

스페이서 및 콘텐츠 요소가 단일 세로 스택으로 렌더링하지 않도록 하거나 콘텐츠 항목의 높이가 달라지도록 하는 모든 접근 방식에서는 Virtualize<TItem> 구성 요소가 올바르게 작동하지 못합니다.

루트 수준 가상화

Virtualize<TItem> 구성 요소는 overflow-y: scroll이 포함된 다른 요소를 사용하는 대신 문서 자체를 스크롤 루트로 사용할 수 있도록 지원합니다. 다음 예제에서는 <html> 또는 <body> 요소가 overflow-y: scroll이 포함된 구성 요소에서 스타일이 지정됩니다.

<HeadContent>
    <style>
        html, body { overflow-y: scroll }
    </style>
</HeadContent>

Virtualize<TItem> 구성 요소는 overflow-y: scroll이 포함된 다른 요소를 사용하는 대신 문서 자체를 스크롤 루트로 사용할 수 있도록 지원합니다. 문서를 스크롤 루트로 사용하는 경우 <html>또는 <body> 요소를 overflow-y: scroll로 스타일을 지정하지 않도록 합니다. 교차 관찰자가 창 뷰포트 대신 페이지의 전체 스크롤 가능 높이를 표시되는 영역으로 지정하게 되기 때문입니다.

큰 가상화된 목록(예: 100,000개 항목)을 만들고 문서를 페이지 CSS 스타일에서 html { overflow-y: scroll }로 스크롤 루트로 사용하여 이 문제를 재현할 수 있습니다. 때때로 제대로 작동할 수도 있지만 브라우저는 렌더링 시작 시 적어도 한 번 이상 100,000개 항목을 모두 렌더링하려고 시도하므로 브라우저 탭이 잠기게 될 수 있습니다.

.NET 7을 출시하기 전에 이 문제를 해결하려면 <html>/<body> 요소를 overflow-y: scroll로 스타일을 지정하지 않도록 하거나 다른 방법을 채택합니다. 다음 예제에서는 <html> 요소의 높이가 뷰포트 높이의 100% 이상으로 설정됩니다.

<HeadContent>
    <style>
        html { min-height: calc(100vh + 0.3px) }
    </style>
</HeadContent>

Virtualize<TItem> 구성 요소는 overflow-y: scroll이 포함된 다른 요소를 사용하는 대신 문서 자체를 스크롤 루트로 사용할 수 있도록 지원합니다. 문서를 스크롤 루트로 사용할 때는 <html> 또는 <body> 요소를 overflow-y: scroll로 스타일을 지정하지 않도록 합니다. 창 뷰포트 대신 페이지의 전체 스크롤 가능 높이를 표시되는 영역으로 지정하게 되기 때문입니다.

큰 가상화된 목록(예: 100,000개 항목)을 만들고 문서를 페이지 CSS 스타일에서 html { overflow-y: scroll }로 스크롤 루트로 사용하여 이 문제를 재현할 수 있습니다. 때때로 제대로 작동할 수도 있지만 브라우저는 렌더링 시작 시 적어도 한 번 이상 100,000개 항목을 모두 렌더링하려고 시도하므로 브라우저 탭이 잠기게 될 수 있습니다.

.NET 7을 출시하기 전에 이 문제를 해결하려면 <html>/<body> 요소를 overflow-y: scroll로 스타일을 지정하지 않도록 하거나 다른 방법을 채택합니다. 다음 예제에서는 <html> 요소의 높이가 뷰포트 높이의 100% 이상으로 설정됩니다.

<style>
    html { min-height: calc(100vh + 0.3px) }
</style>

스페이서 요소 태그 이름 제어

Virtualize<TItem> 구성 요소가 특정 자식 태그 이름이 필요한 요소 내에 배치된 경우 SpacerElement에서 가상화 스페이서 태그 이름을 가져오거나 설정할 수 있습니다. 기본값은 div입니다. 다음 예제의 경우 Virtualize<TItem> 구성 요소가 테이블 본문 요소(tbody) 내에서 렌더링되므로 테이블 행(tr)에 적합한 자식 요소가 스페이서로 설정됩니다.

VirtualizedTable.razor:

@page "/virtualized-table"

<PageTitle>Virtualized Table</PageTitle>

<HeadContent>
    <style>
        html, body {
            overflow-y: scroll
        }
    </style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
    <thead style="position: sticky; top: 0; background-color: silver">
        <tr>
            <th>Item</th>
            <th>Another column</th>
        </tr>
    </thead>
    <tbody>
        <Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">
            <tr @key="context" style="height: 30px;" id="row-@context">
                <td>Item @context</td>
                <td>Another value</td>
            </tr>
        </Virtualize>
    </tbody>
</table>

@code {
    private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}

앞의 예제에서 문서 루트는 스크롤 컨테이너로 사용되므로 htmlbody 요소는 overflow-y: scroll로 스타일 지정됩니다. 자세한 내용은 다음 리소스를 참조하세요.