다음을 통해 공유


ASP.NET Core Razor 구성 요소 수명 주기

참고 항목

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

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Important

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

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

이 문서에서는 ASP.NET Core Razor 구성 요소 수명 주기 및 수명 주기 이벤트를 사용하는 방법을 설명합니다.

수명 주기 이벤트

Razor 구성 요소는 동기 및 비동기 수명 주기 메서드 집합에서 Razor 구성 요소 수명 주기 이벤트를 처리합니다. 구성 요소 초기화 및 렌더링 중에 구성 요소에서 추가 작업을 수행하려면 수명 주기 메서드를 재정의합니다.

이 문서에서는 복잡한 프레임워크 논리를 명확히 하기 위해 구성 요소 수명 주기 이벤트 처리를 간소화하고 수년에 걸쳐 변경된 모든 변경 내용을 다루지는 않습니다. 사용자 지정 이벤트 처리를 Blazor의 수명 주기 이벤트 처리와 통합하려면 ComponentBase 참조 원본에 액세스해야 할 수 있습니다. 참조 원본의 코드 주석에는 이 문서 또는 API 설명서에 표시되지 않는 수명 주기 이벤트 처리에 대한 추가 설명이 포함됩니다.

참고 항목

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

다음 간소화된 다이어그램은 Razor 구성 요소 수명 주기 이벤트 처리를 보여줍니다. 수명 주기 이벤트와 연결된 C# 메서드는 이 문서의 다음 섹션에 있는 예제를 사용하여 정의됩니다.

구성 요소 수명 주기 이벤트:

  1. 구성 요소가 요청에 대해 처음으로 렌더링되는 경우:
    • 구성 요소의 인스턴스를 만듭니다.
    • 속성 삽입을 수행합니다.
    • OnInitialized{Async}을 호출합니다. 불완전한 Task이 반환되면 Task가 대기한 다음 구성 요소가 렌더링됩니다. 동기 메서드는 비동기 메서드 이전에 호출됩니다.
  2. OnParametersSet{Async}을 호출합니다. 불완전한 Task이 반환되면 Task가 대기한 다음 구성 요소가 렌더링됩니다. 동기 메서드는 비동기 메서드 이전에 호출됩니다.
  3. 모든 동기 작업에 대해 렌더링 후 Task을 완료합니다.

참고 항목

구성 요소가 렌더링되기 전에 수명 주기 이벤트에서 수행되는 비동기 작업이 완료되지 않을 수 있습니다. 자세한 내용은 이 문서의 뒷부분에 있는 렌더링시 불완전한 비동기 작업 핸들 섹션을 참조하세요.

렌더링이 어떤 자식이 있는지 결정하기 때문에, 부모 구성 요소가 자식 구성 요소 앞에 렌더링됩니다. 동기 부모 구성 요소 초기화를 사용하는 경우, 부모 초기화가 먼저 완료되도록 보장됩니다. 비동기 부모 구성 요소 초기화를 사용하는 경우, 부모 및 자식 구성 요소 초기화의 완료 순서는 실행 중인 초기화 코드에 따라 달라지므로 확인할 수 없습니다.

에서 구성 요소의 구성 요소 수 Razor 명 주기 이벤트 Blazor

DOM 이벤트 처리:

  1. 이벤트 처리기가 실행됩니다.
  2. 불완전한 Task이 반환되면 Task가 대기한 다음 구성 요소가 렌더링됩니다.
  3. 모든 동기 작업에 대해 렌더링 후 Task을 완료합니다.

DOM 이벤트 처리

Render 수명 주기:

  1. 다음 조건이 모두 충족되면 구성 요소에 대한 추가 렌더링 작업을 피합니다.
    • 첫 번째 렌더링이 아닙니다.
    • ShouldRender은(는) false을(를) 반환합니다.
  2. 렌더링 트리 diff(차이)를 빌드하고 구성 요소를 렌더링합니다.
  3. DOM이 업데이트될 때까지 기다립니다.
  4. OnAfterRender{Async}을 호출합니다. 동기 메서드는 비동기 메서드 이전에 호출됩니다.

렌더링 수명 주기

개발자가 호출하여 StateHasChanged 다시 렌더링합니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

매개 변수가 설정된 경우(SetParametersAsync)

SetParametersAsync는 렌더링 트리 또는 경로 매개 변수에서 구성 요소의 부모가 제공하는 매개 변수를 설정합니다.

SetParametersAsync를 호출할 때마다 메서드의 ParameterView 매개변수에 구성 요소의 매개 변수 값 세트가 포함됩니다. SetParametersAsync 메서드를 재정의하면 개발자 코드가 ParameterView의 매개 변수와 직접 상호 작용할 수 있습니다.

SetParametersAsync의 기본 구현에서는 ParameterView에 해당 값이 있는 [Parameter] 또는 [CascadingParameter] 특성으로 각 속성의 값을 설정합니다. ParameterView에 해당 값이 없는 매개 변수는 변경되지 않고 그대로 유지됩니다.

