다음을 통해 공유


서버 쪽 Blazor 앱 호스트 및 배포

비고

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

경고

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

중요합니다

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

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

이 문서에서는 ASP.NET Core를 사용하여 서버 측 Blazor 앱(Blazor Web App 앱 및 Blazor Server 앱)을 호스트하고 배포하는 방법을 설명합니다.

호스트 구성 값

서버 쪽 Blazor 앱은 제네릭 호스트 구성 값을 허용할 수 있습니다.

배치

서버 쪽 호스팅 모델을 Blazor 사용하면 ASP.NET Core 앱 내에서 서버에서 실행됩니다. UI 업데이트, 이벤트 처리 및 JavaScript 호출은 SignalR 연결을 통해 처리됩니다.

ASP.NET Core 앱을 호스팅할 수 있는 웹 서버가 필요합니다. Visual Studio에는 서버 쪽 앱 프로젝트 템플릿이 포함되어 있습니다. Blazor 프로젝트 템플릿에 대한 자세한 내용은 ASP.NET Core Blazor 프로젝트 구조를 참조하세요.

릴리스 구성에서 앱을 게시하고 폴더의 bin/Release/{TARGET FRAMEWORK}/publish 콘텐츠를 배포합니다. 여기서 {TARGET FRAMEWORK} 자리 표시자는 대상 프레임워크입니다.

확장성

단일 서버의 확장성(스케일 업)을 고려할 때, 앱에 사용할 수 있는 메모리는 사용자 요구가 늘어남에 따라 앱이 소진하는 첫 번째 리소스일 가능성이 높습니다. 서버에서 사용 가능한 메모리는 다음 사항에 영향을 줍니다.

  • 서버가 지원할 수 있는 활성 회로 수
  • 클라이언트의 UI 대기 시간

안전하고 확장 가능한 서버 쪽 Blazor 앱을 빌드하는 방법에 대한 지침은 다음 리소스를 참조하세요.

각 회로는 최소 Hello World와 같은 앱에 약 250KB의 메모리를 사용합니다. 회로의 크기는 앱의 코드 및 각 구성 요소와 연결된 상태 유지 관리 요구 사항에 따라 달라집니다. 앱 및 인프라를 개발하는 동안 리소스 수요를 측정하는 것이 좋지만 다음 기준은 배포 대상을 계획하기 위한 시작점이 될 수 있습니다. 앱에서 동시 사용자 5,000명 이상을 지원할 것으로 예상되는 경우 앱에 최소 1.3GB의 서버 메모리(또는 사용자당 ~273KB)의 예산을 책정하는 것이 좋습니다.

SignalR 구성

SignalR의 호스팅 및 스케일링 조건이 Blazor를 사용하는 SignalR 앱에 적용됩니다.

구성 지침을 포함하여 앱에 대한 자세한 내용은 ASP.NET Core SignalRBlazor 지침을 참조Blazor.SignalR

운송수단

Blazor는 짧은 대기 시간, 보다 나은 안정성, 향상된 보안 덕분에 SignalR을 전송으로 사용하는 경우에 가장 효과적입니다. 긴 폴링은 WebSocket을 사용할 수 없거나 앱이 긴 폴링을 사용하도록 명시적으로 구성된 경우 SignalR에서 사용됩니다.

긴 폴링이 사용되는 경우, 콘솔 경고가 나타납니다.

긴 폴링 대체 전송을 사용하여 WebSocket을 통해 연결하지 못했습니다. 연결을 차단하는 VPN 또는 프록시 때문일 수 있습니다.

전역 배포 및 연결 실패

지리적 데이터 센터에 대한 전역 배포에 대한 권장 사항:

  • 대부분의 사용자가 있는 지역에 앱을 배포합니다.
  • 대륙 간 트래픽의 대기 시간이 증가하는 것을 고려합니다. 다시 연결 UI의 모양을 제어하려면 ASP.NET Core BlazorSignalR 지침을 참조하세요.
  • Azure SignalR 서비스를 사용하는 것이 좋습니다.

Azure App Service

Azure 앱 Service에서 호스팅하려면 WebSocket 및 세션 선호도(ARR(애플리케이션 요청 라우팅) 선호도라고도 함)에 대한 구성이 필요합니다.

비고

Azure App Service의 Blazor 앱은 Azure SignalR 서비스가 필요하지 않습니다 .

