다음을 통해 공유


ASP.NET Core Blazor 상태 관리

참고 항목

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

Warning

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

Important

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

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

이 문서에서는 앱을 사용하는 동안 및 브라우저 세션에서 사용자의 데이터(상태)를 유지 관리하는 일반적인 방법을 설명합니다.

참고 항목

이 문서의 코드 예제에서는 NRT(nullable 참조 형식) 및 .NET 컴파일러 null 상태 정적 분석을 채택합니다. 이 분석은 .NET 6 이상의 ASP.NET Core에서 지원됩니다. ASP.NET Core 5.0 이하를 대상으로 하는 경우 문서 예제의 형식에서 null 형식 지정(?)을 제거합니다.

사용자 상태 유지 관리

서버 쪽 Blazor 은 상태 저장 앱 프레임워크입니다. 대체로 앱은 서버와의 연결을 유지합니다. 사용자 상태는 서버 메모리의 ‘회로’에 저장됩니다.

회로에 저장되는 사용자 상태의 예는 다음과 같습니다.

  • 구성 요소 인스턴스의 계층 구조 및 렌더링된 UI의 가장 최근 렌더링 출력.
  • 구성 요소 인스턴스의 필드 및 속성 값.
  • 회로로 범위가 지정된 DI(종속성 주입) 서비스 인스턴스에 저장된 데이터

사용자 상태는 JavaScript interop 호출을 통해 브라우저의 메모리 집합에 있는 JavaScript 변수에서 찾을 수도 있습니다.

사용자에게 임시 네트워크 연결 손실이 발생하는 경우, Blazor는 사용자의 원래 상태로 사용자를 원래 회로에 다시 연결하려 시도합니다. 그러나 사용자를 서버 메모리의 원래 회로에 다시 연결할 수 없는 경우도 있습니다.

  • 서버는 연결이 끊어진 회로를 계속 유지할 수 없습니다. 시간 제한 이후 또는 서버에 메모리 압력이 발생할 경우 서버는 연결이 끊어진 회로를 해제해야 합니다.
  • 부하가 분산된 다중 서버 배포 환경에서 개별 서버는 오류가 발생하거나 전체 요청 볼륨을 더 이상 처리할 필요가 없으면 자동으로 제거될 수 있습니다. 사용자가 다시 연결하려고 할 때 사용자에 대한 원래 서버 처리 요청을 사용할 수 없게 될 수 있습니다.
  • 사용자는 브라우저를 닫고 다시 열거나 페이지를 다시 로드하여 브라우저의 메모리에 저장된 상태를 제거할 수 있습니다. 예를 들어 JavaScript interop 호출을 통해 설정된 JavaScript 변수 값이 손실됩니다.

사용자를 원래 회로에 다시 연결할 수 없는 경우, 사용자는 빈 상태로 새 회로를 받게 됩니다. 이는 데스크톱 앱을 닫고 다시 여는 것과 같습니다.

회로 간 상태 유지

일반적으로 사용자가 이미 존재하는 데이터를 단순히 읽는 것이 아니라 데이터를 적극적으로 만드는 회로 간에 상태를 유지합니다.

회로 간에 상태를 유지하려면 앱이 브라우저 메모리 외의 스토리지 위치에 데이터를 유지해야 합니다. 상태 지속성은 자동이 아닙니다. 상태 저장 데이터 지속성을 구현하려면 앱을 개발할 때 일정 단계를 수행해야 합니다.

데이터 지속성은 일반적으로 사용자가 특별히 노력해서 만든 높은 가치의 상태에만 필요합니다. 다음 예제에서 상태를 유지하면 상업 활동의 시간 또는 지원을 절약할 수 있습니다.

  • 다단계 웹 양식: 상태가 손실된 경우 사용자가 다단계 웹 양식의 완료된 여러 단계에 대한 데이터를 다시 입력하는 데 시간이 많이 걸립니다. 이 시나리오에서는 사용자가 양식을 벗어났다가 나중에 돌아올 경우 상태가 손실됩니다.
  • 쇼핑 카트: 잠재적인 수익을 나타내는 앱의 상업적으로 중요한 구성 요소는 유지 관리할 수 있습니다. 상태와 쇼핑 카트가 손실된 사용자는 나중에 사이트로 돌아올 때 제품 또는 서비스를 더 적게 구매할 수도 있습니다.