일반적으로 코드는 재정의할 때 기본 클래스 메서드(await base.SetParametersAsync(parameters);)를 SetParametersAsync호출해야 합니다. 고급 시나리오에서 개발자 코드는 기본 클래스 메서드를 호출하지 않는 데 필요한 방식으로 들어오는 매개 변수의 값을 해석할 수 있습니다. 예를 들어 들어오는 매개 변수를 클래스의 속성에 할당해야 하는 요구 사항이 없습니다. 그러나 다른 수명 주기 메서드를 ComponentBase 호출하고 복잡한 방식으로 렌더링을 트리거하기 때문에 기본 클래스 메서드를 호출하지 않고 코드를 구성할 때 참조 소스를 참조해야 합니다.

참고 항목

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

들어오는 매개 변수를 처리하지 않고 초기화 및 렌더링 논리 ComponentBase.SetParametersAsync 를 사용하려는 경우 기본 클래스 메서드에 빈 ParameterView 매개 변수를 전달하는 옵션이 있습니다.

await base.SetParametersAsync(ParameterView.Empty);

개발자 코드에서 이벤트 처리기를 제공하는 경우 삭제 시 해당 처리기를 작동합니다. 자세한 내용은 IDisposableIAsyncDisposable을 사용한 구성 요소 삭제 섹션을 참조하세요.

다음 예제에서 Param의 경로 매개 변수 구문 분석이 성공하는 경우 ParameterView.TryGetValueParam 매개 변수의 값을 value에 할당합니다. valuenull이 아니면 값이 구성 요소에 의해 표시됩니다.

경로 매개 변수 일치는 대/소문자를 구분하지 않지만, TryGetValue는 경로 템플릿에서 대/소문자를 구분하는 매개 변수 이름만 일치시킵니다. 다음 예에서는 TryGetValue가 아닌 /{param?}를 사용하여 값을 가져오기 위해 경로 템플릿에서 /{Param?}를 사용해야 합니다. 이 시나리오에서 /{param?}를 사용하면 TryGetValuefalse를 반환하고 messagemessage문자열 중 하나로 설정되지 않습니다.

SetParamsAsync.razor:

@page "/set-params-async/{Param?}"

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async"
@page "/set-params-async/{Param}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

구성 요소 초기화(OnInitialized{Async})

OnInitialized 구성 OnInitializedAsync 요소 인스턴스의 전체 수명 동안 구성 요소를 초기화하는 데만 사용됩니다. 매개 변수 값 및 매개 변수 값 변경은 이러한 메서드에서 수행된 초기화에 영향을 주지 않아야 합니다. 예를 들어 구성 요소의 수명 동안 변경되지 않고 매개 변수 값에 종속되지 않는 드롭다운 목록에 정적 옵션을 로드하는 것은 이러한 수명 주기 메서드 중 하나에서 수행됩니다. 매개 변수 값 또는 매개 변수 값의 변경 내용이 구성 요소 상태에 영향을 주는 경우 대신 사용합니다 OnParametersSet{Async} .

이러한 메서드는 에서 초기 매개 변수를 받은 후 구성 요소가 초기화될 때 호출됩니다 SetParametersAsync. 동기 메서드는 비동기 메서드 이전에 호출됩니다.

동기 부모 구성 요소 초기화를 사용하는 경우, 자식 구성 요소 초기화 전에 부모 초기화가 완료되도록 보장됩니다. 비동기 부모 구성 요소 초기화를 사용하는 경우, 부모 및 자식 구성 요소 초기화의 완료 순서는 실행 중인 초기화 코드에 따라 달라지므로 확인할 수 없습니다.

동기 작업의 경우 OnInitialized를 재정의합니다.

OnInit.razor:

@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized() => 
        message = $"Initialized at {DateTime.Now}";
}
@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized() => 
        message = $"Initialized at {DateTime.Now}";
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

비동기 작업을 수행하려면 OnInitializedAsync를 재정의하고 작업에 await 연산자를 사용합니다.

protected override async Task OnInitializedAsync()
{
    await ...
}

사용자 지정 초기화 논리와 함께 사용자 지정 기본 클래스 를 사용하는 경우 기본 클래스를 호출 OnInitializedAsync 합니다.

protected override async Task OnInitializedAsync()
{
    await ...

    await base.OnInitializedAsync();
}

사용자 지정 기본 클래스를 사용자 지정 논리와 함께 사용하지 않는 한 호출 ComponentBase.OnInitializedAsync 할 필요가 없습니다. 자세한 내용은 기본 클래스 수 명 주기 메서드 섹션을 참조하세요.

서버 호출 OnInitializedAsync에 대해 콘텐츠를 ‘두 번’ 미리 렌더링하는 Blazor 앱:

  • 첫 번째 호출: 구성 요소가 처음에 페이지 일부로 정적 렌더링될 때
  • 브라우저에서 구성 요소를 렌더링할 때가 두 번째 호출입니다.

