시리즈: .NET 10 미리보기 살펴보기 | Andrew Lock


.NET 10 Preview 시리즈 상세 요약

개요

Andrew Lock의 .NET Escapades 블로그에서 발행된 “.NET 10 Preview 탐색” 시리즈는 .NET 10의 프리뷰 기능들을 심도 있게 다룬 7편의 글로 구성되어 있습니다. 각 글은 .NET 10에서 도입된 새로운 기능들을 실제 예제와 구현 세부사항을 통해 상세히 설명합니다.


Part 1: dotnet run app.cs 기능 탐색

주요 내용

.NET 10의 새로운 단일 파일 실행 기능

.NET 10에서는 프로젝트 파일(.csproj) 없이도 단일 C# 파일만으로 애플리케이션을 실행할 수 있는 혁신적인 기능이 도입되었습니다.

핵심 기능들

1. 기본 사용법

# 단순한 Hello World
Console.WriteLine("Hello, world");
# app.cs로 저장 후
dotnet run app.cs

2. 지원되는 지시문들

#:sdk 지시문 - SDK 참조

#:sdk Microsoft.NET.Sdk.Web
#:sdk Aspire.AppHost.Sdk 9.3.0

#:package 지시문 - NuGet 패키지 참조

#:package Newtonsoft.Json@13.0.3
#:package Aspire.Hosting.AppHost@*
#:package SomePackage@9.*

#:property 지시문 - MSBuild 속성 설정

#:property UserSecretsId 2eec9746-c21a-4933-90af-c22431f35459

#:project 지시문 - 프로젝트 참조 (Preview 6에서 예정)

#:project ../src/MyProject
#:project ../src/MyProject/MyProject.csproj

3. Shebang 지원

#!/usr/bin/dotnet run
Console.WriteLine("Hello from script!");

Unix 시스템에서 직접 실행 가능합니다.

대상 사용자

  • .NET 입문자: 프로젝트 파일의 복잡성 없이 학습 가능
  • 유틸리티 스크립트: Bash나 PowerShell 대신 C# 사용
  • 샘플 애플리케이션: 각 .cs 파일이 독립적인 샘플
  • 기존 스크립팅 도구 사용자: Cake, dotnet-script, CS-Script 등에서 전환

예정된 기능들

dotnet publish 지원 (Preview 6)

dotnet publish app.cs
# 기본적으로 NativeAOT 앱으로 빌드됨

dotnet app.cs 직접 실행

# 기존: dotnet run app.cs
# 새로움: dotnet app.cs

stdin에서 C# 코드 실행

echo 'Console.WriteLine("Hello, World!");' | dotnet run -

변환 기능

# 단일 파일을 정규 프로젝트로 변환
dotnet project convert app.cs

고급 활용

  • Escape Hatches: global.json, NuGet.config, Directory.Build.props 등 지원
  • 다중 파일 지원: .NET 11에서 계획됨
  • Visual Studio 지원: Visual Studio Code만 지원 (Visual Studio는 미지원)

Part 2: dotnet run app.cs 구현 내부

구현 아키텍처

이 기능은 “가상” .csproj 파일을 생성하여 기존 .NET 빌드 시스템을 활용하는 방식으로 구현되었습니다.

처리 단계

1. 단일 파일 프로그램 식별