앱은 ‘앱 상태’만 유지할 수 있습니다. 구성 요소 인스턴스 및 렌더링 트리와 같은 UI는 유지할 수 없습니다. 구성 요소 및 렌더링 트리는 일반적으로 직렬화할 수 없습니다. 트리 뷰 컨트롤의 펼친 노드와 같은 UI 상태를 유지하려면 앱이 사용자 지정 코드를 사용하여 UI 상태의 동작을 직렬화 가능한 앱 상태로 모델링해야 합니다.

상태를 유지할 위치

상태를 유지하기 위한 공통 위치는 다음과 같습니다.

서버 쪽 스토리지

여러 사용자 및 디바이스에 걸친 영구적 데이터 지속성을 위해 앱이 서버 쪽 스토리지를 사용할 수 있습니다. 표시되는 옵션은 다음과 같습니다.

  • Blob Storage
  • 키-값 스토리지
  • 관계형 데이터베이스
  • Table Storage

데이터가 저장된 후 사용자의 상태가 유지되고 모든 새 회로에서 사용할 수 있습니다.

Azure 데이터 스토리지 옵션에 대한 자세한 내용은 다음을 참조하세요.

URL

탐색 상태를 나타내는 임시 데이터의 경우 데이터를 URL 일부로 모델링합니다. URL에 모델링된 사용자 상태의 예는 다음과 같습니다.

  • 표시된 엔터티의 ID
  • 페이징 그리드의 현재 페이지 번호

브라우저의 주소 표시줄 내용은 다음과 같은 경우에 유지됩니다.

  • 사용자가 페이지를 수동으로 다시 로드하는 경우
  • 웹 서버를 사용할 수 없게 되고 사용자가 다른 서버에 연결하기 위해 페이지를 강제로 다시 로드해야 하는 경우

@page 지시문을 사용하여 URL 패턴을 정의하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor 라우팅 및 탐색을 참조하세요.

브라우저 스토리지

사용자가 적극적으로 만드는 임시 데이터의 경우, 일반적으로 사용되는 스토리지 위치는 브라우저의 localStoragesessionStorage 컬렉션입니다.

  • localStorage 브라우저의 인스턴스로 범위가 지정됩니다. 사용자가 페이지를 다시 로드하거나 브라우저를 닫고 다시 열면 상태가 유지됩니다. 사용자가 여러 개의 브라우저 탭을 여는 경우 탭 간에 상태가 공유됩니다. 명시적으로 지울 때까지 데이터가 localStorage에 유지됩니다. 마지막 "프라이빗" 탭을 닫으면 "프라이빗 브라우징" 또는 "incognito" 세션에 로드된 문서의 localStorage 데이터가 지워집니다.
  • sessionStorage 은 브라우저 탭으로 범위가 지정됩니다. 사용자가 탭을 다시 로드하면 상태가 유지됩니다. 사용자가 탭 또는 브라우저를 닫으면 상태가 손실됩니다. 사용자가 여러 개의 브라우저 탭을 여는 경우 각 탭에 독립적인 고유한 버전의 데이터가 있습니다.

일반적으로 sessionStorage를 사용하는 것이 더 안전합니다. sessionStorage를 사용하면 사용자가 여러 탭을 열 때 다음과 같은 경우가 발생하는 위험을 방지할 수 있습니다.

  • 탭 간에 상태 스토리지의 버그
  • 한 탭에서 다른 탭의 상태를 덮어쓸 때 혼동되는 동작

localStorage 는 앱이 브라우저를 닫고 다시 여는 동안 상태를 유지해야 하는 경우 더 나은 선택입니다.

