Share via


ASP.NET Core 호스팅 Blazor WebAssembly 앱에 대한 배포 레이아웃

이 문서에서는 DLL(동적 연결 라이브러리) 파일의 다운로드 및 실행을 차단하는 환경에서 호스트된 Blazor WebAssembly 배포를 사용하도록 설정하는 방법을 설명합니다.

참고 항목

이 지침에서는 클라이언트가 DLL을 다운로드하고 실행하는 것을 차단하는 환경을 다룹니다. .NET 8 이상 Blazor 에서는 웹실 파일 형식을 사용하여 이 문제를 해결합니다. 자세한 내용은 ASP.NET Core Blazor WebAssembly 호스트 및 배포를 참조하세요. 이 문서에서 설명하는 실험적 NuGet 패키지를 사용하는 다중 파트 번들링은 .NET 8 이상의 앱에서 지원 Blazor 되지 않습니다. 자세한 내용은 사용자 지정 번들 형식(dotnet/aspnetcore #36978)을 정의하려면 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 패키지 향상을 참조하세요. 이 문서의 지침을 사용하여 .NET 8 이상에 대한 고유한 다중 파트 번들 NuGet 패키지를 만들 수 있습니다.

Blazor WebAssembly 앱을 사용하려면 DLL(동적 연결 라이브러리)이 작동해야 하지만 일부 환경에서는 클라이언트가 DLL을 다운로드하여 실행하지 못하도록 차단합니다. 이러한 환경 의 하위 집합에서 DLL 파일의 파일 이름 확장명(.dll) 을 변경하면 보안 제한을 우회하기에 충분하지만 보안 제품은 종종 네트워크를 트래버스하는 파일의 콘텐츠를 검색하고 DLL 파일을 차단하거나 격리할 수 있습니다. 이 문서에서는 이러한 환경에서 Blazor WebAssembly 앱을 사용하도록 설정하는 한 가지 접근 방식을 설명합니다. 이렇게 하면 앱의 DLL에서 다중 파트 번들 파일이 만들어지므로 보안 제한 사항을 무시하여 DLL을 함께 다운로드할 수 있습니다.

호스트된 Blazor WebAssembly 앱은 다음 기능을 사용하여 앱 DLL의 게시된 파일과 패키징을 사용자 지정할 수 있습니다.

  • Blazor 부팅 프로세스를 사용자 지정할 수 있는 JavaScript 이니셜라이저입니다.
  • 게시된 파일 목록을 변환하고 ‘Blazor 게시 확장’을 정의하는 MSBuild 확장성입니다. Blazor 게시 확장은 게시된 Blazor WebAssembly 앱을 실행하는 데 필요한 파일 세트에 대한 대체 표현을 제공하는 게시 프로세스 중에 정의되는 파일입니다. 이 문서에서는 DLL을 함께 다운로드할 수 있도록 모든 앱의 DLL이 단일 파일로 압축된 다중 파트 번들을 생성하는 Blazor 게시 확장이 만들어집니다.

이 문서에서 설명하는 접근 방식은 개발자가 자신의 전략과 사용자 지정 로딩 프로세스를 고안하기 위한 시작점으로 사용됩니다.

Warning

보안 제한 사항을 피하기 위해 사용되는 접근 방식은 보안에 미치는 영향에 대해 신중하게 고려해야 합니다. 이 문서의 접근 방식을 채택하기 전에 조직의 네트워크 보안 전문가와 함께 주제를 더 자세히 살펴보는 것이 좋습니다. 고려할 대안은 다음과 같습니다.

  • 보안 어플라이언스와 보안 소프트웨어를 사용하도록 설정하여 네트워크 클라이언트가 Blazor WebAssembly 앱에 필요한 정확한 파일을 다운로드하고 사용할 수 있도록 합니다.
  • Blazor WebAssembly 호스팅 모델에서 서버에 있는 모든 앱의 C# 코드를 유지 관리하고 클라이언트에 DLL을 다운로드하지 않아도 되는 Blazor Server 호스팅 모델로 전환합니다. Blazor Server는 Blazor WebAssembly 앱에서 C# 코드 프라이버시에 대해 웹 API 앱을 사용하지 않아도 C# 코드를 프라이빗으로 유지할 수 있는 이점을 제공합니다.

실험적 NuGet 패키지 및 샘플 앱

이 문서에 설명된 접근 방식은 .NET 6 이상을 대상으로 하는 앱에 대해 실험적Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 패키지(NuGet.org)에서 사용됩니다. 패키지에는 사용자 지정 부팅 리소스 로더를 사용하도록 Blazor 게시 출력과 JavaScript 이니셜라이저를 사용자 지정하는 MSBuild 대상이 포함되며 각 대상은 이 문서의 뒷부분에서 자세히 설명합니다.

실험적 코드(NuGet 패키지 참조 소스와 CustomPackagedApp 샘플 앱 포함)

Warning

사용자 의견을 수집하기 위해 실험적인 미리 보기 기능이 제공되며 프로덕션 용도로는 지원되지 않습니다.

이 문서의 뒷부분에서 NuGet 패키지를 통해 Blazor WebAssembly 로딩 프로세스 사용자 지정 섹션과 3개의 하위 섹션에서는 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 패키지의 구성과 코드에 관한 자세한 설명을 제공합니다. Blazor WebAssembly 앱에 대한 고유한 전략과 사용자 지정 로딩 프로세스를 만드는 경우를 이해하려면 자세한 설명이 중요합니다. 사용자 지정 없이 게시되고 실험적이며 지원되지 않는 NuGet 패키지를 로컬 데모로 사용하려면 다음 단계를 수행합니다.

  1. 기존 호스트된 Blazor WebAssembly솔루션을 사용하거나, Visual Studio를 사용하거나 -ho|--hosted 옵션dotnet new 명령(dotnet new blazorwasm -ho)에 전달하여 Blazor WebAssembly 프로젝트 템플릿에서 새 솔루션을 만듭니다. 자세한 내용은 ASP.NET Core Blazor 도구를 참조하세요.

  2. Client 프로젝트에서 실험적 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 패키지를 추가합니다.

    참고 항목

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

  3. Server 프로젝트에서 번들 파일(app.bundle)을 제공하기 위한 엔드포인트를 추가합니다. 예제 코드는 이 문서의 호스트 서버 앱에서 번들 제공 섹션에서 찾을 수 있습니다.

  4. 릴리스 구성에 앱을 게시합니다.

NuGet 패키지를 통해 Blazor WebAssembly 로딩 프로세스 사용자 지정

Warning

이 섹션의 지침과 세 개의 하위 섹션은 NuGet 패키지를 처음부터 빌드하여 고유한 전략과 사용자 지정 로딩 프로세스를 구현하는 것과 관련됩니다. .NET 6 및 7에 대한 실험Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle적 패키지(NuGet.org)는 이 섹션의 지침을 기반으로 합니다. 다중 파트 번들 다운로드 접근 방식의 로컬 데모에서 제공된 패키지를 사용하는 경우에는 이 섹션의 지침을 따르지 않아도 됩니다. 제공된 패키지를 사용하는 방법에 관한 지침은 실험적 NuGet 패키지 및 샘플 앱 섹션을 참조하세요.

Blazor 앱 리소스는 다중 파트 번들 파일로 압축되고 사용자 지정 JS(JavaScript) 이니셜라이저를 통해 브라우저에서 로드됩니다. JS 이니셜라이저를 통해 패키지를 사용하는 앱의 경우 앱은 요청될 때 번들 파일만 제공하면 됩니다. 이 접근 방식의 다른 모든 측면은 투명하게 처리됩니다.

기본 게시된 Blazor 앱이 로드되는 방식에는 네 가지 사용자 지정이 필요합니다.

  • 게시 파일을 변환하는 MSBuild 작업입니다.
  • Blazor 게시 프로세스에 후크되고, 출력을 변환하고, 하나 이상의 Blazor 게시 확장 파일(이 경우 단일 번들)을 정의하는 MSBuild 대상이 포함된 NuGet 패키지입니다.
  • 번들을 로드하고 앱에 개별 파일을 제공하도록 Blazor WebAssembly 리소스 로더 콜백을 업데이트하기 위한 JS 이니셜라이저입니다.
  • 요청 시 번들이 클라이언트에 제공되도록 하는 호스트 Server 앱의 도우미입니다.

게시된 파일 목록을 사용자 지정하고 새 확장을 정의하는 MSBuild 작업 만들기

MSBuild 컴파일의 일부로 가져올 수 있고 빌드와 상호 작용할 수 있는 퍼블릭 C# 클래스로 MSBuild 작업을 만듭니다.

C# 클래스에는 다음이 필요합니다.

참고 항목

이 문서의 예제를 위한 NuGet 패키지는 Microsoft에서 제공하는 패키지 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle에 따라 이름이 지정됩니다. 고유한 NuGet 패키지를 명명하고 생성하는 방법에 관한 지침은 다음 NuGet 문서를 참조하세요.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="{VERSION}" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="{VERSION}" />
  </ItemGroup>

</Project>

NuGet.org에서 {VERSION} 자리 표시자의 최신 패키지 버전을 확인합니다.

MSBuild 작업을 만들려면 Microsoft.Build.Utilities.Task(System.Threading.Tasks.Task가 아님)를 확장하여 퍼블릭 C# 클래스를 만들고 다음 세 개의 속성을 선언합니다.

  • PublishBlazorBootStaticWebAsset: Blazor 앱에 대해 게시할 파일 목록입니다.
  • BundlePath: 번들이 작성된 경로입니다.
  • Extension: 빌드에 포함할 새 게시 확장입니다.

다음 예제 BundleBlazorAssets 클래스는 추가 사용자 지정을 위한 시작점입니다.

  • 메서드에서 Execute 번들은 다음 세 가지 파일 형식에서 만들어집니다.
    • JavaScript 파일(dotnet.js)
    • WASM 파일(dotnet.wasm)
    • 앱 DLL(.dll)
  • multipart/form-data 번들이 생성됩니다. 각 파일은 Content-Disposition 헤더Content-Type 헤더를 통해 각 설명과 함께 번들에 추가됩니다.
  • 번들은 생성된 후 번들이 파일에 기록됩니다.
  • 빌드는 확장에 맞게 구성됩니다. 다음 코드는 확장 항목을 만들고 Extension 속성에 추가합니다. 각 확장 항목에는 다음 세 가지 데이터가 포함됩니다.
    • 확장 파일의 경로입니다.
    • Blazor WebAssembly 앱의 루트에 상대적인 URL 경로입니다.
    • 지정된 확장에 의해 생성된 파일을 그룹화하는 확장의 이름입니다.

위의 목표를 달성한 후 Blazor 게시 출력을 사용자 지정하기 위한 MSBuild 작업이 생성됩니다. Blazor는 확장을 수집하고 확장이 게시 출력 폴더의 올바른 위치(예: bin\Release\net6.0\publish)에 복사되었는지 확인합니다. Blazor가 다른 파일에 적용하는 것과 동일한 최적화(예: 압축)가 JavaScript, WASM, DLL 파일에 적용됩니다.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAssets.cs:

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
    public class BundleBlazorAssets : Task
    {
        [Required]
        public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

        [Required]
        public string? BundlePath { get; set; }

        [Output]
        public ITaskItem[]? Extension { get; set; }

        public override bool Execute()
        {
            var bundle = new MultipartFormDataContent(
                "--0a7e8441d64b4bf89086b85e59523b7d");

            foreach (var asset in PublishBlazorBootStaticWebAsset)
            {
                var name = Path.GetFileName(asset.GetMetadata("RelativePath"));
                var fileContents = File.OpenRead(asset.ItemSpec);
                var content = new StreamContent(fileContents);
                var disposition = new ContentDispositionHeaderValue("form-data");
                disposition.Name = name;
                disposition.FileName = name;
                content.Headers.ContentDisposition = disposition;
                var contentType = Path.GetExtension(name) switch
                {
                    ".js" => "text/javascript",
                    ".wasm" => "application/wasm",
                    _ => "application/octet-stream"
                };
                content.Headers.ContentType = 
                    MediaTypeHeaderValue.Parse(contentType);
                bundle.Add(content);
            }

            using (var output = File.Open(BundlePath, FileMode.OpenOrCreate))
            {
                output.SetLength(0);
                bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
                    .GetResult();
                output.Flush(true);
            }

            var bundleItem = new TaskItem(BundlePath);
            bundleItem.SetMetadata("RelativePath", "app.bundle");
            bundleItem.SetMetadata("ExtensionName", "multipart");

            Extension = new ITaskItem[] { bundleItem };

            return true;
        }
    }
}