public static bool IsValidEntryPointPath(string entryPointFilePath)
{
    if (!File.Exists(entryPointFilePath))
        return false;

    if (entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
        return true;

    // #! shebang 확인
    try
    {
        using var stream = File.OpenRead(entryPointFilePath);
        int first = stream.ReadByte();
        int second = stream.ReadByte();
        return first == '#' && second == '!';
    }
    catch
    {
        return false;
    }
}

2. 캐시 확인

빌드가 필요한지 확인하는 로직:

  • 전역 MSBuild 속성 비교
  • 진입점 파일 변경 여부
  • 암시적 파일들(global.json, NuGet.config 등) 변경 여부
  • 새로운 암시적 빌드 파일 존재 여부

3. 가상 프로젝트 생성

WriteProjectFile() 메서드가 핵심 역할을 담당:

SDK 처리

<!-- 기본 SDK -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<!-- 추가 SDK들 -->
<Import Project="Sdk.props" Sdk="Aspire.AppHost.Sdk" Version="9.3.0" />

기본 속성들

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>

파일 기반 프로그램 기능 활성화

<PropertyGroup>
    <Features>$(Features);FileBasedProgram</Features>
</PropertyGroup>

4. MSBuild 실행

  • Restore 실행
  • Build 실행
  • 캐시 항목 저장

가상 프로젝트 예제

단일 파일:

#!/program
#:sdk Microsoft.NET.Sdk
#:sdk Aspire.Hosting.Sdk@9.1.0
#:property TargetFramework=net11.0
#:package System.CommandLine@2.0.0-beta4.22272.1
#:property LangVersion=preview
Console.WriteLine();

생성되는 가상 프로젝트:

<Project>
  <PropertyGroup>
    <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
    <ArtifactsPath>/artifacts</ArtifactsPath>
  </PropertyGroup>

  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <!-- 사용자 정의 속성들 -->
  <PropertyGroup>
    <TargetFramework>net11.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
  </ItemGroup>

  <ItemGroup>
    <Compile Include="/path/to/Program.cs" />
  </ItemGroup>

  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
  <Import Project="Sdk.targets" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
</Project>

Part 3: C# 14 확장 멤버 - 모든 것의 확장

배경

C# 3.0에서 도입된 확장 메서드는 LINQ를 지원하기 위한 기능이었지만, 개발자들은 오랫동안 확장 속성, 정적 확장 메서드 등 "확장 모든 것"을 요구해왔습니다. C# 14에서 드디어 이 기능이 "확장 멤버"라는 이름으로 도입되었습니다.

새로운 확장 멤버 문법

기존 확장 메서드에서 변환

기존 문법:

public static class EnumerableExtensions
{
    public static bool IsEmpty<T>(this IEnumerable<T> target)
        => !target.Any();
}

새로운 확장 멤버 문법:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> target)
    {
        public bool IsEmpty() => !target.Any();
    }
}

변환 과정

  1. extension(){ } 블록으로 메서드 감싸기
  2. 수신자 매개변수를 extension 블록으로 이동
  3. 제네릭 타입 인수를 extension 블록으로 이동
  4. 메서드에서 static 수정자 제거

새로운 확장 멤버 유형

1. 정적 확장 메서드

public static class StringExtensions
{
    extension(string)  // 매개변수 이름 불필요
    {
        public static bool HasValue(string value)
            => !string.IsNullOrEmpty(value);
    }
}

// 사용법
if (string.HasValue(someValue))  // 타입에서 직접 호출
{
    Console.WriteLine("The value was: " + someValue);
}

2. 인스턴스 확장 속성

public static class StringExtensions
{
    extension(string target)
    {
        public bool IsAscii
            => target.All(x => char.IsAscii(x));
    }
}

// 사용법
string someValue = "something";
bool isAscii = someValue.IsAscii;  // 인스턴스에서 속성 접근

3. 정적 확장 속성

public static class StringExtensions
{
    extension(string)
    {
        public static bool SomeStaticProperty { get; set; }
    }
}

4. 확장 연산자 (Preview 7 예정)

static class PathExtensions
{
    extension(string)
    {
        public static string operator /(string left, string right)
            => Path.Combine(left, right);
    }
}

// 사용법
var fullPath = "part1" / "part2" / "test.txt";

직접 호출 방법 (모호성 해결)

// 인스턴스 확장 메서드
bool isEmpty = EnumerableExtensions.IsEmpty([]);

// 정적 확장 메서드
bool hasValue = StringExtensions.HasValue("something");

// 인스턴스 확장 속성
bool isAscii = StringExtensions.get_IsAscii("something");

실제 활용 사례: NetEscapades.EnumGenerators

C# 14 지원 전:

var colour = MyColoursExtensions.Parse("Red");  // 클래스를 통해 호출

C# 14 지원 후:

var colour = MyColours.Parse("Red");  // 열거형에서 직접 호출

생성된 코드:

public static partial class MyColoursExtensions
{
    // 기존 확장 메서드들
    public static string ToStringFast(this global::MyColours value) { }
    public static int AsUnderlyingType(this global::MyColours value) { }

    // C#14용 확장 블록
    extension(global::MyColours)
    {
        public static bool IsDefined(global::MyColours value) { }
        public static global::MyColours Parse(string? name) { }
    }
}

Part 4: 소스 생성기의 ‘마커 특성’ 문제 해결

문제 정의

소스 생성기에서 마커 특성을 사용할 때 발생하는 CS0436 경고 문제:

[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(i =>
        {
            i.AddSource("MyExampleAttribute.g.cs", @"
                namespace HelloWorld
                {
                    internal class MyExampleAttribute: global::System.Attribute {} 
                }");
        });
    }
}

문제 상황:

  • 여러 프로젝트에서 같은 생성기 사용
  • 한 프로젝트가 다른 프로젝트 참조
  • [InternalsVisibleTo] 사용

결과: 동일한 타입이 여러 프로젝트에 정의되어 충돌

기존 해결책: 공유 DLL

마커 특성을 별도 DLL에 포함하여 NuGet 패키지에 패키징:

장점:

  • 모든 프로젝트가 동일한 타입 참조
  • 타입 충돌 없음

단점:

  • 빌드 복잡성 증가
  • 별도 프로젝트 필요
  • MSBuild 설정 복잡

.NET 10의 새로운 해결책: [Embedded] 특성

Embedded 특성 정의

namespace Microsoft.CodeAnalysis
{
    internal sealed partial class EmbeddedAttribute : global::System.Attribute
    {
    }
}

요구사항

  1. internal 접근 수준
  2. class여야 함
  3. sealed여야 함
  4. static
  5. internal 또는 public 매개변수 없는 생성자
  6. System.Attribute에서 상속
  7. 모든 타입 선언에서 허용

사용법

[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context
            .AddEmbeddedAttributeDefinition()  // ← 추가
            .RegisterPostInitializationOutput(i =>
            {
                i.AddSource("MyExampleAttribute.g.cs", @"
                namespace HelloWorld
                {
                    [global::Microsoft.CodeAnalysis.EmbeddedAttribute]  // ← 추가
                    internal class MyExampleAttribute: global::System.Attribute {} 
                }");
            });
    }
}

장단점 비교

AddEmbeddedAttributeDefinition() 사용 시기

장점:

  • 구현 간단
  • 추가 프로젝트 불필요
  • 타입 충돌 해결

단점:

  • .NET 10 SDK 필요 (Roslyn 4.14+)
  • Visual Studio 17.14+ 필요
  • 기존 생성기에는 파괴적 변경

공유 DLL 계속 사용 시기

  • 이미 공유 DLL 방식 구현됨
  • 추가 기능이 필요 (공개 API 타입 등)
  • 구버전 SDK 지원 필요

혼합 접근법

// 마커 특성은 [Embedded]로 생성
extension(string)
{
    [Embedded]
    internal class MyMarkerAttribute : Attribute { }
}

// 공개 API 타입은 공유 DLL에 포함
public enum TransformType
{
    LowerInvariant,
    UpperInvariant
}

Part 5: dnx를 통한 일회성 .NET 도구 실행

개요

Node.js의 npx와 유사한 기능으로, .NET 도구를 설치하지 않고 직접 실행할 수 있는 dnx 명령이 .NET 10 Preview 6에 추가되었습니다.

기본 사용법

도구 실행

# 처음 실행 시 확인 프롬프트
> dnx dotnetsay
Tool package dotnetsay@2.1.7 will be downloaded from source https://api.nuget.org/v3/index.json.
Proceed? [y/n] (y): y

        Welcome to using a .NET Core global tool!

# 두 번째 실행부터는 즉시 실행
> dnx dotnetsay

        Welcome to using a .NET Core global tool!

버전 지정