브라우저 스토리지를 사용하는 경우의 주의 사항:

  • 서버 쪽 데이터베이스를 사용하는 경우와 유사하게, 데이터 로드 및 저장은 비동기입니다.
  • 미리 렌더링하는 동안 요청된 페이지가 브라우저에 없으므로 미리 렌더링하는 동안 로컬 스토리지를 사용할 수 없습니다.
  • 서버 쪽 Blazor 앱에 대해 몇 킬로바이트 데이터 스토리지를 유지하는 것이 합리적입니다. 몇 킬로바이트를 넘을 경우, 네트워크를 통해 데이터를 로드하고 저장하기 때문에 성능에 미치는 영향을 고려해야 합니다.
  • 사용자가 데이터를 보거나 조작할 수 있습니다. ASP.NET Core 데이터 보호는 위험을 완화할 수 있습니다. 예를 들어 ASP.NET Core 보호된 브라우저 스토리지는 ASP.NET Core 데이터 보호를 사용합니다.

타사 NuGet 패키지는 localStoragesessionStorage 작업을 위한 API를 제공합니다. ASP.NET Core 데이터 보호를 투명하게 사용하는 패키지를 선택하는 것이 좋습니다. 데이터 보호는 저장된 데이터를 암호화하고, 저장된 데이터의 잠재적 변조 위험을 줄입니다. JSON 직렬화된 데이터를 일반 텍스트로 저장한 경우, 사용자는 브라우저 개발자 도구를 사용하여 데이터를 확인할 수 있으며 저장된 데이터를 수정할 수도 있습니다. 사소한 데이터 보안은 문제가 되지 않습니다. 예를 들어 저장된 UI 요소 색을 읽거나 수정하는 경우 사용자 또는 조직에 중요한 보안 위험이 되지 않습니다. 사용자가 ‘중요한 데이터’를 검사하거나 변조할 수 있도록 허용하면 안 됩니다.

ASP.NET Core 보호된 브라우저 스토리지

ASP.NET Core 보호된 브라우저 스토리지는 ASP.NET Core 데이터 보호localStoragesessionStorage에 사용합니다.

참고 항목

보호된 브라우저 스토리지는 ASP.NET Core Data Protection을 사용하며 서버 쪽 Blazor 앱에만 지원됩니다.

Warning

Microsoft.AspNetCore.ProtectedBrowserStorage는 지원되지 않는 실험적 패키지로, 프로덕션 사용에는 적합하지 않습니다.

이 패키지는 ASP.NET Core 3.1 앱에서만 사용할 수 있습니다.

구성

  1. Microsoft.AspNetCore.ProtectedBrowserStorage에 대한 패키지 참조를 추가합니다.

    참고 항목

    .NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

  2. _Host.cshtml 파일에서 닫는 </body> 태그 안에 다음 스크립트를 추가합니다.

    <script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
    
  3. Startup.ConfigureServices에서 AddProtectedBrowserStorage를 호출하여 서비스 컬렉션에 localStoragesessionStorage 서비스를 추가합니다.

    services.AddProtectedBrowserStorage();
    

구성 요소 내에서 데이터 저장 및 로드

브라우저 스토리지에 데이터를 로드하거나 저장해야 하는 구성 요소에서 @inject 지시문을 사용하여 다음 중 하나의 인스턴스를 삽입합니다.

  • ProtectedLocalStorage
  • ProtectedSessionStorage

선택은 사용하려는 브라우저 스토리지 위치에 따라 달라집니다. 다음 예제에서는 sessionStorage를 사용합니다.

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

구성 요소 대신 앱의 @using 파일에 _Imports.razor 지시문을 배치할 수 있습니다. _Imports.razor 파일을 사용하면 더 큰 앱 세그먼트나 전체 앱에서 네임스페이스를 사용할 수 있게 됩니다.

Counter을 기반으로 하는 앱의 Blazor 구성 요소에서 값을 유지하려면 IncrementCount를 사용하도록 ProtectedSessionStore.SetAsync 메서드를 수정합니다.

private async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}

더 크고 현실적인 앱에서 개별 필드 스토리지는 가능성이 거의 없는 시나리오입니다. 앱에서 복합 상태를 포함하는 전체 모델 개체를 저장할 가능성이 더 큽니다. ProtectedSessionStore는 JSON 데이터를 자동으로 직렬화 및 역직렬화하여 복잡한 상태 개체를 저장합니다.

