개발자 도구
Angular CLI

사전(AOT) 컴파일

Angular 애플리케이션은 주로 컴포넌트와 해당 컴포넌트의 HTML 템플릿으로 구성됩니다. 브라우저는 Angular가 제공하는 컴포넌트와 템플릿을 직접 이해할 수 없기 때문에 Angular 애플리케이션이 브라우저에서 실행되려면 컴파일 과정이 필요합니다.

Angular의 사전 컴파일(Ahead-of-time, AOT) 컴파일러는 브라우저가 코드를 다운로드하고 실행하기 전에 빌드 단계에서 Angular HTML과 TypeScript 코드를 효율적인 JavaScript 코드로 변환합니다. 빌드 과정에서 애플리케이션을 컴파일하면 브라우저에서 더 빠르게 렌더링할 수 있습니다.

이 가이드에서는 AOT 컴파일러로 애플리케이션을 효율적으로 컴파일하기 위해 메타데이터를 지정하고 컴파일러 옵션을 적용하는 방법을 알아봅니다.

도움말: AngularConnect 2019에서 Alex Rickabaugh가 Angular 컴파일러를 설명하는 영상을 확인해 보세요.

AOT를 사용하는 이유는 다음과 같습니다.

이유 설명
빠른 렌더링 AOT를 사용하면 브라우저가 미리 컴파일된 애플리케이션을 다운로드합니다. 실행 가능한 코드를 불러오므로 먼저 애플리케이션을 컴파일할 필요 없이 바로 렌더링할 수 있습니다.
비동기 요청 감소 컴파일러는 외부 HTML 템플릿과 CSS 스타일시트를 애플리케이션 JavaScript에 인라인으로 포함 하므로 해당 소스 파일을 위한 별도 Ajax 요청이 필요하지 않습니다.
Angular 프레임워크 다운로드 크기 감소 애플리케이션이 이미 컴파일되어 있다면 Angular 컴파일러를 다운로드할 필요가 없습니다. 컴파일러는 Angular 자체 크기의 약 절반을 차지하므로 이를 제외하면 애플리케이션 전송 크기가 크게 줄어듭니다.
템플릿 오류 조기 발견 AOT 컴파일러는 사용자가 오류를 만나기 전에 빌드 단계에서 템플릿 바인딩 오류를 감지하고 보고합니다.
보안 향상 AOT는 클라이언트에 제공하기 훨씬 전에 HTML 템플릿과 컴포넌트를 JavaScript 파일로 컴파일합니다. 읽어야 할 템플릿이 없고 클라이언트에서 위험한 HTML이나 JavaScript 평가를 수행하지 않으므로 주입 공격의 가능성이 줄어듭니다.

컴파일러 선택하기

Angular는 두 가지 애플리케이션 컴파일 방식을 제공합니다:

Angular 컴파일 방식 설명
적시 컴파일(JIT) 실행 시점에 브라우저에서 애플리케이션을 컴파일합니다. Angular 8까지 기본 방식이었습니다.
사전 컴파일(AOT) 빌드 시점에 애플리케이션과 라이브러리를 컴파일합니다. Angular 9부터 기본 방식입니다.

CLI의 ng build(빌드만 수행) 또는 ng serve(빌드 후 로컬에서 제공) 명령어를 실행할 때 컴파일 방식(JIT 또는 AOT)은 angular.json의 빌드 설정에 지정한 aot 프로퍼티 값에 따라 결정됩니다. 새 CLI 애플리케이션에서는 기본적으로 aot가 true로 설정됩니다.

자세한 내용은 CLI 명령어 레퍼런스와 Angular 앱 빌드하고 실행하기를 참고하세요.

AOT 동작 방식

Angular AOT 컴파일러는 Angular가 관리해야 하는 애플리케이션의 부분을 해석하기 위해 메타데이터를 추출합니다. 메타데이터는 @Component()와 같은 데코레이터에 명시적으로 지정하거나 데코레이터가 적용된 클래스의 생성자 선언에 암묵적으로 지정할 수 있습니다. 메타데이터는 애플리케이션 클래스의 인스턴스를 생성하고 실행 시점에 상호작용하는 방법을 Angular에 알려줍니다.