# @ 구분자로 버전 지정
dnx dotnetsay@2.1.7
dnx SomePackage@*
dnx SomePackage@9.*

지원 옵션

dnx --help
Usage:
  dotnet dnx <packageId> [<commandArguments>...] [options]

Options:
  --version <VERSION>       도구 패키지 버전
  -y, --yes                모든 확인 프롬프트를 "yes"로 자동 응답
  --interactive            사용자 입력 대기 허용
  --allow-roll-forward     새 .NET 런타임 버전으로 롤포워드 허용
  --prerelease             프리릴리스 패키지 포함
  --configfile <FILE>      사용할 NuGet 구성 파일
  --source <SOURCE>        설치 중 사용할 NuGet 패키지 소스
  --add-source <ADDSOURCE> 추가 NuGet 패키지 소스

dotnet tool install과의 차이점

전통적인 전역 설치

# 전역 설치
dotnet tool install -g dotnetsay

# 설치 위치: ~/.dotnet/tools/
# 실행: 직접 명령어로 실행 가능
dotnetsay

dnx 방식

# 일회성 실행
dnx dotnetsay

# 다운로드 위치: 전역 패키지 캐시
# 도구 저장소나 shim 생성 안함
# 패키지 캐시에서 직접 실행

내부 구현

독립 실행 파일

  • Windows: C:\Program Files\dotnet\dnx.cmd
  • Linux/macOS: /usr/share/dotnet/dnx

dnx.cmd 내용:

@echo off
"%~dp0dotnet.exe" dnx %*

Unix 스크립트:

#!/bin/sh
"$(dirname "$0")/dotnet" dnx "$@"

처리 단계

  1. 로컬 도구 매니페스트 확인: 버전이 지정되지 않은 경우 dotnet-tools.json 확인
  2. 패키지 검색: nuget.org에서 패키지 검색
  3. 권한 확인: 다운로드 권한 요청 (–yes로 생략 가능)
  4. 도구 실행: 다운로드 후 실행

활용 시나리오

  • 개발 중 빠른 테스트: 도구를 영구 설치하지 않고 테스트
  • CI/CD 파이프라인: 일회성 도구 사용
  • 스크립팅: 도구 설치 없이 스크립트에서 활용
  • 탐색적 사용: 새로운 도구 시도

Part 6: ASP.NET Core Identity의 Passkey 지원

Passkey 개요

Passkey는 비밀번호 없는 인증 방식으로, FIDO(Fast IDentity Online) 표준을 기반으로 합니다:

  • 보안: 피싱 공격에 면역
  • 편의성: 생체인식이나 PIN으로 인증
  • 표준화: 웹 표준 WebAuthn 기반

.NET 10에서의 제한사항

중요: 현재 구현은 여전히 비밀번호를 요구합니다.

  • 계정 생성 시 비밀번호 필수
  • Passkey는 추가 인증 수단으로만 사용
  • 진정한 “비밀번호 없는” 환경은 아님

새로운 템플릿 경험

프로젝트 생성

dotnet new blazor -au Individual

사용자 등록

  1. 일반적인 비밀번호 기반 등록
  2. 이메일 확인
  3. 로그인 후 계정 관리 페이지로 이동

Passkey 추가

  1. 계정 관리 → Passkeys 섹션
  2. “Add a new passkey” 클릭
  3. 브라우저 네이티브 다이얼로그
  4. 생체인식/PIN 인증
  5. Passkey 이름 지정

Passkey 로그인

  1. 로그인 페이지에서 “Log in with a passkey” 클릭
  2. 브라우저에서 저장된 Passkey 선택
  3. 생체인식/PIN 인증
  4. 즉시 로그인 완료

기술적 구현

핵심 컴포넌트

PasskeySubmit.razor:

<button type="submit" name="__passkeySubmit" @attributes="AdditionalAttributes">
    @ChildContent
</button>
<passkey-submit operation="@Operation" name="@Name" email-name="@EmailName">
</passkey-submit>

JavaScript 구현 (PasskeySubmit.razor.js)