위의 코드 예제에서 currentCount 데이터는 사용자 브라우저에 sessionStorage['count']로 저장됩니다. 데이터는 일반 텍스트로 저장되지 않고 ASP.NET Core의 데이터 보호를 사용하여 보호됩니다. 브라우저의 개발자 콘솔에서 sessionStorage['count']를 평가하는 경우 암호화된 데이터를 검사할 수 있습니다.

사용자가 새로운 회로에 있는 경우를 포함하여 사용자가 나중에 currentCount 구성 요소로 돌아오는 경우 Counter 데이터를 복구하려면 ProtectedSessionStore.GetAsync를 사용합니다.

protected override async Task OnInitializedAsync()
{
    var result = await ProtectedSessionStore.GetAsync<int>("count");
    currentCount = result.Success ? result.Value : 0;
}
protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}

구성 요소의 매개 변수에 탐색 상태가 포함된 경우, ProtectedSessionStore.GetAsync를 호출하고 null이 아닌 결과를 OnParametersSetAsync가 아니라 OnInitializedAsync에 할당합니다. OnInitializedAsync는 구성 요소를 처음 인스턴스화할 때 한 번만 호출됩니다. 나중에 사용자가 동일한 페이지를 유지하면서 다른 URL로 이동하는 경우에는 OnInitializedAsync가 다시 호출되지 않습니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 수명 주기를 참조하세요.

Warning

이 섹션의 예제는 서버에서 미리 렌더링을 사용하지 않는 경우에만 작동합니다. 미리 렌더링을 사용하도록 설정하면 구성 요소가 미리 렌더링되고 있기 때문에 JavaScript interop 호출을 실행할 수 없음을 설명하는 오류가 생성됩니다.

미리 렌더링을 사용하지 않도록 설정하거나, 미리 렌더링 작업을 위한 코드를 추가합니다. 미리 렌더링 작업을 위한 코드 작성 방법에 대한 자세한 내용은 미리 렌더링 처리 섹션을 참조하세요.

로드 상태 처리

브라우저 스토리지는 네트워크 연결을 통해 비동기로 액세스되므로 데이터가 로드되고 구성 요소에서 사용할 수 있으려면 항상 일정 시간이 지나야 합니다. 최상의 결과를 얻으려면 빈 데이터나 기본 데이터를 표시하는 대신 로드하는 동안 메시지를 렌더링합니다.

한 가지 방법은 데이터가 아직 로드 중임을 뜻하는 null인지 여부를 추적하는 것입니다. 기본 Counter 구성 요소에서 개수는 int에 저장됩니다. int으로 설정합니다.

private int? currentCount;

개수 및 Increment 단추를 무조건 표시하는 대신 HasValue를 선택하여 데이터가 로드된 경우에만 해당 요소를 표시합니다.

@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

미리 렌더링 처리

미리 렌더링 과정과 관련해서 다음 사항을 확인합니다.

  • 사용자 브라우저에 대한 대화형 연결이 없습니다.
  • 브라우저에 JavaScript 코드를 실행할 수 있는 페이지가 아직 없습니다.

미리 렌더링하는 동안 localStorage 또는 sessionStorage를 사용할 수 없습니다. 구성 요소가 스토리지와 상호 작용하려고 하면 구성 요소가 미리 렌더링되고 있기 때문에 JavaScript interop 호출을 실행할 수 없음을 설명하는 오류가 생성됩니다.

오류를 해결하는 한 가지 방법은 미리 렌더링을 사용하지 않도록 설정하는 것입니다. 앱에서 브라우저 기반 스토리지를 많이 사용하는 경우, 일반적으로 이 옵션을 선택하는 것이 가장 좋습니다. localStorage 또는 sessionStorage를 사용할 수 있어야 앱에서 유용한 콘텐츠를 미리 렌더링할 수 있기 때문에 미리 렌더링은 복잡성만 추가하고 앱에 도움이 되지 않습니다.

미리 렌더링을 사용하지 않도록 설정하려면 루트 구성 요소가 아닌 앱의 구성 요소 계층 구조에서 가장 높은 수준의 구성 요소로 설정된 prerender 매개 변수가 있는 렌더링 모드 false 를 나타냅니다.

참고 항목

