다음을 통해 공유


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 앱에만 지원됩니다.

경고

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 파일을 사용하면 더 큰 앱 세그먼트나 전체 앱에서 네임스페이스를 사용할 수 있게 됩니다.

currentCount 프로젝트 템플릿을 기반으로 하는 앱의 Counter 구성 요소에서 Blazor 값을 지속적으로 유지하려면 메서드를 IncrementCount를 사용하도록 수정합니다.

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 구성 요소 수명 주기를 참조하세요.

경고

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

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

로드 상태 처리

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

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

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)" />

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

미리 렌더링을 사용하지 않도록 설정하려면 _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 구성 요소를 참조하세요.

앱의 모든 구성 요소에서 상태에 접근할 수 있도록 하기 위해, CounterStateProvider 구성 요소 내에서 Router을(를) 포함한 <Router>...</Router> 구성 요소를 대화형 SSR(서버 측 렌더링)과 함께 Routes 주위에 래핑하십시오.

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

<Routes @rendermode="InteractiveServer" />

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

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

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

비고

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

래핑된 구성 요소는 영구 카운터 상태를 받고 수정할 수 있습니다. 다음 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와 상호 작용하는 데 필요하지 않으며, “로드” 단계를 처리하지도 않습니다.

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

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

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