customElements.define('passkey-submit', class extends HTMLElement {
    static formAssociated = true;

    connectedCallback() {
        this.internals = this.attachInternals();
        this.attrs = {
            operation: this.getAttribute('operation'),
            name: this.getAttribute('name'),
            emailName: this.getAttribute('email-name'),
        };

        // 폼 제출 이벤트 처리
        this.internals.form.addEventListener('submit', (event) => {
            if (event.submitter?.name === '__passkeySubmit') {
                event.preventDefault();
                this.obtainCredentialAndSubmit();
            }
        });

        // 자동 완성 시도
        this.tryAutofillPasskey();
    }

    async obtainCredentialAndSubmit(useConditionalMediation = false) {
        let credential;
        if (this.attrs.operation === 'Create') {
            credential = await createCredential(signal);
        } else if (this.attrs.operation === 'Request') {
            const email = new FormData(this.internals.form).get(this.attrs.emailName);
            const mediation = useConditionalMediation ? 'conditional' : undefined;
            credential = await requestCredential(email, mediation, signal);
        }

        // 자격 증명을 JSON으로 변환하여 폼에 추가
        const credentialJson = JSON.stringify(credential);
        formData.append(`${this.attrs.name}.CredentialJson`, credentialJson);
        
        this.internals.setFormValue(formData);
        this.internals.form.submit();
    }
});

WebAuthn API 호출

Passkey 생성:

async function createCredential(signal) {
    // ASP.NET Core Identity 엔드포인트 호출
    const optionsResponse = await fetchWithErrorHandling('/Account/PasskeyCreationOptions', {
        method: 'POST',
        signal,
    });

    const optionsJson = await optionsResponse.json();
    const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);

    // 브라우저 WebAuthn API 호출
    return await navigator.credentials.create({ publicKey: options, signal });
}

Passkey 인증:

async function requestCredential(email, mediation, signal) {
    const optionsResponse = await fetchWithErrorHandling(
        `/Account/PasskeyRequestOptions?username=${email}`, {
        method: 'POST',
        signal,
    });

    const optionsJson = await optionsResponse.json();
    const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);

    return await navigator.credentials.get({ publicKey: options, mediation, signal });
}

백엔드 API 엔드포인트

Passkey 생성 옵션:

accountGroup.MapPost("/PasskeyCreationOptions", async (
    HttpContext context,
    [FromServices] UserManager<ApplicationUser> userManager,
    [FromServices] SignInManager<ApplicationUser> signInManager) =>
{
    var user = await userManager.GetUserAsync(context.User);
    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";
    var userEntity = new PasskeyUserEntity(userId, userName, displayName: userName);
    var passkeyCreationArgs = new PasskeyCreationArgs(userEntity);

    var options = await signInManager.ConfigurePasskeyCreationOptionsAsync(passkeyCreationArgs);
    return TypedResults.Content(options.AsJson(), contentType: "application/json");
});

데이터베이스 스키마

CREATE TABLE AspNetUserPasskeys (
    CredentialId BLOB(1024) NOT NULL PRIMARY KEY,
    UserId TEXT NOT NULL,
    Data TEXT NOT NULL,
    FOREIGN KEY (UserId) REFERENCES AspNetUsers (Id) ON DELETE CASCADE
);

주요 특징

  • 조건부 중재: 자동 완성 지원
  • 크로스 디바이스: 다른 기기의 Passkey 사용 가능
  • 다중 Passkey: 여러 디바이스에 Passkey 등록 가능
  • 관리 기능: Passkey 이름 변경, 삭제

한계점

  1. 비밀번호 여전히 필요: 진정한 비밀번호 없는 환경 아님
  2. 프리뷰 기능: API 변경 가능성
  3. 제한된 WebAuthn 구현: 전체 FIDO2 라이브러리 아님
  4. Attestation 검증 없음: 확장성 포인트로 제3자 라이브러리 사용 필요

Part 7: 자체 포함 및 Native AOT .NET 도구의 NuGet 패키징

개요