구성 요소와 같은 루트 구성 요소를 대화형으로 App 만드는 것은 지원되지 않습니다. 따라서 미리 렌더링은 구성 요소에서 App 직접 사용하지 않도록 설정할 수 없습니다.

프로젝트 템플릿을 기반으로 하는 앱의 Blazor Web App 경우 일반적으로 구성 요소에서 구성 요소가 사용되는 경우 Routes 미리 렌더링이 App 비활성화됩니다(Components/App.razor).

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

또한 구성 요소에 대한 사전 렌더링을 HeadOutlet 사용하지 않도록 설정합니다.

<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />

자세한 내용은 ASP.NET Core Blazor 렌더링 모드를 참조하세요.

미리 렌더링을 사용하지 않도록 설정하려면 _Host.cshtml 파일을 열고 render-mode 특성을 Server로 변경합니다.

<component type="typeof(App)" render-mode="Server" />

미리 렌더링을 사용하지 않도록 설정<head> 미리 렌더링 이 비활성화됩니다.

localStorage 또는 sessionStorage를 사용하지 않는 다른 페이지에는 미리 렌더링이 유용할 수 있습니다. 미리 렌더링을 유지하려면 브라우저가 회로에 연결될 때까지 로드 작업을 연기합니다. 다음은 카운터 값을 저장하기 위한 예제입니다.

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;
    private bool isConnected;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount = 0;
    private bool isConnected = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("count");
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}

일반 공급자에 대한 상태 보존 고려

많은 구성 요소가 브라우저 기반 스토리지를 사용하는 경우 상태 공급자 코드를 여러 번 구현하면 코드 중복이 발생합니다. 코드 중복을 방지하는 한 가지 옵션은 상태 제공자 논리를 캡슐화하는 ‘상태 제공자 부모 구성 요소’를 만드는 것입니다. 자식 구성 요소는 상태 지속성 메커니즘과 관계없이 영구 데이터를 사용할 수 있습니다.

CounterStateProvider 구성 요소의 다음 예제에서는 카운터 데이터가 sessionStorage에 저장되어, 상태 로딩이 완료될 때까지 자식 콘텐츠를 렌더링하지 않음으로써 로드 단계를 관리합니다.

CounterStateProvider 구성 요소는 미리 렌더링하는 동안 실행되지 않는 OnAfterRenderAsync 수명 주기 메서드구성 요소 렌더링 후까지 상태를 로드하지 않음으로써 미리 렌더링을 처리합니다.

이 섹션의 접근 방식은 동일한 페이지에서 구독된 여러 구성 요소의 다시 렌더링을 트리거할 수 없습니다. 구독한 컴포넌트 중 하나가 상태를 변경하면 다시 렌더링되어 업데이트된 상태를 표시합니다. 하지만 동일한 페이지의 해당 상태를 표시하는 다른 컴포넌트는 자신이 다시 렌더링될 때까지 오래된 데이터를 표시합니다. 따라서 이 섹션에 설명된 접근 방식은 페이지의 단일 구성 요소에서 상태를 사용하는 데 가장 적합합니다.

CounterStateProvider.razor:

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}

참고 항목

RenderFragment에 대한 자세한 내용은 ASP.NET Core Razor 구성 요소를 참조하세요.

앱의 모든 구성 요소에서 상태에 액세스할 수 있도록 하려면 전역 대화형 서버 쪽 렌더링(대화형 SSR)을 사용하여 구성 요소의 () CounterStateProvider 주위에 Router 구성 요소를 래핑 <Router>...</Router> 합니다.Routes

App 구성 요소(Components/App.razor)에서:

<Routes @rendermode="InteractiveServer" />

Routes 구성 요소(Components/Routes.razor)에서:

CounterStateProvider 구성 요소를 사용하려면 카운터 상태에 액세스해야 하는 다른 모든 구성 요소를 이 구성 요소 인스턴스로 래핑합니다. 앱의 모든 구성 요소가 상태에 액세스할 수 있게 하려면 CounterStateProvider 구성 요소(Router)의 AppApp.razor 구성 요소로 래핑합니다.

<CounterStateProvider>
    <Router ...>
        ...
    </Router>