미리 렌더링할 때 OnInitializedAsync의 개발자 코드가 두 번 실행되지 않도록 하려면 미리 렌더링 후의 상태 저장 다시 연결 섹션을 참조하세요. 섹션의 콘텐츠는 s 및 상태 저장 SignalR다시 연결에 중점을 Blazor Web App둡니다. 미리 렌더링하는 동안 초기화 코드를 실행하는 동안 상태를 유지하려면 Prerender ASP.NET Core Razor 구성 요소를 참조하세요.

미리 렌더링할 때 OnInitializedAsync의 개발자 코드가 두 번 실행되지 않도록 하려면 미리 렌더링 후의 상태 저장 다시 연결 섹션을 참조하세요. 섹션의 콘텐츠는 상태 저장 SignalR다시 연결에 Blazor Server 중점을 두지만 호스트 Blazor WebAssembly 된 솔루션(WebAssemblyPrerendered)에서 미리 렌더링하는 시나리오에는 개발자 코드가 두 번 실행되지 않도록 하는 유사한 조건과 접근 방식이 포함됩니다. 미리 렌더링하는 동안 초기화 코드를 실행하는 동안 상태를 유지하려면 MVC 또는 Razor Pages와 ASP.NET Core Razor 구성 요소 통합을 참조하세요.

Blazor 앱이 미리 렌더링하는 동안, JavaScript(JS interop)를 호출하는 것과 같은 특정 실행은 불가능합니다. 미리 렌더링된 경우, 구성 요소를 다르게 렌더링해야 할 수도 있습니다. 자세한 내용은 JavaScript interop을 사용한 미리 렌더링 섹션을 참조하세요.

개발자 코드에서 이벤트 처리기를 제공하는 경우 삭제 시 해당 처리기를 작동합니다. 자세한 내용은 IDisposable을 사용한 구성 요소 삭제IAsyncDisposable 섹션을 참조하세요.

정적 서버 쪽 렌더링(정적 SSR) 또는 미리 렌더링과 함께 스트리밍 렌더링을 사용하여 장기 실행 비동기 작업을 수행하는 구성 요소의 OnInitializedAsync 사용자 환경을 개선하여 완전히 렌더링할 수 있습니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

매개 변수를 설정한 후(OnParametersSet{Async})

다음 두 가지 경우에 OnParametersSet 또는 OnParametersSetAsync가 호출됩니다.

  • 구성 요소가 OnInitialized 또는 OnInitializedAsync에서 초기화된 후

  • 부모 구성 요소가 다시 렌더링되고 다음을 제공하는 경우

    • 알려졌거나 변경 불가능한 기본 형식(하나 이상의 매개 변수가 변경된 경우)
    • 복합 형식 매개 변수 프레임워크는 복잡한 형식의 매개 변수 값이 내부적으로 변경되는지 여부를 알 수 없으므로, 하나 이상의 복잡한 형식의 매개 변수가 있는 경우 프레임워크는 항상 매개 변수 집합을 변경된 것으로 처리합니다.

    렌더링 규칙에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

동기 메서드는 비동기 메서드 이전에 호출됩니다.

매개 변수 값이 변경되지 않은 경우에도 메서드를 호출할 수 있습니다. 이 동작은 데이터 또는 해당 매개 변수에 종속된 상태를 다시 초기화하기 전에 매개 변수 값이 실제로 변경되었는지 여부를 확인하기 위해 개발자가 메서드 내에서 추가 논리를 구현해야 한다는 것을 강조합니다.

다음 예제 구성 요소에서 URL의 구성 요소 페이지로 이동합니다.

  • StartDate로부터 받은 시작 날짜는 /on-parameters-set/2021-03-19입니다.
  • 시작 날짜 없이, StartDate이 현재 로컬 시간의 값으로 할당될 때는 /on-parameters-set입니다.

참고 항목

구성 요소 경로에서는 DateTime 매개 변수를 경로 제약 조건datetime으로 제한하고 매개 변수를 선택적으로 만들 수 없습니다. 따라서 다음OnParamsSet 구성 요소에서는 두 개의 @page 지시문을 사용하여 URL에서 제공된 날짜 세그먼트를 포함하거나 제외하고 라우팅을 처리합니다.

OnParamsSet.razor:

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied " +
                $"(StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used " +
                $"(StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied " +
                $"(StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used " +
                $"(StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

매개 변수와 속성 값을 적용하는 경우 비동기 작업은 OnParametersSetAsync 수명 주기 이벤트 중에 수행해야 합니다.

protected override async Task OnParametersSetAsync()
{
    await ...
}

사용자 지정 초기화 논리와 함께 사용자 지정 기본 클래스 를 사용하는 경우 기본 클래스를 호출 OnParametersSetAsync 합니다.

protected override async Task OnParametersSetAsync()
{
    await ...

    await base.OnParametersSetAsync();
}

사용자 지정 기본 클래스를 사용자 지정 논리와 함께 사용하지 않는 한 호출 ComponentBase.OnParametersSetAsync 할 필요가 없습니다. 자세한 내용은 기본 클래스 수 명 주기 메서드 섹션을 참조하세요.

개발자 코드에서 이벤트 처리기를 제공하는 경우 삭제 시 해당 처리기를 작동합니다. 자세한 내용은 IDisposable을 사용한 구성 요소 삭제IAsyncDisposable 섹션을 참조하세요.