.NET 10 Preview 6부터 .NET 도구를 다양한 배포 모델로 패키징할 수 있게 되었습니다:

  1. 프레임워크 종속, 플랫폼 무관 (기존 방식)
  2. 프레임워크 종속, 플랫폼 특정
  3. 자체 포함, 플랫폼 특정
  4. 자체 포함, 트림됨
  5. Native AOT 컴파일됨

샘플 애플리케이션

테스트용 도구 설정:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>sayhello</ToolCommandName>
    <PackageId>sayhello</PackageId>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.50.0" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.8" />
  </ItemGroup>
</Project>

1. 프레임워크 종속, 플랫폼 무관 (기존 방식)

dotnet pack -o ./artifacts/packages/agnostic

특징:

  • 패키지 크기: ~91MB (대용량)
  • 모든 대상 프레임워크 포함
  • 모든 플랫폼용 네이티브 파일 포함
  • .NET 런타임 설치 필요

DotnetToolSettings.xml:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="1">
  <Commands>
    <Command Name="sayhello" EntryPoint="MultiRid.dll" Runner="dotnet" />
  </Commands>
</DotNetCliTool>

2. 프레임워크 종속, 플랫폼 특정

<PropertyGroup>
  <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64;any</RuntimeIdentifiers>
  <PublishSelfContained>false</PublishSelfContained>
</PropertyGroup>
dotnet pack -o ./artifacts/packages/specific

결과: 6개 패키지 생성

  • sayhello.1.0.0.nupkg (루트 패키지)
  • sayhello.linux-x64.1.0.0.nupkg
  • sayhello.linux-arm64.1.0.0.nupkg
  • sayhello.win-x64.1.0.0.nupkg
  • sayhello.win-arm64.1.0.0.nupkg
  • sayhello.any.1.0.0.nupkg (폴백)

루트 패키지 DotnetToolSettings.xml:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="2">
  <Commands>
    <Command Name="sayhello" />
  </Commands>
  <RuntimeIdentifierPackages>
    <RuntimeIdentifierPackage RuntimeIdentifier="linux-x64" Id="sayhello.linux-x64" />
    <RuntimeIdentifierPackage RuntimeIdentifier="linux-arm64" Id="sayhello.linux-arm64" />
    <RuntimeIdentifierPackage RuntimeIdentifier="win-x64" Id="sayhello.win-x64" />
    <RuntimeIdentifierPackage RuntimeIdentifier="win-arm64" Id="sayhello.win-arm64" />
    <RuntimeIdentifierPackage RuntimeIdentifier="any" Id="sayhello.any" />
  </RuntimeIdentifierPackages>
</DotNetCliTool>

플랫폼별 패키지 DotnetToolSettings.xml:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="2">
  <Commands>
    <Command Name="sayhello" EntryPoint="MultiRid" Runner="executable" />
  </Commands>
</DotNetCliTool>

3. 자체 포함, 플랫폼 특정

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64</RuntimeIdentifiers>
  <PublishSelfContained>true</PublishSelfContained>
</PropertyGroup>

특징:

  • .NET 런타임 포함
  • 단일 타겟 프레임워크
  • 패키지 크기: 각각 ~30MB
  • 대상 머신에 .NET 설치 불필요

4. 자체 포함, 트림됨

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64</RuntimeIdentifiers>
  <PublishSelfContained>true</PublishSelfContained>
  <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

특징:

  • 미사용 코드 제거
  • 패키지 크기: 각각 ~10MB
  • 트리밍 호환성 고려 필요

5. Native AOT

<PropertyGroup>
  <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64</RuntimeIdentifiers>
  <PublishSelfContained>true</PublishSelfContained>
  <PublishTrimmed>true</PublishTrimmed>
  <PublishAot>true</PublishAot>
  <StripSymbols>true</StripSymbols>
</PropertyGroup>

빌드 과정:

# 루트 패키지
dotnet pack -o ./artifacts/packages/nativeoat

# 각 런타임별로 개별 빌드 필요
dotnet pack -o ./artifacts/packages/nativeaot --runtime-id win-x64
dotnet pack -o ./artifacts/packages/nativeaot --runtime-id linux-x64
# ... 다른 런타임들