</CounterStateProvider>

참고 항목

ASP.NET Core 5.0.1 릴리스 및 추가 5.x 릴리스부터 Router 구성 요소에는 PreferExactMatches로 설정된 @true 매개 변수가 포함됩니다. 자세한 내용은 ASP.NET Core 3.1에서 5.0으로 마이그레이션을 참조하세요.

래핑된 구성 요소는 영구 카운터 상태를 받고 수정할 수 있습니다. 다음 Counter 구성 요소는 패턴을 구현합니다.

@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>

@code {
    [CascadingParameter]
    private CounterStateProvider? CounterStateProvider { get; set; }

    private async Task IncrementCount()
    {
        if (CounterStateProvider is not null)
        {
            await CounterStateProvider.IncrementCount();
        }
    }
}

위의 구성 요소는 ProtectedBrowserStorage와 상호 작용하는 데 필요하지 않으며, “로드” 단계를 처리하지도 않습니다.

일반적으로 상태 공급자 부모 구성 요소 패턴을 사용하는 것이 좋습니다.

  • 여러 구성 요소에서 상태를 사용하려는 경우
  • 유지할 최상위 상태 개체가 하나뿐인 경우

여러 다른 상태 개체를 유지하고 서로 다른 위치에 있는 여러 개체 하위 집합을 사용하려면 전역적으로 상태를 유지하지 않는 것이 좋습니다.

Blazor WebAssembly 앱에서 만든 사용자 상태는 브라우저의 메모리에 저장됩니다.

브라우저 메모리에 저장되는 사용자 상태의 예는 다음과 같습니다.

  • 구성 요소 인스턴스의 계층 구조 및 렌더링된 UI의 가장 최근 렌더링 출력.
  • 구성 요소 인스턴스의 필드 및 속성 값.
  • DI(종속성 주입) 서비스 인스턴스에 저장된 데이터.
  • JavaScript interop 호출을 통해 설정된 값.

사용자가 브라우저를 닫고 다시 열거나 페이지를 다시 로드하면 브라우저의 메모리에 저장된 사용자 상태가 손실됩니다.

참고 항목

보호된 브라우저 스토리지 (Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage 네임스페이스)는 ASP.NET Core Data Protection을 사용하며 서버 쪽 Blazor 앱에서만 지원됩니다.

브라우저 세션 간 상태 유지

일반적으로 사용자가 이미 존재하는 데이터를 단순히 읽는 것이 아니라 데이터를 적극적으로 만드는 브라우저 세션 간에 상태를 유지합니다.

브라우저 세션 간에 상태를 유지하려면 앱에서 브라우저의 메모리 외의 스토리지 위치에 데이터를 유지해야 합니다. 상태 지속성은 자동이 아닙니다. 상태 저장 데이터 지속성을 구현하려면 앱을 개발할 때 일정 단계를 수행해야 합니다.

데이터 지속성은 일반적으로 사용자가 특별히 노력해서 만든 높은 가치의 상태에만 필요합니다. 다음 예제에서 상태를 유지하면 상업 활동의 시간 또는 지원을 절약할 수 있습니다.

  • 다단계 웹 양식: 상태가 손실된 경우 사용자가 다단계 웹 양식의 완료된 여러 단계에 대한 데이터를 다시 입력하는 데 시간이 많이 걸립니다. 이 시나리오에서는 사용자가 양식을 벗어났다가 나중에 돌아올 경우 상태가 손실됩니다.
  • 쇼핑 카트: 잠재적인 수익을 나타내는 앱의 상업적으로 중요한 구성 요소는 유지 관리할 수 있습니다. 상태와 쇼핑 카트가 손실된 사용자는 나중에 사이트로 돌아올 때 제품 또는 서비스를 더 적게 구매할 수도 있습니다.

앱은 ‘앱 상태’만 유지할 수 있습니다. 구성 요소 인스턴스 및 렌더링 트리와 같은 UI는 유지할 수 없습니다. 구성 요소 및 렌더링 트리는 일반적으로 직렬화할 수 없습니다. 트리 뷰 컨트롤의 펼친 노드와 같은 UI 상태를 유지하려면 앱이 사용자 지정 코드를 사용하여 UI 상태의 동작을 직렬화 가능한 앱 상태로 모델링해야 합니다.