Azure 앱 Service에서 앱 등록에 대해 다음을 사용하도록 설정합니다.

  • WebSockets 전송이 기능하도록 하는 것입니다. 기본 설정은 꺼짐입니다.
  • 사용자의 요청을 동일한 App Service 인스턴스로 다시 라우팅하는 세션 선호도입니다. 기본 설정은 기입니다.
  1. Azure Portal에서 App Services의 웹앱으로 이동합니다.
  2. 설정>구성
  3. 웹 소켓켜기로 설정합니다.
  4. 세션 선호도가 On으로 설정되어 있는지 확인합니다.

Azure SignalR 서비스

선택적 Azure SignalR 서비스는 서버 쪽 앱을 많은 수의 동시 연결로 확장하기 위해 앱의 SignalR 허브와 함께 작동합니다. 또한 서비스의 글로벌 및 고성능 데이터 센터는 지리적 위치로 인한 대기 시간을 줄이는 데 큰 도움이 됩니다.

서비스는 Azure 앱 Service 또는 Azure Container Apps에서 호스트되는 앱에 필요하지 Blazor 않지만 다른 호스팅 환경에서 유용할 수 있습니다.

  • 연결 규모를 쉽게 확장할 수 있도록 합니다.
  • 전역 배포를 처리합니다.

SDK SignalR 이상의 Azure Service는 SignalR 상태 저장 다시 연결(WithStatefulReconnect)을 지원합니다.

앱이 긴 폴링을 사용하거나 WebSocket 대신 긴 폴링으로 대체되는 경우 Azure MaxPollIntervalInSeconds 서비스에서 긴 폴링 연결에 허용되는 최대 폴링 간격을 정의하는 최대 폴링 간격(SignalR기본값: 5초, 제한: 1-300초)을 구성해야 할 수 있습니다. 다음 폴링 요청이 최대 폴링 간격 내에 도착하지 않으면 서비스는 클라이언트 연결을 닫습니다.

프로덕션 배포에 종속성으로 서비스를 추가하는 방법에 대한 지침은 Azure 앱 서비스에 ASP.NET Core SignalR 앱 게시를 참조하세요.

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

Azure Container Apps (Azure 컨테이너 애플리케이션)

Azure Container Apps 서비스에서 서버 쪽 Blazor 앱의 크기를 조정하는 방법을 자세히 알아보려면 Azure에서 ASP.NET Core Apps 크기 조정을 참조하세요. 이 자습서에서는 Azure Container Apps에서 앱을 호스트하는 데 필요한 서비스를 만들고 통합하는 방법을 설명합니다. 이 섹션에서는 기본 단계도 제공합니다.

  1. Azure Container Apps의 세션 선호도(Azure 설명서)지침에 따라 세션 선호도에 대한 Azure Container Apps 서비스를 구성합니다.

  2. ASP.NET Core Data Protection(DP) 서비스는 모든 컨테이너 인스턴스가 액세스할 수 있는 중앙 집중식 위치에 키를 유지하도록 구성해야 합니다. 키는 Azure Blob Storage에 저장하고 Azure Key Vault를 사용하여 보호할 수 있습니다. DP 서비스는 키를 사용하여 구성 요소를 역직렬화 Razor 합니다. Azure Blob Storage 및 Azure Key Vault를 사용하도록 DP 서비스를 구성하려면 다음 NuGet 패키지를 참조하세요.

    비고

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

  3. Program.cs를 다음 강조된 코드로 업데이트합니다.

    using Azure.Identity;
    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.Extensions.Azure;
    
    var builder = WebApplication.CreateBuilder(args);
    var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
    var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
    
    builder.Services.AddRazorPages();
    builder.Services.AddHttpClient();
    builder.Services.AddServerSideBlazor();
    
    builder.Services.AddAzureClientsCore();
    
    builder.Services.AddDataProtection()
                    .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                    new DefaultAzureCredential())
                    .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                    new DefaultAzureCredential());
    var app = builder.Build();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    

    위의 변경 내용을 통해 앱은 중앙 집중식 확장성 아키텍처를 사용하여 DP 서비스를 관리할 수 있습니다. DefaultAzureCredential 코드가 Azure에 배포된 후 컨테이너 앱 관리 ID를 검색하고 이를 사용하여 Blob Storage 및 앱의 키 자격 증명 모음에 연결합니다.

  4. 컨테이너 앱 관리 ID를 만들고 Blob Storage 및 키 자격 증명 모음에 대한 액세스 권한을 부여하려면 다음 단계를 완료합니다.

    1. Azure Portal에서 컨테이너 앱의 개요 페이지로 이동합니다.
    2. 왼쪽 탐색 영역에서 서비스 커넥터를 선택합니다.
    3. 위쪽 탐색 영역에서 + 만들기를 선택합니다.
    4. 연결 플라이아웃 만들기 메뉴에서 다음 값을 입력합니다.
      • 컨테이너: 앱을 호스트하기 위해 만든 컨테이너 앱을 선택합니다.
      • 서비스 유형: Blob Storage를 선택합니다.
      • 구독: 컨테이너 앱을 소유하는 구독을 선택합니다.
      • 연결 이름: scalablerazorstorage의 이름을 입력합니다.
      • 클라이언트 유형: .NET을 선택하고 다음을 선택합니다.
    5. 시스템 할당 관리 ID를 선택하고, 다음을 선택합니다 .
    6. 기본 네트워크 설정을 사용하고 다음을 선택합니다.
    7. Azure에서 설정의 유효성을 검사한 후 만들기를 선택합니다.

    키 자격 증명 모음에 대한 이전 설정을 반복합니다. 기본 탭에서 적절한 키 자격 증명 저장소 서비스와 키를 선택합니다.

