컴파일러 지시어

컴파일러 지시어 (Compiler Directives)

F# 컴파일러가 소스 코드를 처리하는 방식을 제어하는 지시어들을 살펴볼게요. #if, #else, #endif 같은 조건부 컴파일 지시어부터 경고를 끄고 켜는 #nowarn/#warnon, 디버깅용 줄 정보를 알려주는 #line까지, F# 코드에서 꼭 알아두면 좋은 기능들을 정리해 드릴게요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-directives

본문

이 문서에서는 컴파일러 지시어에 대해 다룹니다. F# Interactive의 지시어(예: dotnet fsi)가 궁금하다면 Interactive Programming with F# 을 참고하세요.

컴파일러 지시어는 # 기호로 시작하며, 한 줄을 단독으로 차지해야 합니다.

다음 표는 F#에서 사용할 수 있는 컴파일러 지시어 목록입니다.

지시어 설명
#if if-expression 조건부 컴파일을 지원합니다. #if 다음의 코드는 해당 if-expression이 defined로 평가될 때만 포함됩니다 (아래 설명 참고).
#else 조건부 컴파일을 지원합니다. 이전 #if에 사용된 심볼이 defined로 평가되지 않을 때 포함할 코드 구간을 표시합니다.
#endif 조건부 컴파일을 지원합니다. 조건부 코드 구간의 끝을 표시합니다.
#[line] int, #[line] int string, #[line] int verbatim-string 디버깅을 위해 원본 소스 코드의 줄 번호와 파일 이름을 알려줍니다. 이 기능은 F# 소스 코드를 생성하는 도구를 위해 제공됩니다.
#nowarn warningcodes warningcodes로 지정된 하나 이상의 컴파일러 경고를 비활성화합니다 (아래 설명 참고).
#warnon warningcodes warningcodes로 지정된 하나 이상의 컴파일러 경고를 활성화합니다 (아래 설명 참고).

조건부 컴파일 지시어

이 지시어들에 의해 비활성화된 코드는 Visual Studio 코드 편집기에서 흐리게(디밍) 표시됩니다.

다음 코드는 #if, #else, #endif 지시어의 사용 예를 보여줍니다. 이 예시에서 function1의 정의가 두 가지 버전으로 존재합니다. -define 컴파일러 옵션으로 VERSION1이 정의되면 #if 지시어와 #else 지시어 사이의 코드가 활성화됩니다. 그렇지 않으면 #else#endif 사이의 코드가 활성화됩니다.

#if VERSION1
let function1 x y =
   printfn "x: %d y: %d" x y
   x + 2 * y
#else
let function1 x y =
   printfn "x: %d y: %d" x y
   x - 2*y
#endif

let result = function1 10 20

#if 지시어는 논리 표현식도 받아들입니다:

#if SILVERLIGHT || COMPILED && (NETCOREFX || !DEBUG)
#endif

다음 표현식들을 사용할 수 있습니다.

if-expr 평가 결과
if-expr1 || if-expr2 if-expr1 또는 if-expr2 중 하나가 defined이면 defined.
if-expr1 && if-expr2 if-expr1if-expr2가 모두 defined이면 defined.
!if-expr1 if-expr1defined가 아니면 defined.
( if-expr1 ) if-expr1defined이면 defined.
symbol -define 컴파일러 옵션으로 정의된 것으로 표시되면 defined.

논리 연산자는 일반적인 논리 연산자 우선순위를 따릅니다.

하지만 F#에는 #define 컴파일러 지시어가 없습니다. #if 지시어가 사용하는 심볼을 정의하려면 컴파일러 옵션이나 프로젝트 설정을 사용해야 합니다.

조건부 컴파일 지시어는 중첩할 수 있습니다. 컴파일러 지시어에서는 들여쓰기가 중요하지 않습니다.

미리 정의된 심볼 (Predefined symbols)

F# 컴파일러와 빌드 시스템은 조건부 컴파일에 사용할 수 있는 여러 심볼을 자동으로 정의합니다.

빌드 구성 심볼 (Build configuration symbols)