상태를 유지할 위치

상태를 유지하기 위한 공통 위치는 다음과 같습니다.

서버 쪽 스토리지

여러 사용자 및 디바이스에 걸친 영구적 데이터 지속성을 위해 웹 API를 통해 액세스하는 독립적인 서버 쪽 스토리지를 앱에서 사용할 수 있습니다. 표시되는 옵션은 다음과 같습니다.

  • Blob Storage
  • 키-값 스토리지
  • 관계형 데이터베이스
  • Table Storage

데이터가 저장된 후 사용자의 상태가 유지되고 모든 새 브라우저 세션에서 사용할 수 있습니다.

Blazor WebAssembly 앱은 전적으로 사용자의 브라우저에서 실행되므로 스토리지 서비스 및 데이터베이스와 같은 안전한 외부 시스템에 액세스하기 위한 추가 조치가 필요합니다. Blazor WebAssembly 앱은 SPA(단일 페이지 애플리케이션)와 동일한 방식으로 보호됩니다. 일반적으로 앱은 OAuth/OpenID Connect(OIDC)를 통해 사용자를 인증한 다음 서버 쪽 앱에 대한 웹 API 호출을 통해 스토리지 서비스 및 데이터베이스와 상호 작용합니다. 서버 쪽 앱은 Blazor WebAssembly 앱과 스토리지 서비스 또는 데이터베이스 간 데이터 전송을 중재합니다. Blazor WebAssembly 앱은 서버 쪽 앱에 대한 임시 연결을 유지하고, 서버 쪽 앱은 스토리지에 대한 영구 연결을 유지합니다.

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

Azure 데이터 스토리지 옵션에 대한 자세한 내용은 다음을 참조하세요.

URL

탐색 상태를 나타내는 임시 데이터의 경우 데이터를 URL 일부로 모델링합니다. URL에 모델링된 사용자 상태의 예는 다음과 같습니다.

  • 표시된 엔터티의 ID
  • 페이징 그리드의 현재 페이지 번호

사용자가 페이지를 수동으로 다시 로드하는 경우 브라우저 주소 표시줄의 내용이 유지됩니다.

@page 지시문을 사용하여 URL 패턴을 정의하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor 라우팅 및 탐색을 참조하세요.

브라우저 스토리지

사용자가 적극적으로 만드는 임시 데이터의 경우, 일반적으로 사용되는 스토리지 위치는 브라우저의 localStoragesessionStorage 컬렉션입니다.

  • localStorage는 브라우저의 인스턴스로 범위를 지정합니다. 사용자가 페이지를 다시 로드하거나 브라우저를 닫고 다시 열면 상태가 유지됩니다. 사용자가 여러 개의 브라우저 탭을 여는 경우 탭 간에 상태가 공유됩니다. 명시적으로 지울 때까지 데이터가 localStorage에 유지됩니다. 마지막 "프라이빗" 탭을 닫으면 "프라이빗 브라우징" 또는 "incognito" 세션에 로드된 문서의 localStorage 데이터가 지워집니다.
  • sessionStorage 은 브라우저 탭으로 범위가 지정됩니다. 사용자가 탭을 다시 로드하면 상태가 유지됩니다. 사용자가 탭 또는 브라우저를 닫으면 상태가 손실됩니다. 사용자가 여러 개의 브라우저 탭을 여는 경우 각 탭에 독립적인 고유한 버전의 데이터가 있습니다.

참고 항목

localStorage 앱에서는 sessionStorage 및 Blazor WebAssembly를 사용할 수 있지만, 사용자 지정 코드를 작성하거나 타사 패키지를 사용해야 합니다.

일반적으로 sessionStorage를 사용하는 것이 더 안전합니다. sessionStorage를 사용하면 사용자가 여러 탭을 열 때 다음과 같은 경우가 발생하는 위험을 방지할 수 있습니다.

  • 탭 간에 상태 스토리지의 버그
  • 한 탭에서 다른 탭의 상태를 덮어쓸 때 혼동되는 동작