게시 출력을 자동으로 변환하는 NuGet 패키지 작성

패키지가 참조될 때 자동으로 포함되는 MSBuild 대상이 있는 NuGet 패키지를 생성합니다.

  • RCL(Razor 클래스 라이브러리) 프로젝트를 만듭니다.
  • 프로젝트 사용 시 패키지를 자동으로 가져오는 NuGet 규칙에 따라 대상 파일을 만듭니다. 예를 들어, build\net6.0\{PACKAGE ID}.targets를 만듭니다. 여기서 {PACKAGE ID}는 패키지의 패키지 식별자입니다.
  • MSBuild 작업을 포함하는 클래스 라이브러리의 출력을 수집하고 출력이 올바른 위치에 압축되었는지 확인합니다.
  • Blazor 파이프라인에 연결하는 데 필요한 MSBuild 코드를 추가하고 MSBuild 작업을 호출하여 번들을 생성합니다.

이 섹션에서 설명하는 접근 방식은 패키지에 라이브러리 DLL이 포함되는 대부분의 패키지와는 다르게 대상과 콘텐츠를 제공하는 데만 패키지를 사용합니다.

Warning

이 섹션에 설명된 샘플 패키지는 Blazor 게시 프로세스를 사용자 지정하는 방법을 보여 줍니다. 샘플 NuGet 패키지는 로컬 데모용으로만 사용됩니다. 프로덕션 환경에서 이 패키지를 사용하는 것은 지원되지 않습니다.

