다음을 통해 공유


ASP.NET Core Razor 구성 요소 삭제

비고

이 문서의 최신 버전은 아닙니다. 현재 버전을 보려면 이 문서의 .NET 9 버전 을 참조하십시오.

경고

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

중요합니다

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

현재 버전을 보려면 이 문서의 .NET 9 버전 을 참조하십시오.

이 문서에서는 IDisposableIAsyncDisposable사용하여 ASP.NET Core Razor 구성 요소 삭제에 대해 설명합니다.

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

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

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

자세한 내용은 ASP.NET Core Blazor 동기화 컨텍스트소개를 참조하세요.

JavaScript interop 개체 참조 삭제

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

JS interop 객체 참조는 참조를 생성하는 JS interop 호출 측면의 식별자를 키로 하는 맵으로 구현됩니다. .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 지시문으로 구현합니다.
  • IDisposable을 구현하는 형식인 obj를 삭제합니다.
  • 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 호출은 콜백이 ComponentBase.InvokeAsync의 동기화 컨텍스트 외부에서 호출되기 때문에 Blazor 호출에 의해 래핑됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

OnInitialized{Async}과 같은 수명 주기 메서드에서 개체가 만들어진 경우 null를 호출하기 전에 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();
}

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

...

@code {
    ...

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

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

삭제된 개체에 null를 할당하기

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

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

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

StateHasChanged

비고

StateHasChanged, Dispose, 및 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;
    }
}

구성 요소 및 양식에 대한 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 삭제

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

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