.NET 세계화 및 ICU

.NET 5 이전에는 .NET 세계화 API가 서로 다른 플랫폼에서 서로 다른 기본 라이브러리를 사용했습니다. Unix에서는 ICU(International Components for Unicode)를 사용했고, Windows에서는 NLS(National Language Support)를 사용했습니다. 이로 인해 서로 다른 플랫폼에서 애플리케이션을 실행할 때 일부 세계화 API의 동작이 약간 달랐습니다. 다음 영역에서는 동작이 확실히 달랐습니다.

  • 문화권 및 문화권 데이터
  • 문자열 대/소문자 구분
  • 문자열 정렬 및 검색
  • 정렬 키
  • 문자열 정규화
  • IDN(다국어 도메인 이름) 지원
  • Linux의 표준 시간대 표시 이름

.NET 5부터는 사용되는 기본 라이브러리에 대한 개발자의 제어가 향상되면서 애플리케이션의 플랫폼 간 차이를 방지할 수 있게 되었습니다.

Windows의 ICU

이제 Windows는 미리 설치된 icu.dll 버전을 세계화 작업에 자동으로 적용되는 기능의 일부로 통합합니다. 이 수정을 통해 .NET은 이 ICU 라이브러리를 세계화 지원에 활용할 수 있습니다. 이전 Windows 버전의 경우와 마찬가지로 ICU 라이브러리를 사용할 수 없거나 로드할 수 없는 경우 .NET 5 및 후속 버전은 NLS 기반 구현을 사용하여 되돌려집니다.

다음 표에서는 여러 Windows 클라이언트 및 서버 버전에서 ICU 라이브러리를 로드할 수 있는 .NET 버전을 보여 줍니다.

.NET 버전 Windows 버전
.NET 5 또는 .NET 6 Windows 클라이언트 10 버전 1903 이상
.NET 5 또는 .NET 6 Windows Server 2022 이상
.NET 7 이상 Windows 클라이언트 10 버전 1703 이상
.NET 7 이상 Windows Server 2019 이상

참고 항목

.NET 7 이상 버전에는 .NET 6 및 .NET 5와 달리 이전 Windows 버전에서 ICU를 로드하는 기능이 있습니다.

참고 항목

ICU를 사용하는 경우에도 CurrentCulture, CurrentUICulture, CurrentRegion 멤버는 여전히 Windows 운영 체제 API를 사용하여 사용자 설정을 적용합니다.

동작의 차이

.NET 5 이상을 대상으로 앱을 업그레이드하는 경우 세계화 기능을 사용하고 있다는 사실을 모르더라도 앱에 변경 내용이 표시될 수 있습니다. 이 섹션에는 표시될 수 있는 동작 변경 중 하나가 나열되어 있지만 다른 변경도 있습니다.

String.IndexOf

문자열에서 null 문자 \0 인덱스를 찾기 위해 String.IndexOf(String)(을)를 호출하는 다음 코드를 고려합니다.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • Windows의 .NET Core 3.1 이전 버전에서 코드 조각은 세 줄 각각에 3(을)를 인쇄합니다.
  • Windows 섹션 테이블의 ICU에 나열된 Windows 버전에서 실행되는 .NET 5 및 후속 버전의 경우 코드 조각은 서수 검색을 위해 0, 03(을)를 인쇄합니다.

기본적으로 String.IndexOf(String)(이)가 문화권 인식 언어 검색을 수행합니다. ICU는 null 문자 \0(을)를 0 가중치 문자로 간주하므로 .NET 5 이상에서 언어 검색을 사용하는 경우 문자열에서 문자를 찾을 수 없습니다. 그러나 NLS는 null 문자 \0(을)를 0 가중치 문자로 간주하지 않으며 .NET Core 3.1 이하에서 언어 검색을 통해 위치 3에서 문자를 찾습니다. 서수 검색은 모든 .NET 버전에서 위치 3의 문자를 찾습니다.