다음 심볼들은 빌드 구성에 따라 정의됩니다:

  • DEBUG: Debug 모드로 컴파일할 때 정의됩니다. 프로젝트 시스템에서 DEBUG 심볼은 Debug 구성에서 자동으로 정의되지만, Release 구성에서는 정의되지 않습니다. 이 심볼은 주로 어서션(assertion)이나 진단 코드와 함께 사용됩니다. 자세한 내용은 Assertions 을 참고하세요.

  • TRACE: 추적(tracing)을 활성화한 빌드에 대해 정의됩니다. DEBUG와 마찬가지로 보통 Debug 구성에서 정의되지만, Release 구성에서도 활성화할 수 있습니다.

이 값들은 -define 컴파일러 옵션이나 프로젝트 설정을 통해 재정의할 수 있습니다.

컴파일 모드 심볼 (Compilation mode symbols)

다음 심볼들은 서로 다른 컴파일 모드를 구분합니다:

  • COMPILED: F# 컴파일러로 코드를 컴파일할 때 정의됩니다. 컴파일된 어셈블리와 F# Interactive 세션에서 코드가 다르게 동작하도록 해야 할 때 유용합니다.

  • INTERACTIVE: F# Interactive (dotnet fsi)에서 코드를 컴파일하거나 실행할 때 정의되는데, 여기에는 대화형 세션(interactive session)과 스크립트 실행이 모두 포함됩니다. 이 심볼을 이용하면 대화형으로 실행할 때 다르게 동작하는 코드를 작성할 수 있습니다.

스크립트에서 이 심볼들을 사용하는 방법에 대한 자세한 내용은 Interactive Programming with F# 을 참고하세요.

예시:

#if INTERACTIVE
// Code specific to F# Interactive
#r "nuget: Newtonsoft.Json"
#endif

#if COMPILED
// Code specific to compiled assemblies
open System.Configuration
#endif

대상 프레임워크 심볼 (Target framework symbols)

빌드 시스템은 SDK 스타일 프로젝트에서 다양한 대상 프레임워크에 대한 전처리기 심볼도 정의합니다. 이러한 심볼은 여러 .NET 버전을 대상으로 하는 라이브러리나 애플리케이션을 만들 때 유용합니다.

대상 프레임워크 심볼 추가 심볼 (.NET 5+ SDK 사용 가능) 플랫폼 심볼 (OS 특정 TFM을 지정할 때만 사용 가능)
.NET Framework NETFRAMEWORK, NET481, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20 NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATER, NET47_OR_GREATER, NET462_OR_GREATER, NET461_OR_GREATER, NET46_OR_GREATER, NET452_OR_GREATER, NET451_OR_GREATER, NET45_OR_GREATER, NET40_OR_GREATER, NET35_OR_GREATER, NET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_4_OR_GREATER, NETSTANDARD1_3_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER, NETSTANDARD1_0_OR_GREATER
.NET 5+ (및 .NET Core) NET, NET10_0, NET9_0, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 NET10_0_OR_GREATER, NET9_0_OR_GREATER, NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_1_OR_GREATER, NETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS, [OS][version] (예: IOS15_1), [OS][version]_OR_GREATER (예: IOS15_1_OR_GREATER)

참고:

  • 버전이 없는 심볼은 대상 버전과 무관하게 항상 정의됩니다.

  • 버전이 있는 심볼은 대상으로 하는 버전에 대해서만 정의됩니다.

  • <framework>_OR_GREATER 심볼은 대상 버전과 그보다 낮은 모든 이전 버전에 대해 정의됩니다. 예를 들어 .NET Framework 2.0을 대상으로 한다면 다음 심볼들이 정의됩니다: NET20, NET20_OR_GREATER, NET11_OR_GREATER, NET10_OR_GREATER.

  • NETSTANDARD<x>_<y>_OR_GREATER 심볼은 .NET Standard 대상에 대해서만 정의되며, .NET Core나 .NET Framework 같이 .NET Standard를 구현하는 대상에는 정의되지 않습니다.

  • 이 심볼들은 MSBuild TargetFramework 속성과 NuGet에서 사용하는 대상 프레임워크 모니커(TFM)와는 다릅니다.

예를 들어, 대상 프레임워크에 따라 조건부 컴파일을 하려면 이렇게 사용합니다:

#if NET6_0_OR_GREATER
// Use .NET 6+ specific APIs
#else
// Use alternative implementation for older frameworks
#endif

NULLABLE 지시어

F# 9부터 프로젝트에서 nullable 참조 타입을 활성화할 수 있습니다:

<Nullable>enable</Nullable>