localStorage 는 앱이 브라우저를 닫고 다시 여는 동안 상태를 유지해야 하는 경우 더 나은 선택입니다.

Warning

사용자는 localStoragesessionStorage에 저장된 데이터를 보거나 변조할 수 있습니다.

메모리 내 상태 컨테이너 서비스

중첩된 구성 요소는 일반적으로 Blazor에서 설명하는 것처럼 체인 바인딩을 사용하여 데이터를 바인딩합니다. 중첩된 구성 요소와 중첩되지 않은 구성 요소는 등록된 메모리 내 상태 컨테이너를 사용하여 데이터에 대한 액세스를 공유할 수 있습니다. 사용자 지정 상태 컨테이너 클래스는 할당 가능한 Action을 사용하여 앱의 서로 다른 구성 요소에게 상태 변경을 알립니다. 다음 예제에서

  • 구성 요소 쌍은 상태 컨테이너를 사용하여 속성을 추적합니다.
  • 다음 예제의 한 구성 요소는 다른 구성 요소에 중첩되어 있지만 이 방법을 사용하기 위해 중첩이 필요하지는 않습니다.

Important

이 섹션의 예제에서는 메모리 내 상태 컨테이너 서비스를 만들고, 서비스를 등록하고, 구성 요소에서 서비스를 사용하는 방법을 보여 줍니다. 이 예제는 추가 개발 없이는 데이터를 유지하지 않습니다. 데이터의 영구 스토리지의 경우 상태 컨테이너는 브라우저 메모리를 지울 때 유지되는 기본 스토리지 메커니즘을 채택해야 합니다. 이 작업은 localStorage/sessionStorage 또는 다른 기술로 수행할 수 있습니다.

StateContainer.cs:

public class StateContainer
{
    private string? savedString;

    public string Property
    {
        get => savedString ?? string.Empty;
        set
        {
            savedString = value;
            NotifyStateChanged();
        }
    }

    public event Action? OnChange;

    private void NotifyStateChanged() => OnChange?.Invoke();
}

클라이언트 쪽 앱(Program 파일):

builder.Services.AddSingleton<StateContainer>();

서버 쪽 앱(Program 파일, .NET 6 이상의 ASP.NET Core):

builder.Services.AddScoped<StateContainer>();

서버 쪽 앱(Startup.ConfigureServices 6.0 이전의 Startup.csASP.NET Core):

services.AddScoped<StateContainer>();

Shared/Nested.razor:

@implements IDisposable
@inject StateContainer StateContainer

<h2>Nested component</h2>

<p>Nested component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the Nested component
    </button>
</p>

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = 
            $"New value set in the Nested component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

StateContainerExample.razor:

@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer

<h1>State Container Example component</h1>

<p>State Container component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the State Container Example component
    </button>
</p>

<Nested />

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = "New value set in the State " +
            $"Container Example component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

이전 구성 요소는 IDisposable을 구현하며 OnChange 대리자는 구성 요소가 삭제될 때 프레임워크에서 호출하는 Dispose 메서드에서 구독 취소됩니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 수명 주기를 참조하세요.

추가 방법

사용자 지정 상태 스토리지를 구현할 때 유용한 방법은 연계 값 및 매개 변수를 채택하는 것입니다.

  • 여러 구성 요소에서 상태를 사용하려는 경우
  • 유지할 최상위 상태 개체가 하나뿐인 경우

문제 해결

사용자 지정 상태 관리 서비스를 사용할 때 외부 Blazor동기화 컨텍스트(예: 타이머나 백그라운드 서비스)에서 상태 수정을 지원하려면, 모든 소비 구성 요소가 StateHasChanged 호출을 ComponentBase.InvokeAsync로 래핑해야 합니다. 이렇게 하면 렌더러의 동기화 컨텍스트에서 변경 알림이 처리됩니다.

상태 관리 서비스가 '동기화 컨텍스트'를 StateHasChanged호출 Blazor 하지 않으면 다음 오류가 throw됩니다.

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

이 오류를 해결하는 방법에 대한 자세한 내용과 예제는 ASP.NET Core Razor 구성 요소 렌더링을 참조하세요.

추가 리소스