CA1307:명확성을 위한 StringComparison을 지정하고CA1309: 서수 StringComparison을 사용하여 코드 분석 규칙을 실행하여 문자열 비교가 지정되지 않았거나 서수가 아닌 코드에서 호출 사이트를 찾습니다.

자세한 내용은 .NET 5+에서 문자열 비교 시 동작 변경을 참조하세요.

String.EndsWith

const string foo = "abc";

Console.WriteLine(foo.EndsWith("\0"));
Console.WriteLine(foo.EndsWith("c"));
Console.WriteLine(foo.EndsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.EndsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.EndsWith('\0'));

Important

Windows 테이블의 ICU에 나열된 Windows 버전에서 실행되는 .NET 5 이상에서는 위의 코드 조각이 인쇄됩니다.

True
True
True
False
False

이 동작을 방지하려면 char 매개 변수 오버로드 또는 StringComparison.Oridinal(을)를 사용합니다.

String.StartsWith

const string foo = "abc";

Console.WriteLine(foo.StartsWith("\0"));
Console.WriteLine(foo.StartsWith("a"));
Console.WriteLine(foo.StartsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.StartsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.StartsWith('\0'));

Important

Windows 테이블의 ICU에 나열된 Windows 버전에서 실행되는 .NET 5 이상에서는 위의 코드 조각이 인쇄됩니다.

True
True
True
False
False

이 동작을 방지하려면 char 매개 변수 오버로드 또는 StringComparison.Oridinal(을)를 사용합니다.

TimeZoneInfo.FindSystemTimeZoneById

ICU는 애플리케이션이 Windows에서 실행되는 경우에도 IANA표준 시간대 ID를 사용하여TimeZoneInfo 인스턴스를 유연하게 만들 수 있습니다. 마찬가지로 Windows 이외의 플랫폼에서 실행되는 경우에도 Windows 표준 시간대 ID를 사용하여 TimeZoneInfo 인스턴스를 만들 수 있습니다. 그러나 NLS 모드를 사용하거나 세계화 고정 모드를 사용하는 경우 이 기능을 사용할 수 없다는 점에 유의해야 합니다.

ICU 종속 API

.NET은 ICU에 종속된 API를 도입했습니다. 이러한 API는 ICU를 사용하는 경우에만 성공할 수 있습니다. 다음 몇 가지 예를 참조하세요.

Windows 섹션 테이블의 ICU에 나열된 Windows 버전에서 언급된 API는 지속적으로 성공합니다. 그러나 이전 버전의 Windows에서는 이러한 API가 지속적으로 실패합니다. 이러한 경우 앱 로컬 ICU 기능을 사용하도록 설정하여 이러한 API의 성공을 보장할 수 있습니다. Windows가 아닌 플랫폼에서 이러한 API는 버전에 관계없이 항상 성공합니다.

또한 앱이 세계화 고정 모드 또는 NLS 모드로 실행되지 않도록 하여 이러한 API의 성공을 보장하는 것이 중요합니다.

ICU 대신 NLS 사용

NLS 대신 ICU를 사용하면 일부 세계화 관련 작업에서 동작 차이가 발생할 수 있습니다. 개발자는 NLS 사용으로 되돌리기 위해 ICU 구현을 옵트아웃할 수 있습니다. 애플리케이션은 다음 방법으로 NLS 모드를 사용하도록 설정할 수 있습니다.

  • 프로젝트 파일에서:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • runtimeconfig.json 파일에서:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • 환경 변수 DOTNET_SYSTEM_GLOBALIZATION_USENLStrue 또는 1으로 설정.

참고 항목

프로젝트 또는 runtimeconfig.json 파일에 설정된 값은 환경 변수보다 우선적으로 적용됩니다.

자세한 내용은 런타임 구성 설정을 참조하세요.

앱에서 ICU를 사용하고 있는지 확인