참고 항목

이 문서의 예제를 위한 NuGet 패키지는 Microsoft에서 제공하는 패키지 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle에 따라 이름이 지정됩니다. 고유한 NuGet 패키지를 명명하고 생성하는 방법에 관한 지침은 다음 NuGet 문서를 참조하세요.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.csproj:

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Description>
      Sample demonstration package showing how to customize the Blazor publish 
      process. Using this package in production is not supported!
    </Description>
    <IsPackable>true</IsPackable>
    <IsShipping>true</IsShipping>
    <IncludeBuildOutput>false</IncludeBuildOutput>
  </PropertyGroup>

  <ItemGroup>
    <None Update="build\**" 
          Pack="true" 
          PackagePath="%(Identity)" />
    <Content Include="_._" 
             Pack="true" 
             PackagePath="lib\net6.0\_._" />
  </ItemGroup>

  <Target Name="GetTasksOutputDlls" 
          BeforeTargets="CoreCompile">
    <MSBuild Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj" 
             Targets="Publish;PublishItemsOutputGroup" 
             Properties="Configuration=Release">
      <Output TaskParameter="TargetOutputs" 
              ItemName="_TasksProjectOutputs" />
    </MSBuild>
    <ItemGroup>
      <Content Include="@(_TasksProjectOutputs)" 
               Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" 
               Pack="true" 
               PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" 
               KeepMetadata="Pack;PackagePath" />
    </ItemGroup>
  </Target>