경로 매개 변수 및 제약 조건에 대한 자세한 내용은 ASP.NET Core Blazor 라우팅 및 탐색을 참조하세요.

일부 시나리오에서 성능을 향상시키기 위해 수동으로 구현하는 SetParametersAsync 예제는 ASP.NET Core Blazor 성능 모범 사례를 참조하세요.

구성 요소 렌더링 후(OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 구성 요소가 대화형으로 렌더링되고 UI가 업데이트를 완료한 후 호출됩니다(예: 요소가 브라우저 DOM에 추가된 후). 이때 요소 및 구성 요소 참조가 채워집니다. 렌더링된 DOM 요소와 상호작용하는 JS interop 호출처럼 렌더링된 콘텐츠로 부가적인 초기화 단계를 시행할 때 이 단계가 필요합니다. 동기 메서드는 비동기 메서드 이전에 호출됩니다.

이러한 메서드는 서버에서 미리 렌더링하거나 정적 서버 쪽 렌더링(정적 SSR)하는 동안 호출되지 않습니다. 이러한 프로세스는 라이브 브라우저 DOM에 연결되지 않고 DOM이 업데이트되기 전에 이미 완료되었기 때문입니다.

따라서 OnAfterRenderAsync무한 렌더링 루프를 방지하기 위해 반환 Task 된 항목이 완료된 후 구성 요소가 자동으로 다시 렌더링되지 않습니다.

OnAfterRenderOnAfterRenderAsync는 구성 요소 렌더링을 완료한 후에 호출됩니다. 이때 요소 및 구성 요소 참조가 채워집니다. 렌더링된 DOM 요소와 상호작용하는 JS interop 호출처럼 렌더링된 콘텐츠로 부가적인 초기화 단계를 시행할 때 이 단계가 필요합니다. 동기 메서드는 비동기 메서드 이전에 호출됩니다.

미리 렌더링은 라이브 브라우저 DOM에 연결되지 않고 DOM이 업데이트되기 전에 이미 완료되었으므로 이러한 메서드는 미리 렌더링하는 동안 호출되지 않습니다.

따라서 OnAfterRenderAsync무한 렌더링 루프를 방지하기 위해 반환 Task 된 항목이 완료된 후 구성 요소가 자동으로 다시 렌더링되지 않습니다.

OnAfterRenderOnAfterRenderAsyncfirstRender 매개 변수와 관련해서 다음 사항을 확인합니다.

  • 구성 요소 인스턴스를 처음 렌더링할 때는 true로 설정됩니다.
  • 초기화 작업이 한 번만 수행되도록 하는 데 사용할 수 있습니다.

AfterRender.razor:

@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender) =>
        Logger.LogInformation("firstRender = {FirstRender}", firstRender);

    private void HandleClick() => Logger.LogInformation("HandleClick called");
}
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender) =>
        Logger.LogInformation("firstRender = {FirstRender}", firstRender);

    private void HandleClick() => Logger.LogInformation("HandleClick called");
}
@page "/after-render"
@inject ILogger<AfterRender> Logger

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}

이 샘플은 AfterRender.razor 페이지가 로드되고 단추가 선택되면 콘솔에 다음 출력을 생성합니다.

OnAfterRender: firstRender = True
HandleClick called
OnAfterRender: firstRender = False

렌더링 직후의 비동기 작업은 OnAfterRenderAsync 수명 주기 이벤트 중에 수행해야 합니다.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...
}

사용자 지정 초기화 논리와 함께 사용자 지정 기본 클래스 를 사용하는 경우 기본 클래스를 호출 OnAfterRenderAsync 합니다.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...

    await base.OnAfterRenderAsync(firstRender);
}

사용자 지정 기본 클래스를 사용자 지정 논리와 함께 사용하지 않는 한 호출 ComponentBase.OnAfterRenderAsync 할 필요가 없습니다. 자세한 내용은 기본 클래스 수 명 주기 메서드 섹션을 참조하세요.

OnAfterRenderAsync에서 Task를 반환하는 경우에도, 해당 작업이 완료된 후에는 프레임워크에서 구성 요소에 대해 추가 렌더링 주기를 예약하지 않습니다. 이 동작은 무한 렌더링 루프를 방지하기 위한 것입니다. 반환된 Task 작업이 완료된 후 추가 렌더링 주기를 예약하는 기타 수명 주기 메서드와는 다릅니다.

OnAfterRenderOnAfterRenderAsync는 ‘서버에서 미리 렌더링 프로세스 중에는 호출되지 않습니다’. 이 메서드는 미리 렌더링이 완료된 후 구성 요소가 대화형으로 렌더링될 때 호출됩니다. 앱이 미리 렌더링되는 경우:

  1. 구성 요소가 서버에서 실행되어 HTTP 응답에 몇 가지 정적 HTML 태그를 생성합니다. 이 단계에서는 OnAfterRenderOnAfterRenderAsync가 호출되지 않습니다.
  2. Blazor 스크립트(blazor.{server|webassembly|web}.js)가 브라우저에서 시작되면 구성 요소가 대화형 렌더링 모드에서 다시 시작됩니다. 구성 요소가 다시 시작되면 앱이 더 이상 미리 렌더링 단계에 포함되지 않기 때문에 OnAfterRenderOnAfterRenderAsync이 호출됩니다.