다음 코드 조각은 앱이 NLS가 아닌 ICU 라이브러리를 사용하여 실행 중인지 확인하는 데 도움이 될 수 있습니다.

public static bool ICUMode()
{
    SortVersion sortVersion = CultureInfo.InvariantCulture.CompareInfo.Version;
    byte[] bytes = sortVersion.SortId.ToByteArray();
    int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
    return version != 0 && version == sortVersion.FullVersion;
}

.NET 버전을 확인하려면 RuntimeInformation.FrameworkDescription(을)를 사용합니다.

앱 로컬 ICU

각 ICU 릴리스는 버그 수정뿐 아니라 세계 언어를 설명하는 업데이트된 CLDR(Common Locale Data Repository) 데이터를 제공할 수 있습니다. ICU 버전 간 이동은 세계화 관련 작업과 관련하여 앱 동작에 미묘한 영향을 줄 수 있습니다. 애플리케이션 개발자가 모든 배포에서 일관성을 유지할 수 있도록 .NET 5 이상 버전에서는 Windows 및 Unix의 앱이 자체 ICU 복사본을 갖고 사용할 수 있습니다.

애플리케이션은 다음 방법 중 하나로 앱 로컬 ICU 구현 모드에 옵트인할 수 있습니다.

  • 프로젝트 파일에서 적절한 RuntimeHostConfigurationOption 값을 설정합니다.

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • 또는 runtimeconfig.json 파일에서 적절한 runtimeOptions.configProperties 값을 설정합니다.

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • 또는 환경 변수 DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU(을)를 <suffix>:<version> 또는 <version>값으로 설정합니다.

    <suffix>: 공용 ICU 패키징 규칙에 따라 길이가 36자 미만인 선택적 접미사입니다. 사용자 지정 ICU를 빌드할 때 lib 이름을 생성하고 내보낸 기호 이름이 접미사를 포함하도록(예: libicuucmyapp, 여기서 myapp은 접미사) 사용자 지정할 수 있습니다.

    <version>: 유효한 ICU 버전(예: 67.1)입니다. 이 버전은 이진 파일을 로드하고 내보낸 기호를 가져오는 데 사용됩니다.

이러한 옵션 중 하나가 설정되면 구성된 version에 해당하는 프로젝트에 Microsoft.ICU.ICU4C.RuntimePackageReference을 추가할 수 있습니다.

또는 앱-로컬 스위치가 설정되면 ICU를 로드하기 위해 .NET은 여러 경로를 검색하는 NativeLibrary.TryLoad 메서드를 사용합니다. 메서드는 먼저 NATIVE_DLL_SEARCH_DIRECTORIES 속성에서 라이브러리를 찾으려고 시도합니다. 이 라이브러리는 앱의 deps.json 파일에 기반한 dotnet 호스트에서 만들어집니다. 자세한 내용은 기본 검색을 참조하세요.

자체 포함 앱의 경우 ICU가 앱 디렉터리에 있는지 확인하는 것 외에 특별한 작업이 필요하지 않습니다(자체 포함 앱의 경우 작업 디렉터리의 기본값은 NATIVE_DLL_SEARCH_DIRECTORIES).

NuGet 패키지를 통해 사용하는 ICU는 프레임워크 종속 애플리케이션에서 작동합니다. NuGet은 네이티브 자산을 확인하여 deps.json 파일 및 runtimes 디렉터리 아래에 있는 애플리케이션의 출력 디렉터리에 포함합니다. .NET은 여기에서 로드합니다.

ICU가 로컬 빌드에서 사용되는 프레임워크 종속 앱(자체 포함 아님)의 경우 추가 단계를 수행해야 합니다. .NET SDK에는 "느슨한" 네이티브 이진 파일을 deps.json에 통합하는 기능이 아직 없습니다(이 SDK 문제 참조). 대신 애플리케이션의 프로젝트 파일에 추가 정보를 추가하여 이 기능을 사용하도록 설정할 수 있습니다. 예시:

<ItemGroup>
  <IcuAssemblies Include="icu\*.so*" />
  <RuntimeTargetsCopyLocalItems Include="@(IcuAssemblies)" AssetType="native" CopyLocal="true"
    DestinationSubDirectory="runtimes/linux-x64/native/" DestinationSubPath="%(FileName)%(Extension)"
    RuntimeIdentifier="linux-x64" NuGetPackageId="System.Private.Runtime.UnicodeData" />
</ItemGroup>

지원되는 런타임의 모든 ICU 이진 파일에 이 작업을 수행해야 합니다. 또한 RuntimeTargetsCopyLocalItems 항목 그룹의 NuGetPackageId 메타데이터는 프로젝트가 실제로 참조하는 NuGet 패키지와 일치해야 합니다.

macOS 동작

macOS는 Mach-O 파일에 지정된 부하 명령에서 종속 동적 라이브러리를 확인하는 동작이 Linux 로더와 다릅니다. Linux 로더에서 .NET은 ICU 종속성 그래프를 충족하기 위해 libicudata, libicuuc, libicui18n을 (이 순서로) 시도할 수 있습니다. 하지만 macOS에서는 이것이 작동하지 않습니다. macOS에서 ICU를 빌드하는 경우 기본적으로 libicuuc에서 이러한 load 명령을 사용하여 동적 라이브러리를 가져옵니다. 다음 코드 조각은 예제를 보여 줍니다.

~/ % otool -L /Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib
/Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib:
 libicuuc.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 libicudata.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
 /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0)

이러한 명령은 ICU의 다른 구성 요소에 대한 종속 라이브러리의 이름을 참조합니다. 로더는 dlopen 규칙에 따라 검색을 수행합니다. 그러려면 시스템 디렉터리에 이러한 라이브러리가 있거나 LD_LIBRARY_PATH 환경 변수를 설정하거나 앱 수준 디렉터리에 ICU가 있어야 합니다. LD_LIBRARY_PATH를 설정할 수 없거나 ICU 바이너리가 앱 수준 디렉터리에 있는지 확인할 수 없다면 몇 가지 추가 작업을 수행해야 합니다.

@loader_path와 같이 해당 load 명령을 사용하여 이진 파일과 동일한 디렉터리에서 해당 종속성을 검색하도록 로더에게 지시하는 로더를 위한 몇 가지 지시문이 있습니다. 두 가지 방법으로 이 작업을 수행할 수 있습니다.

  • install_name_tool -change

    다음 명령을 실행합니다.

    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicuuc.67.1.dylib
    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicui18n.67.1.dylib
    install_name_tool -change "libicuuc.67.dylib" "@loader_path/libicuuc.67.dylib" /path/to/libicui18n.67.1.dylib
    
  • @loader_path로 설치 이름을 생성하도록 ICU 패치

    autoconf(./runConfigureICU)를 실행하기 전에 이 줄을 다음으로 변경합니다.

    LD_SONAME = -Wl,-compatibility_version -Wl,$(SO_TARGET_VERSION_MAJOR) -Wl,-current_version -Wl,$(SO_TARGET_VERSION) -install_name @loader_path/$(notdir $(MIDDLE_SO_TARGET))
    

WebAssembly의 ICU

WebAssembly 워크로드 전용인 ICU 버전을 사용할 수 있습니다. 이 버전은 데스크톱 프로필과의 세계화 호환성을 제공합니다. ICU 데이터 파일 크기를 24MB에서 1.4MB(또는 Brotli로 압축된 경우 0.3MB 이하)로 줄이기 위해 이 워크로드에는 몇 가지 제한 사항이 있습니다.

다음 API는 지원되지 않습니다.

다음 API는 제한적으로 지원됩니다.

또한 지원되는 로캘은 더 적습니다. 지원되는 목록은 dotnet/icu 리포지토리에서 찾을 수 있습니다.