비고

위의 예제에서는 Azure 호스팅 환경에서 사용되는 자격 증명과 로컬 개발에 사용되는 자격 증명을 결합하여 Azure에 배포하는 앱을 개발하는 동안 인증을 간소화하는 데 사용합니다 DefaultAzureCredential . 프로덕션으로 전환할 때와 같은 ManagedIdentityCredential대안이 더 나은 선택입니다. 자세한 내용은 시스템 할당 관리 ID를 사용하여 Azure 리소스에 Azure 호스팅 .NET 앱 인증을 참조하세요.

IIS

IIS를 사용하는 경우 다음을 사용하도록 설정합니다.

자세한 내용은 IIS에 ASP.NET Core 앱 게시의 지침 및 외부 IIS 리소스 교차 링크를 참조하세요.

쿠버네티스

세션 어피니티를 위한 다음 Kubernetes 주석을 사용하여 인그레스 정의를 만듭니다.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"

Nginx를 사용하는 Linux

다음 변경 내용에 대해 ASP.NET Core SignalR 앱의 지침을 따릅니다.

  • location 경로를 /hubroute(location /hubroute { ... })에서 루트 경로 /(location / { ... })로 변경합니다.
  • 이 설정은 proxy_buffering off; 앱 클라이언트-서버 상호 작용과 관련이 없는 SSE(Server-Sent 이벤트)에 적용되므로 프록시 버퍼링(Blazor)에 대한 구성을 제거합니다.

자세한 내용 및 구성 지침은 다음 리소스를 참조하세요.

Apache를 사용하는 Linux

Linux에서 Apache 뒤에 Blazor 앱을 호스트하려면 HTTP 및 WebSockets 트래픽에 대해 ProxyPass를 구성합니다.

다음 예제에서

  • Kestrel 서버가 호스트 머신에서 실행되고 있습니다.
  • 앱은 포트 5000에서 트래픽을 수신 대기합니다.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

다음 모듈을 사용하도록 설정합니다.

a2enmod   proxy
a2enmod   proxy_wstunnel

브라우저 콘솔에서 WebSockets 오류를 확인합니다. 오류의 예:

  • Firefox에서 서버(ws://the-domain-name.tld/_blazor?id=XXX)에 대한 연결을 설정할 수 없습니다.
  • 오류: 'WebSockets' 전송을 시작하지 못했습니다. 오류: 전송에 오류가 발생했습니다.
  • 오류: 전송 'LongPolling'을 시작하지 못했습니다. TypeError: this.transport이 정의되지 않았습니다.
  • 오류: 사용 가능한 전송을 사용하여 서버에 연결할 수 없습니다. WebSockets 실패
  • 오류: 연결이 '연결됨' 상태가 아니면 데이터를 보낼 수 없습니다.

자세한 내용 및 구성 지침은 다음 리소스를 참조하세요.

네트워크 대기 시간 측정

JS interop은 다음 예제와 같이 네트워크 대기 시간을 측정하는 데 사용할 수 있습니다.

MeasureLatency.razor:

@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}

적절한 UI 환경을 위해 UI 대기 시간을 250ms 이하로 유지하는 것이 좋습니다.