개발자 코드에서 이벤트 처리기를 제공하는 경우 삭제 시 해당 처리기를 작동합니다. 자세한 내용은 IDisposable을 사용한 구성 요소 삭제IAsyncDisposable 섹션을 참조하세요.

기본 클래스 수명 주기 메서드

'의 수명 주기 메서드를 Blazor재정의하는 경우 기본 클래스 수명 주기 메서드를 ComponentBase호출할 필요가 없습니다. 그러나 구성 요소는 다음과 같은 상황에서 재정의된 기본 클래스 수명 주기 메서드를 호출해야 합니다.

  • 재정의할 ComponentBase.SetParametersAsyncawait base.SetParametersAsync(parameters); 때 기본 클래스 메서드가 다른 수명 주기 메서드를 호출하고 복잡한 방식으로 렌더링을 트리거하기 때문에 일반적으로 호출됩니다. 자세한 내용은 매개 변수가 설정된 경우(SetParametersAsync) 섹션을 참조하세요.
  • 기본 클래스 메서드에 실행해야 하는 논리가 포함되어 있는 경우 라이브러리 기본 클래스에는 실행할 사용자 지정 수명 주기 논리가 있는 경우가 많기 때문에 라이브러리 소비자는 일반적으로 기본 클래스를 상속할 때 기본 클래스 수명 주기 메서드를 호출합니다. 앱이 라이브러리의 기본 클래스를 사용하는 경우 라이브러리의 설명서에서 지침을 참조하세요.

다음 예제 base.OnInitialized(); 에서는 기본 클래스의 OnInitialized 메서드가 실행되도록 호출됩니다. 호출이 없으면 BlazorRocksBase2.OnInitialized 실행되지 않습니다.

BlazorRocks2.razor:

@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}

BlazorRocksBase2.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";

    protected override void OnInitialized() =>
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";

    protected override void OnInitialized() =>
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}

상태 변경 내용(StateHasChanged)

StateHasChanged는 구성 요소에 상태가 변경되었음을 알립니다. 해당하는 경우 호출 StateHasChanged 은 앱의 주 스레드가 무료일 때 발생하는 다시 렌더링을 큐에 추가합니다.

StateHasChangedEventCallback 메서드에 대해 자동으로 호출됩니다. 이벤트 콜백에 대한 자세한 내용은 ASP.NET Core Blazor 이벤트 처리를 참조하세요.

구성 요소 렌더링 및 StateHasChanged 호출 시기(ComponentBase.InvokeAsync로 호출하는 시기 포함)에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

렌더링 시 불완전한 비동기 작업 처리

수명 주기 이벤트에서 수행한 비동기 작업이 구성 요소를 렌더링하기 전에 완료되지 않았을 수 있습니다. 수명 주기 메서드를 실행하는 동안 개체가 null이거나 불완전하게 데이터로 채워졌을 수 있습니다. 개체가 초기화되었는지 확인하는 렌더링 논리를 제공합니다. 개체가 null인 동안 자리 표시자 UI 요소(예: 로드 메시지)를 렌더링합니다.

다음 구성 요소 OnInitializedAsync 에서는 영화 등급 데이터(movies)를 비동기적으로 제공하도록 재정의됩니다. moviesnull인 경우 사용자에게 로드 메시지가 표시됩니다. OnInitializedAsync에서 반환된 Task가 완료되면 구성 요소가 업데이트된 상태로 다시 렌더링됩니다.

<h1>Sci-Fi Movie Ratings</h1>

@if (movies == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <ul>
        @foreach (var movie in movies)
        {
            <li>@movie.Title &mdash; @movie.Rating</li>
        }
    </ul>
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovieRatings(DateTime.Now);
    }
}

오류 처리

수명 주기 메서드를 실행하는 동안 오류를 처리하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor 앱의 오류 처리를 참조하세요.

미리 렌더링 후의 상태 저장 다시 연결

서버에서 미리 렌더링할 때 구성 요소는 처음에 페이지의 일부로 정적으로 렌더링됩니다. 브라우저가 서버에 다시 SignalR 연결을 구성하면 구성 요소가 다시 렌더링되고, 이제 대화형 구성 요소가 됩니다. 구성 요소를 초기화하기 위한 OnInitialized{Async} 수명 주기 메서드가 있는 경우 메서드가 다음과 같이 ‘두 번’ 실행됩니다.

  • 구성 요소를 정적으로 미리 렌더링할 때
  • 서버 연결이 설정된 후

이 동작 때문에 구성 요소를 최종적으로 렌더링할 때는 UI에 표시되는 데이터가 상당히 변경될 수 있습니다. 이 동작을 방지하려면 미리 렌더링하는 동안 상태를 캐시하고 미리 렌더링한 후 상태를 검색하도록 식별자를 전달합니다.