다음 예제에서 @Component() 메타데이터 객체와 클래스 생성자는 Angular에 Typical 인스턴스를 만들고 표시하는 방법을 알려줍니다.

@Component({
  selector: 'app-typical',
  template: '<div>A typical component for {{data().name}}</div>',
})
export class Typical {
  data = input.required<TypicalData>();
  private someService = inject(SomeService);
}

Angular 컴파일러는 메타데이터를 한 번 추출하고 Typical의 팩토리 를 생성합니다. Typical 인스턴스가 필요하면 Angular는 팩토리를 호출합니다. 팩토리는 의존성이 주입된 새 컴포넌트 클래스 인스턴스와 바인딩된 새 화면 엘리먼트를 생성합니다.

컴파일 단계

AOT 컴파일은 세 단계로 진행됩니다.

순서 단계 설명
1 코드 분석 TypeScript 컴파일러와 AOT 수집기 가 소스를 표현하는 데이터를 생성합니다. 수집기는 수집한 메타데이터를 해석하지 않습니다. 가능한 범위에서 메타데이터를 표현하고 메타데이터 문법 위반을 발견하면 오류를 기록합니다.
2 코드 생성 컴파일러의 StaticReflector가 1단계에서 수집한 메타데이터를 해석하고 추가로 검증하며, 메타데이터 제약 위반을 발견하면 오류를 발생시킵니다.
3 템플릿 타입 검사 이 단계에서 Angular 템플릿 컴파일러 가 TypeScript 컴파일러를 사용해서 템플릿의 바인딩 표현식을 검증합니다. 엄격한 검사는 기본적으로 활성화되며 strictTemplates 설정 옵션으로 제어합니다. Angular 컴파일러 옵션을 참고하세요.

메타데이터 제약

메타데이터는 TypeScript의 일부 문법 으로 작성하며 다음 일반 제약을 따라야 합니다:

도움말: AOT 컴파일 오류는 아래에서 자세히 설명하는 컴파일러 요구사항을 충족하지 못하는 메타데이터 때문에 발생하는 경우가 많습니다. 이러한 문제를 이해하고 해결하는 방법은 AOT 메타데이터 오류를 참고하세요.

AOT 컴파일 설정하기

TypeScript 설정 파일에 옵션을 지정해서 컴파일 과정을 제어할 수 있습니다. 사용할 수 있는 모든 옵션은 Angular 컴파일러 옵션을 참고하세요.

1단계: 코드 분석

첫 번째 단계의 분석 작업 일부는 TypeScript 컴파일러가 수행합니다. AOT 컴파일러가 애플리케이션 코드를 생성하는 데 필요한 타입 정보를 담은 .d.ts 타입 정의 파일 을 출력합니다. 동시에 AOT 수집기는 Angular 데코레이터에 기록된 메타데이터를 분석하고 각 .d.ts 파일에 대응하는 .metadata.json 파일에 메타데이터 정보를 출력합니다.

.metadata.json은 데코레이터 메타데이터의 전체 구조를 추상 구문 트리(AST)로 표현한 도식이라고 생각하면 됩니다.

도움말: Angular의 schema.ts는 TypeScript 인터페이스 집합으로 JSON 형식을 설명합니다.

표현식 문법의 제한

AOT 수집기는 JavaScript 문법의 일부만 이해합니다. 메타데이터 객체는 다음과 같이 제한된 문법으로 정의하세요:

문법 예제
객체 리터럴 {cherry: true, apple: true, mincemeat: false}
배열 리터럴 ['cherries', 'flour', 'sugar']
배열 리터럴의 전개 구문 ['apples', 'flour', …]
호출 bake(ingredients)
new new Oven()
프로퍼티 접근 pie.slice
배열 인덱스 ingredients[0]
식별자 참조 Component
템플릿 문자열 pie is ${multiplier} times better than cake
문자열 리터럴 'pi'
숫자 리터럴 3.14153265
불리언 리터럴 true
null 리터럴 null
지원되는 전위 연산자 !cake
지원되는 이항 연산자 a+b
조건 연산자 a ? b : c
괄호 (a+b)