</Project>

참고 항목

이전 예제의 <NoWarn>NU5100</NoWarn> 속성은 tasks 폴더에 배치된 어셈블리에 관한 경고를 표시하지 않습니다. 자세한 내용은 NuGet 경고 NU5100을 참조하세요.

.targets 파일을 추가하여 MSBuild 작업을 빌드 파이프라인에 연결합니다. 이 파일에서 다음 목표를 달성합니다.

  • 작업을 빌드 프로세스로 가져옵니다. DLL의 경로는 패키지에 있는 파일의 최종 위치를 기준으로 합니다.
  • ComputeBlazorExtensionsDependsOn 속성은 사용자 지정 대상을 Blazor WebAssembly 파이프라인에 연결합니다.
  • 작업 출력에서 Extension 속성을 캡처하고 BlazorPublishExtension에 추가하여 Blazor에 확장에 관해 알려 줍니다. 대상에서 작업을 호출하면 번들이 생성됩니다. 게시된 파일 목록은 PublishBlazorBootStaticWebAsset 항목 그룹의 Blazor WebAssembly 파이프라인에서 제공합니다. 번들 경로는 IntermediateOutputPath를 사용하여 정의됩니다(일반적으로 obj 폴더 내부에). 궁극적으로 번들은 게시 출력 폴더의 올바른 위치에 자동으로 복사됩니다(예: bin\Release\net6.0\publish).