다음 코드에서는 미리 렌더링으로 인해 데이터 표시가 변경되지 않도록 하는 방법을 보여 WeatherForecastService 줍니다. 대기(Delayawait Task.Delay(...))는 메서드에서 데이터를 반환하기 전에 짧은 지연을 GetForecastAsync 시뮬레이션합니다.

앱 파일의 Program 서비스 컬렉션에 서비스를 추가 IMemoryCache 합니다AddMemoryCache.

builder.Services.AddMemoryCache();

WeatherForecastService.cs:

using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

public class WeatherForecastService(IMemoryCache memoryCache)
{
    private static readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public IMemoryCache MemoryCache { get; } = memoryCache;

    public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

public class WeatherForecastService(IMemoryCache memoryCache)
{
    private static readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public IMemoryCache MemoryCache { get; } = memoryCache;

    public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

RenderMode에 대한 자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.

이 섹션의 콘텐츠는 s 및 상태 저장 SignalR다시 연결에 중점을 Blazor Web App둡니다. 미리 렌더링하는 동안 초기화 코드를 실행하는 동안 상태를 유지하려면 Prerender ASP.NET Core Razor 구성 요소를 참조하세요.

이 섹션의 콘텐츠는 상태 저장 SignalR다시 연결에 Blazor Server 중점을 두지만 호스트 Blazor WebAssembly 된 솔루션(WebAssemblyPrerendered)에서 미리 렌더링하는 시나리오에는 개발자 코드가 두 번 실행되지 않도록 하는 유사한 조건과 접근 방식이 포함됩니다. 미리 렌더링하는 동안 초기화 코드를 실행하는 동안 상태를 유지하려면 MVC 또는 Razor Pages와 ASP.NET Core Razor 구성 요소 통합을 참조하세요.

JavaScript interop을 사용한 미리 렌더링

이 섹션은 구성 요소를 미리 렌더링 Razor 하는 서버 쪽 앱에 적용됩니다. 사전 렌더링은 Prerender ASP.NET Core Razor 구성 요소에서 다룹니다.

참고 항목

대화형 라우팅에 대한 내부 탐색에는 Blazor Web App서버에서 새 페이지 콘텐츠를 요청하는 작업이 포함되지 않습니다. 따라서 내부 페이지 요청에 대해 미리 렌더링이 발생하지 않습니다. 앱이 대화형 라우팅을 채택하는 경우 사전 렌더링 동작을 보여 주는 구성 요소 예제에 대해 전체 페이지 다시 로드를 수행합니다. 자세한 내용은 Prerender ASP.NET Core Razor 구성 요소를 참조하세요.

이 섹션은 구성 요소를 미리 렌더링 Razor 하는 서버 쪽 앱 및 호스트 Blazor WebAssembly 된 앱에 적용됩니다. 미리 렌더링은 MVC 또는 Razor Pages와 ASP.NET Core Razor 구성 요소 통합에서 다룹니다.

미리 렌더링하는 동안 JavaScript(JS)를 호출할 수 없습니다. 다음 예제에서는 사전 렌더링과 호환되는 방식으로 구성 요소 초기화 논리의 일부로 interop를 사용하는 JS 방법을 보여 줍니다.

다음 scrollElementIntoView 함수는 다음 작업을 수행합니다.

window.scrollElementIntoView = (element) => {
  element.scrollIntoView();
  return element.getBoundingClientRect().top;
}

구성 요소 코드 ElementReference 에서 함수를 JS 호출하는 경우 IJSRuntime.InvokeAsync 구성 요소가 렌더링될 때까지 HTML DOM 요소가 없기 때문에 이전의 수명 주기 메서드에서만 사용되고 OnAfterRenderAsync 사용되지 않습니다.

StateHasChanged(참조 소스)는 interop 호출에서 JS 얻은 새 상태로 구성 요소의 다시 렌더링을 큐에 넣기 위해 호출됩니다(자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링 참조). 무한 루프는 호출될 때만 scrollPosition null생성 StateHasChanged 되지 않습니다.

PrerenderedInterop.razor:

@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop</PageTitle>

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}

앞의 예제에서는 전역 함수를 사용하여 클라이언트를 오염합니다. 프로덕션 앱에서의 더 나은 접근 방식은 JavaScript 모듈에서의 JavaScript 격리를 참조하세요.

IDisposableIAsyncDisposable로 구성 요소 삭제

구성 요소가 구현 IDisposable 되거나 IAsyncDisposable구성 요소가 UI에서 제거될 때 프레임워크에서 리소스 삭제를 요구합니다. 이러한 메서드가 실행되는 정확한 타이밍에 의존하지 마세요. 예를 들어 IAsyncDisposable 대기 중인 비동 Task 기 호출 또는 완료 전후에 OnInitalizedAsync 트리거할 수 있습니다. 또한 개체 삭제 코드는 초기화 또는 다른 수명 주기 메서드 중에 만든 개체가 있다고 가정해서는 안 됩니다.

구성 요소는 IDisposableIAsyncDisposable을 동시에 구현할 필요가 없습니다. 둘 다 구현되는 경우 프레임워크는 비동기 오버로드만 실행합니다.

개발자 코드는 IAsyncDisposable 구현을 완료하는 데 시간이 오래 걸리지 않도록 해야 합니다.

JavaScript interop 개체 참조 삭제

JavaScript(JS) interop 문서의 예제는 일반적인 개체 삭제 패턴을 보여 줍니다.

JS interop 개체 참조는 참조를 만드는 interop 호출의 측면에 JS 있는 식별자에 의해 키가 지정된 맵으로 구현됩니다. .NET 또는 JS 쪽 Blazor 에서 개체 삭제가 시작되면 맵에서 항목을 제거하고 개체에 대한 다른 강력한 참조가 없는 한 개체를 가비지 수집할 수 있습니다.

최소한 .NET 관리 메모리 누수 방지를 위해 항상 .NET 쪽에서 만든 개체를 삭제합니다.

구성 요소 삭제 중 DOM 정리 작업

자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

회로 연결이 끊어진 시기에 대한 JSDisconnectedException 지침은 ASP.NET Core Blazor JavaScript 상호 운용성(JSinterop)을 참조하세요. 일반적인 JavaScript interop 오류 처리 지침은 ASP.NET Core Blazor 앱에서 오류 처리JavaScript interop 섹션을 참조하세요.

동기 IDisposable

동기 삭제 작업의 경우 IDisposable.Dispose을 사용합니다.

다음 구성 요소는