이렇게 하면 빌드에 NULLABLE 지시어가 자동으로 설정됩니다. 이 기능을 도입하면서 충돌이 생기는 코드를 #if NULLABLE 해시 지시어로 조건부로 바꾸고 싶을 때 유용합니다:

#if NULLABLE 
let length (arg: 'T when 'T: not null) =
    Seq.length arg
#else
let length arg =
    match arg with
    | null -> -1
    | s -> Seq.length s
#endif

줄 지시어 (Line Directives)

빌드할 때 컴파일러는 F# 코드의 오류를 각 오류가 발생한 줄 번호를 참조해 보고합니다. 이 줄 번호는 파일의 첫 줄부터 1로 시작합니다. 그런데 다른 도구로 F# 소스 코드를 생성한다면, 생성된 코드의 줄 번호는 대개 관심이 없습니다. 생성된 F# 코드의 오류는 대부분 다른 원인에서 비롯되기 때문이죠. #line 지시어는 F# 소스 코드를 생성하는 도구 작성자가 원본 줄 번호와 소스 파일 정보를 생성된 F# 코드에 전달할 수 있게 해줍니다.

#line 지시어를 사용할 때 파일 이름은 따옴표로 묶어야 합니다. 문자열 앞에 verbatim 토큰(@)이 없다면, 경로에 백슬래시를 사용하려면 백슬래시 하나 대신 두 개로 이스케이프해야 합니다. 다음은 유효한 줄 토큰 예시들입니다. 이 예시들에서는 원본 파일 Script1이 도구를 거쳐 자동 생성된 F# 코드 파일이 되고, 이 지시어들이 있는 위치의 코드가 파일 Script1의 25번째 줄에 있는 어떤 토큰들로부터 생성되었다고 가정합니다.

# 25
#line 25
#line 25 "C:\\Projects\\MyProject\\MyProject\\Script1"
#line 25 @"C:\Projects\MyProject\MyProject\Script1"
# 25 @"C:\Projects\MyProject\MyProject\Script1"

이 토큰들은 이 위치에서 생성된 F# 코드가 Script1의 25번째 줄 부근에 있는 어떤 구성 요소에서 파생되었음을 나타냅니다.

#line 지시어는 #nowarn/#warnon의 동작에는 영향을 주지 않는다는 점을 유의하세요. 이 두 지시어는 항상 컴파일되고 있는 파일을 기준으로 합니다.

경고 지시어 (Warn Directives)

Warn 지시어는 소스 파일의 특정 부분에 대해 지정된 컴파일러 경고를 비활성화하거나 활성화합니다.

warn 지시어는 한 줄의 소스 코드로, 다음과 같은 구성요소로 이루어집니다:

  • 선택적인 앞쪽 공백(whitespace)

  • #nowarn 또는 #warnon 문자열

  • 공백

  • 공백으로 구분된 하나 이상의 warningcode (아래 참고)

  • 선택적인 공백

  • 선택적인 줄 주석(line comment)

warningcode는 숫자 나열(경고 번호를 나타냄)이며, 선택적으로 FS가 앞에 붙거나, 선택적으로 큰따옴표로 둘러쌀 수 있습니다.

#nowarn 지시어는 같은 경고 번호에 대한 #warnon 지시어가 나타나거나 파일 끝에 도달할 때까지 해당 경고를 비활성화합니다. 마찬가지로 #warnon 지시어는 같은 경고 번호에 대한 #nowarn 지시어가 나타나거나 파일 끝에 도달할 때까지 해당 경고를 활성화합니다. 이러한 쌍의 앞과 뒤에서는 컴파일 기본값이 적용되는데, 그 내용은 다음과 같습니다:

  • --nowarn 컴파일러 옵션(또는 해당 MSBuild 속성)으로 비활성화되었으면 경고가 표시되지 않습니다.

  • --warnon 컴파일러 옵션(또는 해당 MSBuild 속성)으로 활성화되지 않았다면 opt-in 경고는 표시되지 않습니다.

다음은 (다소 억지스러운) 예시입니다.

module A
match None with None -> ()     // warning
let x =
    #nowarn 25
    match None with None -> 1  // no warning
    #warnon FS25
match None with None -> ()     // warning
#nowarn "FS25" FS007 "42"
match None with None -> ()     // no warning

더 알아보기 (Learn more)