표현식에 지원하지 않는 문법을 사용하면 수집기가 .metadata.json 파일에 오류 노드를 기록합니다. 나중에 애플리케이션 코드를 생성할 때 해당 메타데이터가 필요하면 컴파일러가 오류를 보고합니다.

도움말: 오류가 포함된 .metadata.json 파일을 생성하는 대신 ngc가 즉시 문법 오류를 보고하게 하려면 TypeScript 설정 파일에서 strictMetadataEmit 옵션을 설정하세요.

"angularCompilerOptions": {
  …
  "strictMetadataEmit" : true
}

Angular 라이브러리는 모든 Angular .metadata.json 파일에 오류가 없도록 이 옵션을 사용합니다. 직접 라이브러리를 빌드할 때도 같은 설정을 사용하는 것이 좋습니다.

코드 접기

컴파일러는 내보낸 심볼의 참조만 해석할 수 있습니다. 하지만 수집기는 수집 과정에서 표현식을 평가하고 원래 표현식 대신 결과를 .metadata.json에 기록할 수 있습니다. 따라서 표현식 안에서 내보내지 않은 심볼도 제한적으로 사용할 수 있습니다.

예를 들어 수집기는 1 + 2 + 3 + 4 표현식을 평가해서 결과인 10으로 대체할 수 있습니다. 이 과정을 접기(folding) 라고 합니다. 이렇게 줄일 수 있는 표현식은 접을 수 있는 표현식 입니다.

수집기는 모듈 내부의 const 선언과 초기화된 var, let 선언에 대한 참조를 평가해서 .metadata.json 파일에서 사실상 제거할 수 있습니다.

다음 컴포넌트 정의를 살펴봅시다:

const template = '<div>{{hero().name}}</div>';

@Component({
  selector: 'app-hero',
  template: template,
})
export class Hero {
  hero = input.required<Hero>();
}

template 상수는 내보내지 않았으므로 컴파일러가 참조할 수 없습니다. 하지만 수집기는 template 상수의 내용을 인라인으로 넣어서 메타데이터 정의에 접어 넣을 수 있습니다. 이는 다음과 같이 작성한 것과 같습니다:

@Component({
  selector: 'app-hero',
  template: '<div>{{hero().name}}</div>',
})
export class Hero {
  hero = input.required<Hero>();
}

이제 template 참조가 없으므로 컴파일러가 나중에 .metadata.json에 기록된 수집기의 출력을 해석할 때 문제가 발생하지 않습니다.

이 예제에서 더 나아가 template 상수를 다른 표현식에 포함할 수도 있습니다:

const template = '<div>{{hero().name}}</div>';

@Component({
  selector: 'app-hero',
  template: template + '<div>{{hero().title}}</div>',
})
export class Hero {
  hero = input.required<Hero>();
}

수집기는 이 표현식을 동일한 의미의 접힌 문자열로 줄입니다:

'<div>{{hero().name}}</div><div>{{hero().title}}</div>';

접을 수 있는 문법

수집기가 접을 수 있는 표현식과 접을 수 없는 표현식은 다음과 같습니다:

문법 접기 가능 여부
객체 리터럴 가능
배열 리터럴 가능
배열 리터럴의 전개 구문 불가능
호출 불가능
new 불가능
프로퍼티 접근 대상이 접을 수 있는 표현식이면 가능
배열 인덱스 대상과 인덱스 모두 접을 수 있는 표현식이면 가능
식별자 참조 로컬 참조이면 가능
치환이 없는 템플릿 가능
치환이 있는 템플릿 치환 부분을 접을 수 있으면 가능
문자열 리터럴 가능
숫자 리터럴 가능
불리언 리터럴 가능
null 리터럴 가능
지원되는 전위 연산자 피연산자를 접을 수 있으면 가능
지원되는 이항 연산자 왼쪽과 오른쪽 모두 접을 수 있으면 가능
조건 연산자 조건을 접을 수 있으면 가능
괄호 표현식을 접을 수 있으면 가능

표현식을 접을 수 없으면 수집기는 컴파일러가 해석할 수 있도록 .metadata.json에 AST로 기록합니다.