  • 지시문을 사용하여 구현합니다 IDisposable @implementsRazor .
  • obj를 구현하는 형식인 삭제합니다.IDisposable
  • obj가 수명 주기 메서드(표시되지 않음)에서 만들어지기 때문에 null 검사가 수행됩니다.
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

단일 개체를 삭제해야 하는 경우 Dispose를 호출할 때 람다를 사용하여 개체를 삭제할 수 있습니다. 다음 예제는 ASP.NET Core Razor 구성 요소 렌더링 문서에서 표시되며 Timer의 삭제에 대한 람다 식의 사용을 보여줍니다.

TimerDisposal1.razor:

@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</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();
}

TimerDisposal1.razor:

@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</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();
}

CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@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();
}

참고 항목

앞의 예제에서 StateHasChanged 호출은 콜백이 Blazor의 동기화 컨텍스트 외부에서 호출되기 때문에 ComponentBase.InvokeAsync 호출에 의해 래핑됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

OnInitialized{Async}과 같은 수명 주기 메서드에서 개체가 만들어진 경우 Dispose를 호출하기 전에 null이 있는지 확인합니다.

TimerDisposal2.razor:

@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</h1>

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

@code {
    private int currentCount = 0;
    private Timer? timer;

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

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

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

TimerDisposal2.razor:

@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</h1>

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

@code {
    private int currentCount = 0;
    private Timer? timer;

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

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

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

CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-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;

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

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

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

자세한 내용은 다음을 참조하세요.

비동기 IAsyncDisposable

비동기 삭제 작업에는 IAsyncDisposable.DisposeAsync을 사용합니다.

다음 구성 요소는

  • 지시문을 사용하여 구현합니다 IAsyncDisposable @implementsRazor .
  • IAsyncDisposable을 구현하는 관리되지 않는 형식인 obj를 삭제합니다.
  • obj가 수명 주기 메서드(표시되지 않음)에서 만들어지기 때문에 null 검사가 수행됩니다.
@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

자세한 내용은 다음을 참조하세요.

삭제된 개체에 null 할당

일반적으로 Dispose/DisposeAsync를 호출한 후에는 삭제된 개체에 null을 할당할 필요가 없습니다. 드물지만 null을 할당하는 경우는 다음과 같습니다.

  • 개체의 형식이 잘못 구현되었으며 Dispose /DisposeAsync에 대 한 반복 호출을 허용하지 않는 경우 삭제한 후에 null을 할당하여 Dispose/DisposeAsync에 대한 추가 호출을 적절하게 건너뜁니다.
  • 수명이 긴 프로세스가 계속해서 삭제된 개체에 대한 참조를 보유하는 경우 null을 할당하면 수명이 긴 프로세스에서 해당 개체에 대한 참조를 보유하더라도 가비지 수집기가 개체를 해제할 수 있습니다.

이것은 비정상적인 시나리오입니다. 올바르게 구현되고 정상적으로 동작하는 개체의 경우 삭제된 개체에 null을 할당할 필요가 없습니다. 드문 경우지만 개체에 null을 할당해야 하는 경우 이유를 문서화하고 null을 할당할 필요가 없도록 하는 솔루션을 검색하는 것이 좋습니다.

StateHasChanged

참고 항목

통화 StateHasChangedDispose 이며 DisposeAsync 지원되지 않습니다. 렌더러를 삭제하는 과정에서 StateHasChanged가 호출될 수 있으므로, 해당 시점에 UI 업데이트를 요청할 수는 없습니다.

이벤트 처리기

항상 .NET 이벤트에서 이벤트 처리기의 구독을 취소합니다. 다음 Blazor 양식 예제에서는 Dispose 메서드에서 이벤트 처리기를 구독 취소하는 방법을 보여 줍니다.

  • 프라이빗 필드 및 람다 접근 방식

    @implements IDisposable
    
    <EditForm ... EditContext="editContext" ...>
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        private EventHandler<FieldChangedEventArgs>? fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new(model);
    
            fieldChanged = (_, __) =>
            {
                ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • 프라이빗 메서드 접근 방식

    @implements IDisposable
    
    <EditForm ... EditContext="editContext" ...>
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        protected override void OnInitialized()
        {
            editContext = new(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

자세한 내용은 IDisposableIAsyncDisposable을 사용한 구성 요소 삭제 섹션을 참조하세요.

구성 요소 및 양식에 대한 EditForm 자세한 내용은 ASP.NET Core Blazor 양식 개요 및 Forms 노드의 다른 양식 문서를 참조하세요.

익명 함수, 메서드 및 식

익명 함수, 메서드 또는 식이 사용되는 경우에는 IDisposable을 구현하고 대리자를 구독 취소할 필요가 없습니다. 그러나 대리자를 구독 취소하지 못하면 이벤트를 노출하는 개체가 대리자를 등록하는 구성 요소보다 수명이 긴 경우 문제가 됩니다. 이 경우 등록된 대리자가 원래 개체를 활성 상태로 유지하기 때문에 메모리 누수가 발생합니다. 따라서 이벤트 대리자가 신속하게 삭제됨을 알고 있는 경우에만 다음 접근 방식을 사용합니다. 삭제해야 하는 개체의 수명이 확실하지 않으면 대리자 메서드를 구독하고 위 예제에 표시된 대로 대리자를 적절하게 삭제합니다.

  • 익명 람다 메서드 방법(명시적 삭제 필요하지 않음)

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • 익명 람다 식 방법(명시적 삭제 필요하지 않음)

    private ValidationMessageStore? messageStore;
    
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    익명 람다 식이 있는 이전 코드의 전체 예제는 ASP.NET Core Blazor 양식 유효성 검사 문서에 나타납니다.

자세한 내용은 관리되지 않는 리소스 정리DisposeDisposeAsync 메서드 구현에 이어지는 토픽을 참조하세요.

interop 중 JS 삭제

회로 손실 SignalR Blazor로 인해 interop 호출이 JS 방지되고 처리되지 않은 예외가 발생하는 잠재적인 경우에 트랩 JSDisconnectedException 합니다.

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

취소할 수 있는 백그라운드 작업

구성 요소는 네트워크 호출(HttpClient) 및 데이터베이스와의 상호 작용 등과 같은 장기 실행 백그라운드 작업을 수행하는 경우가 많습니다. 여러 상황에서 시스템 리소스를 절약하기 위해 백그라운드 작업을 중지하는 것이 좋습니다. 예를 들어 사용자가 구성 요소에서 이동할 때 백그라운드 비동기 작업이 자동으로 중지되지 않습니다.

백그라운드 작업 항목에 취소가 필요할 수 있는 다른 이유는 다음과 같습니다.

  • 잘못된 입력 데이터 또는 처리 매개 변수를 사용하여 실행 중인 백그라운드 작업이 시작되었습니다.
  • 현재 실행 중인 백그라운드 작업 항목 집합을 새 작업 항목 집합으로 바꾸어야 합니다.
  • 현재 실행 중인 작업의 우선 순위를 변경해야 합니다.
  • 서버를 다시 배포 하려면 앱을 종료해야 합니다.
  • 서버 리소스가 제한되므로 백그라운드 작업 항목을 다시 예약해야 합니다.

구성 요소에서 취소할 수 있는 백그라운드 작업 패턴을 구현하는 방법은 다음과 같습니다.

다음 예제에서

  • await Task.Delay(5000, cts.Token);는 장기 실행 비동기 백그라운드 작업을 나타냅니다.
  • BackgroundResourceMethod는 호출되기 전에 Resource가 삭제된 경우 시작하지 않아야 하는 장기 실행 백그라운드 메서드를 나타냅니다.

BackgroundWork.razor:

@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but
    before action is taken on the resource, an <code>ObjectDisposedException</code>
    is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
    processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(10000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            cts.Cancel();
        }
        
        cts?.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose() => disposed = true;
    }
}
@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but
    before action is taken on the resource, an <code>ObjectDisposedException</code>
    is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
    processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(10000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            cts.Cancel();
        }
        
        cts?.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose() => disposed = true;
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new Resource();
    private CancellationTokenSource cts = new CancellationTokenSource();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Blazor Server 재연결 이벤트

이 문서에서 다루는 구성 요소 수명 주기 이벤트는 서버 쪽 다시 연결 이벤트 처리기와 별도로 작동합니다. 클라이언트에 SignalR 대한 연결이 끊어지면 UI 업데이트만 중단됩니다. UI 업데이트는 연결이 다시 설정될 때 다시 시작됩니다. 회로 처리기 이벤트 및 구성에 대한 자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.

추가 리소스

구성 요소의 수명 주기 외부에서 catch된 Razor 예외 처리