패키지가 참조되면 게시하는 동안 Blazor 파일 번들이 생성됩니다.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.targets:

<Project>
  <UsingTask 
    TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.BundleBlazorAssets" 
    AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.dll" />

  <PropertyGroup>
    <ComputeBlazorExtensionsDependsOn>
      $(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
    </ComputeBlazorExtensionsDependsOn>
  </PropertyGroup>

  <Target Name="_BundleBlazorDlls">
    <BundleBlazorAssets
      PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
      BundlePath="$(IntermediateOutputPath)bundle.multipart">
      <Output TaskParameter="Extension" 
              ItemName="BlazorPublishExtension"/>
    </BundleBlazorAssets>
  </Target>

</Project>

번들에서 Blazor를 자동으로 부트스트랩

NuGet 패키지는 JS(JavaScript) 이니셜라이저를 이용하여 개별 DLL 파일을 사용하는 대신 번들에서 Blazor WebAssembly 앱을 자동으로 부트스트랩합니다. JS 이니셜라이저는 Blazor부팅 리소스 로더를 변경하고 번들을 사용하는 데 사용됩니다.

JS 이니셜라이저를 만들려면 이름이 {NAME}.lib.module.js인 JS 파일을 패키지 프로젝트의 wwwroot 폴더에 추가합니다. 여기서 {NAME} 자리 표시자는 패키지 식별자입니다. 예를 들어, Microsoft 패키지의 파일 이름은 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js로 지정됩니다. 내보낸 함수 beforeWebAssemblyStartafterWebAssemblyStarted가 로딩을 처리합니다.

JS 이니셜라이저:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeWebAssemblyStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterWebAssemblyStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

JS 이니셜라이저를 만들려면 이름이 {NAME}.lib.module.js인 JS 파일을 패키지 프로젝트의 wwwroot 폴더에 추가합니다. 여기서 {NAME} 자리 표시자는 패키지 식별자입니다. 예를 들어, Microsoft 패키지의 파일 이름은 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js로 지정됩니다. 내보낸 함수 beforeStartafterStarted가 로딩을 처리합니다.

JS 이니셜라이저:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

호스트 서버 앱에서 번들 제공

보안 제한 사항으로 인해 ASP.NET Core는 기본적으로 app.bundle 파일을 제공하지 않습니다. 클라이언트에서 요청할 때 파일을 제공하려면 요청 처리 도우미가 필요합니다.

참고 항목

동일한 최적화가 앱의 파일에 적용되는 게시 확장 프로그램에 투명하게 적용되므로 app.bundle.gzapp.bundle.br로 압축된 자산 파일은 게시할 때 자동으로 생성됩니다.

Server 프로젝트의 Program.cs에서 대체 파일을 index.html(app.MapFallbackToFile("index.html");)로 설정하는 줄 바로 앞에 C# 코드를 배치하여 번들 파일(예: app.bundle)에 대한 요청에 응답합니다.

app.MapGet("app.bundle", (HttpContext context) =>
{
    string? contentEncoding = null;
    var contentType = 
        "multipart/form-data; boundary=\"--0a7e8441d64b4bf89086b85e59523b7d\"";
    var fileName = "app.bundle";

    var acceptEncodings = context.Request.Headers.AcceptEncoding;

    if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
        .StringWithQualityHeaderValue
        .TryParseList(acceptEncodings, out var encodings))
    {
        if (encodings.Any(e => e.Value == "br"))
        {
            contentEncoding = "br";
            fileName += ".br";
        }
        else if (encodings.Any(e => e.Value == "gzip"))
        {
            contentEncoding = "gzip";
            fileName += ".gz";
        }
    }

    if (contentEncoding != null)
    {
        context.Response.Headers.ContentEncoding = contentEncoding;
    }

    return Results.File(
        app.Environment.WebRootFileProvider.GetFileInfo(fileName)
            .CreateReadStream(), contentType);
});

콘텐츠 형식은 빌드 작업의 앞부분에 정의된 형식과 일치합니다. 엔드포인트는 브라우저에서 허용하는 콘텐츠 인코딩을 확인하고 최적 파일 Brotli(.br) 또는 Gzip(.gz)을 제공합니다.