2단계: 코드 생성

수집기는 수집해서 .metadata.json에 출력하는 메타데이터를 이해하려고 하지 않습니다. 가능한 범위에서 메타데이터를 표현하고 문법 위반을 발견하면 오류를 기록합니다. 코드 생성 단계에서 .metadata.json을 해석하는 것은 컴파일러의 역할입니다.

컴파일러는 수집기가 지원하는 모든 문법 형식을 이해합니다. 하지만 문법적으로 올바른 메타데이터라도 의미 가 컴파일러 규칙에 어긋나면 거부할 수 있습니다.

public 또는 protected 심볼

컴파일러는 내보낸 심볼 만 참조할 수 있습니다.

  • 데코레이터가 적용된 컴포넌트 클래스 멤버는 public 또는 protected여야 합니다. input() 프로퍼티를 private으로 지정할 수 없습니다.

  • 데이터 바인딩에 사용하는 프로퍼티도 public 또는 protected여야 합니다.

지원하는 클래스와 함수

수집기는 문법이 올바르다면 함수 호출이나 new를 사용한 객체 생성을 표현할 수 있습니다. 하지만 컴파일러는 나중에 특정 함수 호출이나 특정 객체 생성을 위한 코드 생성을 거부할 수 있습니다.

컴파일러는 특정 클래스의 인스턴스만 생성할 수 있고, 핵심 데코레이터만 지원하며, 표현식을 반환하는 매크로(함수 또는 정적 메서드)의 호출만 지원합니다.

컴파일러 동작 설명
새 인스턴스 컴파일러는 @angular/core의 InjectionToken 클래스 인스턴스를 생성하는 메타데이터만 허용합니다.
지원하는 데코레이터 컴파일러는 @angular/core 모듈의 Angular 데코레이터에 대한 메타데이터만 지원합니다.

함수와 정적 메서드 호출

수집기는 하나의 return 구문을 포함하는 모든 함수나 정적 메서드를 받아들입니다. 하지만 컴파일러는 표현식 을 반환하는 함수나 정적 메서드 형태의 매크로만 지원합니다.

예를 들어 다음 함수를 살펴봅시다:

export function wrapInArray<T>(value: T): T[] {
  return [value];
}

wrapInArray는 컴파일러가 제한적으로 지원하는 JavaScript 문법을 따르는 표현식의 값을 반환하므로 메타데이터 정의에서 호출할 수 있습니다.

wrapInArray()는 다음과 같이 사용할 수 있습니다:

@NgModule({
  declarations: wrapInArray(Typical),
})
export class TypicalModule {}

컴파일러는 이를 다음과 같이 작성한 것으로 처리합니다:

@NgModule({
  declarations: [Typical],
})
export class TypicalModule {}

Angular의 RouterModule은 최상위 및 하위 라우팅 규칙을 쉽게 선언하도록 forRoot와 forChild라는 두 매크로 정적 메서드를 내보냅니다. 이 메서드의 소스 코드를 살펴보고 매크로가 복잡한 NgModule 설정을 어떻게 단순하게 만드는지 확인해 보세요.

3단계: 템플릿 타입 검사

Angular 컴파일러의 가장 유용한 기능 중 하나는 템플릿 안의 표현식에 대해 타입을 검사하고 실행 중 오류가 발생하기 전에 문제를 발견하는 기능입니다. 템플릿 타입 검사 단계에서 Angular 템플릿 컴파일러는 TypeScript 컴파일러로 템플릿의 바인딩 표현식을 검증합니다.

이 단계의 엄격한 검사는 기본적으로 활성화되며 프로젝트의 TypeScript 설정 파일에서 "angularCompilerOptions"의 "strictTemplates" 컴파일러 옵션으로 제어합니다(Angular 컴파일러 옵션 참고).

템플릿 검증은 바인딩 표현식에서 타입 오류를 발견하면 오류 메시지를 생성합니다. TypeScript 컴파일러가 .ts 파일의 코드에서 타입 오류를 보고하는 것과 비슷합니다.

예를 들어 다음 컴포넌트를 살펴봅시다:

@Component({
  selector: 'my-component',
  template: '{{person.addresss.street}}',
})
class MyComponent {
  person?: Person;
}

다음 오류가 발생합니다:

my.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?

오류 메시지에 표시된 my.component.ts.MyComponent.html 파일은 템플릿 컴파일러가 생성한 가상 파일이며 MyComponent 클래스의 템플릿 내용이 들어 있습니다. 컴파일러는 이 파일을 디스크에 쓰지 않습니다. 줄과 열 번호는 해당 클래스(이 예제에서는 MyComponent)의 @Component 어노테이션에 있는 템플릿 문자열을 기준으로 합니다. 컴포넌트가 template 대신 templateUrl을 사용하면 가상 파일이 아니라 templateUrl이 참조하는 HTML 파일을 기준으로 오류를 보고합니다.

오류 위치는 문제가 있는 문자열 바인딩 표현식을 포함한 텍스트 노드의 시작 지점입니다. [value]="person.address.street"와 같은 어트리뷰트 바인딩에 오류가 있으면 해당 어트리뷰트의 위치를 보고합니다.

검증에는 TypeScript 타입 검사기를 사용하며 TypeScript 컴파일러에 전달한 옵션으로 타입 검증의 엄격함을 제어합니다. 예를 들어 TypeScript의 strictNullChecks 옵션을 활성화하면 앞의 오류 메시지와 함께 다음 오류도 보고합니다.

my.component.ts.MyComponent.html(1,1): : Object is possibly 'undefined'

타입 좁히기

Angular 템플릿 컴파일러는 TypeScript의 if 표현식과 같은 방식으로 ngIf 디렉티브의 표현식을 사용해서 유니온 타입을 좁힙니다. 예를 들어 앞의 템플릿에서 Object is possibly 'undefined' 오류를 피하려면 다음과 같이 person 값이 초기화되었을 때만 문자열 바인딩 표현식을 출력하도록 수정하세요:

@Component({
  selector: 'my-component',
  template: '<span *ngIf="person"> {{person.address.street}} </span>',
})
class MyComponent {
  person?: Person;
}

*ngIf를 사용하면 TypeScript 컴파일러는 바인딩 표현식의 person이 절대 undefined가 아니라고 추론할 수 있습니다.

입력 타입 좁히기에 대한 자세한 내용은 커스텀 디렉티브의 템플릿 타입 검사 개선하기를 참고하세요.

null이 아님을 명시하는 연산자

!는 값이 null이나 undefined가 아님을 타입 검사기에 명시하는 연산자입니다. 실행 중에 값을 검사하거나 null과 undefined가 아님을 보장하지는 않습니다.

*ngIf를 사용하기 불편하거나 컴포넌트의 어떤 제약 때문에 문자열 바인딩 표현식을 평가할 때 항상 null이 아님이 보장된다면, null이 아님을 명시하는 연산자로 Object is possibly 'undefined' 오류를 억제할 수 있습니다.

다음 예제에서 person과 address 프로퍼티는 항상 함께 설정되므로 person이 null이 아니면 address도 항상 null이 아닙니다. 이 제약을 TypeScript와 템플릿 컴파일러에 쉽게 설명할 방법은 없지만 address!.street를 사용해서 오류를 억제합니다.

@Component({
  selector: 'my-component',
  template: '<span *ngIf="person"> {{person.name}} lives on {{address!.street}} </span>',
})
class MyComponent {
  person?: Person;
  address?: Address;

  setData(person: Person, address: Address) {
    this.person = person;
    this.address = address;
  }
}

컴포넌트를 리팩토링하면 이 제약이 깨질 수 있으므로 null이 아님을 명시하는 연산자는 꼭 필요한 경우에만 사용해야 합니다.

이 예제에서는 다음과 같이 *ngIf에 address 검사도 포함하는 것을 권장합니다:

@Component({
  selector: 'my-component',
  template: '<span *ngIf="person && address"> {{person.name}} lives on {{address.street}} </span>',
})
class MyComponent {
  person?: Person;
  address?: Address;

  setData(person: Person, address: Address) {
    this.person = person;
    this.address = address;
  }
}