특징:

  • 패키지 크기: 각각 ~2.5MB (최소)
  • 빠른 시작 시간
  • 작은 메모리 풋프린트
  • 플랫폼별로 개별 빌드 필요

패키지 크기 비교

배포 모델 패키지 크기 특징
플랫폼 무관 ~91MB 모든 플랫폼, 모든 프레임워크
플랫폼 특정 ~20MB 각각 단일 플랫폼, 모든 프레임워크
자체 포함 ~30MB 각각 런타임 포함
트림됨 ~10MB 각각 미사용 코드 제거
Native AOT ~2.5MB 각각 최적화된 네이티브 코드

제한사항 및 권장사항

현재 제한사항

  1. .NET 10 SDK 필요: Version 2 도구는 .NET 10 SDK에서만 설치 가능
  2. any 폴백 버그: RC2에서 수정 예정
  3. Native AOT 호환성: 모든 라이브러리가 지원되지 않음

권장사항

플랫폼 특정 도구를 사용해야 하는 경우:

  • 네이티브 종속성이 있는 경우
  • 다운로드 크기가 중요한 경우
  • 최신 .NET SDK 사용 가능한 환경

기존 방식을 유지해야 하는 경우:

  • 광범위한 호환성 필요
  • 구버전 .NET SDK 지원 필요
  • 안정성이 중요한 프로덕션 환경

마이그레이션 전략

<!-- 점진적 전환: 두 방식 모두 지원 -->
<PropertyGroup Condition="'$(UseNewPackaging)' == 'true'">
  <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64;any</RuntimeIdentifiers>
  <PublishSelfContained>true</PublishSelfContained>
  <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

<PropertyGroup Condition="'$(UseNewPackaging)' != 'true'">
  <TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>

CI/CD 자동화 예제

# GitHub Actions 예제
- name: Pack platform-specific tools
  run: |
    dotnet pack --configuration Release --output ./packages/
    for rid in linux-x64 linux-arm64 win-x64 win-arm64; do
      dotnet pack --configuration Release --runtime-id $rid --output ./packages/
    done

시리즈 총정리

주요 테마

.NET 10 Preview 시리즈는 다음과 같은 주요 개선사항들을 다룹니다:

  1. 개발자 경험 향상: 단일 파일 실행, dnx 도구
  2. 언어 기능 확장: C# 14 확장 멤버
  3. 도구 생태계 개선: 소스 생성기 문제 해결
  4. 보안 강화: Passkey 지원
  5. 배포 옵션 확대: 다양한 .NET 도구 패키징 방식

개발자에게 미치는 영향

입문자

  • 더 낮은 진입 장벽: 프로젝트 파일 없이 C# 학습 가능
  • 즉시 실행: 복잡한 설정 없이 코드 실행

라이브러리 개발자

  • 향상된 API 설계: 확장 멤버로 더 직관적인 API
  • 소스 생성기 안정성: [Embedded] 특성으로 타입 충돌 해결

도구 개발자

  • 유연한 배포: Native AOT, 트림 등 다양한 옵션
  • 향상된 사용자 경험: dnx로 설치 없는 도구 사용

웹 개발자

  • 현대적 인증: Passkey 지원으로 보안 강화
  • 개발 편의성: 향상된 템플릿과 도구

미래 전망

.NET 10은 다음과 같은 방향으로 발전하고 있습니다:

  1. 단순성 추구: 복잡한 설정 없이 즉시 사용 가능
  2. 성능 최적화: Native AOT, 트리밍 등으로 더 빠르고 작은 애플리케이션
  3. 현대적 표준 지원: Passkey, WebAuthn 등 최신 웹 표준
  4. 개발자 생산성: 도구 간소화, 자동화 증진
  5. 생태계 통합: 다른 플랫폼의 좋은 아이디어 수용 (npx → dnx)

이러한 변화들은 .NET을 더욱 접근하기 쉽고, 성능이 뛰어나며, 현대적인 개발 플랫폼으로 만들어가고 있습니다.

1개의 좋아요