개발자 도구
Angular CLI

템플릿 타입 검사

템플릿 타입 검사 개요

TypeScript가 코드의 타입 오류를 찾아내듯이 Angular도 애플리케이션 템플릿의 표현식과 바인딩을 검사하고 발견한 타입 오류를 보고할 수 있습니다. Angular는 기본적으로 엄격 모드로 템플릿을 검사합니다. 필요하다면 Angular 컴파일러 옵션의 strictTemplates 플래그를 false로 설정해서 엄격 모드를 일시적으로 비활성화하고 검사 범위가 더 제한적인 기본 모드로 전환할 수 있습니다.

기본 모드

기본 타입 검사 모드에서는 strictTemplates 플래그가 false이며, Angular는 템플릿의 최상위 표현식만 검증합니다.

<map [city]="user.address.city">라고 작성하면 컴파일러는 다음을 확인합니다:

  • user가 컴포넌트 클래스의 프로퍼티인지 확인합니다.
  • user가 address 프로퍼티를 가진 객체인지 확인합니다.
  • user.address가 city 프로퍼티를 가진 객체인지 확인합니다.

컴파일러는 user.address.city 값을 <map> 컴포넌트의 city 입력에 할당할 수 있는지는 확인하지 않습니다.

이 모드에는 다음과 같은 주요 제약도 있습니다:

  • @if, @for, *ngIf, *ngFor 및 다른 <ng-template> 내장 뷰를 검사하지 않습니다.
  • #refs의 타입, 파이프 결과의 타입, 이벤트 바인딩에서 $event의 타입을 알아내지 않습니다.

이러한 항목은 대부분 any 타입이 되어 표현식의 뒷부분이 검사되지 않을 수 있습니다.

엄격 모드

엄격 모드는 기본으로 활성화되며 strictTemplates 플래그가 true이거나 설정되지 않았을 때 사용됩니다.

엄격 모드에서 Angular는 다음을 수행합니다:

  • @if, @for, *ngIf, *ngFor 내부와 같은 내장 뷰를 검사합니다.
  • 파이프의 올바른 반환 타입을 추론합니다.
  • 디렉티브와 파이프의 지역 참조에 대해 올바른 타입을 추론합니다.
  • 컴포넌트/디렉티브 바인딩을 해당 input()에 할당할 수 있는지 확인합니다.
  • 입력 바인딩을 검증할 때 TypeScript의 strictNullChecks 플래그를 따릅니다.
  • 제네릭을 포함한 컴포넌트/디렉티브의 올바른 타입을 추론합니다.
  • 설정된 경우 템플릿 컨텍스트 타입을 추론합니다. 예를 들어 NgFor의 타입을 올바르게 검사할 수 있습니다.
  • 컴포넌트/디렉티브, DOM, 애니메이션 이벤트 바인딩에서 $event의 올바른 타입을 추론합니다.
  • 태그 이름을 기준으로 DOM 엘리먼트 지역 참조의 올바른 타입을 추론합니다. 예를 들어 해당 태그에 대해 document.createElement가 반환하는 타입을 사용합니다.

*ngFor 검사하기

두 가지 타입 검사 모드는 내장 뷰를 서로 다르게 처리합니다. 다음 예제를 살펴봅시다.

User interface

interface User {
  name: string;
  address: {
    city: string;
    state: string;
  };
}
<div *ngFor="let user of users">
  <h2>{{config.title}}</h2>
  <span>City: {{user.address.city}}</span>
</div>

<h2>와 <span>은 *ngFor 내장 뷰 안에 있습니다. 기본 모드에서는 Angular가 둘 다 검사하지 않습니다. 엄격 모드에서는 <span>의 user가 User 타입이고, address는 string 타입의 city 프로퍼티를 가진 객체라는 것을 알고 있습니다.

템플릿 오류 해결하기

엄격 모드에서는 기본 모드에서 나타나지 않던 템플릿 오류가 발생할 수 있습니다. 이러한 오류는 이전 도구가 발견하지 못한 실제 템플릿 타입 불일치를 나타내는 경우가 많습니다. 이 경우 오류 메시지에서 템플릿의 어느 부분에 문제가 있는지 명확하게 확인할 수 있습니다.

하지만 Angular 라이브러리의 타입 정의가 불완전하거나 잘못되었거나 다음과 같이 예상과 완전히 일치하지 않으면 잘못된 오류가 보고될 수도 있습니다.

  • 라이브러리의 타입 정의가 잘못되었거나 불완전한 경우입니다. 예를 들어 strictNullChecks를 고려하지 않고 작성해서 null | undefined가 빠져 있을 수 있습니다.

  • 라이브러리의 입력 타입 범위가 너무 좁고 Angular가 이를 파악할 수 있는 적절한 메타데이터가 없는 경우입니다. 주로 <input disabled>처럼 disabled 등의 일반적인 불리언 입력을 어트리뷰트로 사용할 때 발생합니다.

  • DOM 이벤트에서 $event.target을 사용하는 경우입니다. 이벤트 버블링 가능성 때문에 DOM 타입 정의의 $event.target은 예상한 타입과 다를 수 있습니다.

이렇게 잘못된 오류가 보고될 때는 다음 방법을 사용할 수 있습니다:

  • 특정 위치에서 $any() 타입 변환 함수를 사용해서 표현식 일부를 타입 검사에서 제외합니다.
  • 애플리케이션의 TypeScript 설정 파일 tsconfig.json에서 strictTemplates: false로 설정해서 엄격 검사를 모두 비활성화합니다.
  • 엄격성 플래그 를 false로 설정해서 특정 타입 검사만 비활성화하고 다른 검사는 엄격하게 유지합니다.
  • strictTemplates와 strictNullChecks를 함께 사용하려면 strictNullInputTypes로 입력 바인딩에 대해서만 엄격한 null 타입 검사를 제외할 수 있습니다.

다음 옵션의 값은 strictTemplates와 같습니다(strictTemplates가 true이면 true이고 그 반대도 같습니다).

엄격성 플래그 효과
strictInputTypes 바인딩 표현식을 @Input() 필드에 할당할 수 있는지 검사합니다. 디렉티브의 제네릭 타입 추론에도 영향을 줍니다.
strictNullInputTypes strictInputTypes에 따라 @Input() 바인딩을 검사할 때 strictNullChecks를 준수할지 결정합니다. strictNullChecks를 고려하지 않고 만든 라이브러리를 사용할 때 비활성화하면 도움이 될 수 있습니다.
strictAttributeTypes 텍스트 어트리뷰트로 지정한 @Input() 바인딩을 검사할지 결정합니다. 예를 들어 <input matInput disabled="true">는 disabled 프로퍼티를 문자열 'true'로 설정하고, <input matInput [disabled]="true">는 불리언 true로 설정합니다.
strictSafeNavigationTypes 안전한 탐색 연산의 반환 타입을 올바르게 추론할지 결정합니다. 예를 들어 user?.name의 타입을 user의 타입을 바탕으로 추론합니다. 비활성화하면 user?.name은 any 타입이 됩니다.
strictDomLocalRefTypes DOM 엘리먼트의 지역 참조가 올바른 타입을 가질지 결정합니다. 비활성화하면 <input #ref>의 ref는 any 타입이 됩니다.
strictOutputEventTypes 컴포넌트/디렉티브의 @Output()이나 애니메이션 이벤트 바인딩에서 $event가 올바른 타입을 가질지 결정합니다. 비활성화하면 any가 됩니다.
strictDomEventTypes DOM 이벤트 바인딩에서 $event가 올바른 타입을 가질지 결정합니다. 비활성화하면 any가 됩니다.
strictContextGenerics 제네릭 제약을 포함한 제네릭 컴포넌트의 타입 매개변수를 올바르게 추론할지 결정합니다. 비활성화하면 모든 타입 매개변수가 any가 됩니다.
strictLiteralTypes 템플릿에 선언된 객체 및 배열 리터럴의 타입을 추론할지 결정합니다. 비활성화하면 이러한 리터럴의 타입은 any가 됩니다.

다른 옵션(strictTemplates와 독립적으로 설정):

엄격성 플래그 효과
strictInputAccessModifiers 바인딩 표현식을 @Input()이나 input()에 할당할 때 private/protected/readonly 등의 접근 제어자를 준수할지 결정합니다. 비활성화하면 입력의 접근 제어자를 무시하고 타입만 검사합니다. 참고: 이 검사는 입력에만 적용하며 출력에는 적용하지 않습니다.
strictUnclaimedEventNames 요소에 적용된 디렉티브의 출력 이름이나 알려진 네이티브 DOM 이벤트 이름과 일치하지 않는 이벤트 바인딩을 보고할지 결정합니다. DOM 이벤트는 TypeScript의 GlobalEventHandlersEventMap을 기준으로 대소문자를 구분하지 않고 비교하며, 일치하지 않으면 보통 출력 이름의 오타입니다. 단일 식별자로 된 camelCase 이름만 검사합니다. 하위 요소가 발생시키는 커스텀 이벤트에 관례적으로 사용하는 my-event 같은 하이픈 포함 이름은 제외하며, CUSTOM_ELEMENTS_SCHEMA 사용 시 일치하는 컴포넌트가 없는 요소도 제외합니다. strictTemplates가 true여도 이 옵션의 기본값은 false입니다.

이 플래그로 문제를 해결할 수 없다면 마지막 수단으로 strictTemplates를 비활성화하여 기본 모드로 돌아갈 수 있습니다.

권장한 어떤 방법으로도 해결되지 않는 타입 검사 오류는 템플릿 타입 검사기 자체의 버그일 수 있습니다. 기본 모드로 돌아가야만 해결되는 오류라면 이러한 버그일 가능성이 높습니다. 이 경우 팀에서 해결할 수 있도록 이슈를 등록해 주세요.

입력과 타입 검사

템플릿 타입 검사기는 바인딩 표현식의 타입이 해당 디렉티브 입력의 타입과 호환되는지 확인합니다. 예를 들어 다음 컴포넌트를 살펴봅시다:

export interface User {
  name: string;
}

@Component({
  selector: 'user-detail',
  template: '{{ user().name }}',
})
export class UserDetailComponent {
  user = input.required<User>();
}

AppComponent 템플릿은 이 컴포넌트를 다음과 같이 사용합니다:

@Component({
  selector: 'app-root',
  template: '<user-detail [user]="selectedUser"></user-detail>',
})
export class AppComponent {
  selectedUser: User | null = null;
}

AppComponent 템플릿의 타입을 검사할 때 [user]="selectedUser" 바인딩은 UserDetailComponent.user 입력에 대응합니다. 따라서 Angular는 selectedUser 프로퍼티를 UserDetailComponent.user에 할당하며 타입이 호환되지 않으면 오류가 발생합니다. TypeScript는 자체 타입 시스템으로 할당을 검사하며 애플리케이션에 설정된 strictNullChecks 등의 플래그를 따릅니다.

실행 중 타입 오류를 방지하려면 템플릿 타입 검사기에 템플릿 내부의 타입 요구사항을 더 구체적으로 제공하세요. 직접 작성한 디렉티브 정의에 템플릿 가드 함수를 제공해서 입력 타입 요구사항을 최대한 구체적으로 지정하세요. 이 가이드의 커스텀 디렉티브의 템플릿 타입 검사 개선하기를 참고하세요.

엄격한 null 검사

strictTemplates와 TypeScript의 strictNullChecks 플래그를 활성화하면 쉽게 피하기 어려운 상황에서 타입 검사 오류가 발생할 수 있습니다. 예를 들면 다음과 같습니다:

  • strictNullChecks를 활성화하지 않은 라이브러리의 디렉티브에 null일 수 있는 값을 바인딩하는 경우입니다.

    strictNullChecks 없이 컴파일한 라이브러리의 선언 파일에는 필드가 null일 수 있는지 표시되지 않습니다. 라이브러리가 null을 올바르게 처리하더라도, 컴파일러는 null 타입이 빠진 선언 파일을 기준으로 null일 수 있는 값을 검사하므로 문제가 됩니다. 컴파일러는 strictNullChecks를 준수하기 때문에 타입 검사 오류를 발생시킵니다.

  • 값을 동기적으로 방출한다는 것을 알고 있는 옵저버블에 async 파이프를 사용하는 경우입니다.

    현재 async 파이프는 구독하는 옵저버블이 비동기일 수 있다고 가정하므로 아직 사용할 수 있는 값이 없을 가능성을 고려합니다. 이 경우에도 무언가를 반환해야 하며 그 값이 null입니다. 즉, async 파이프의 반환 타입에는 null이 포함됩니다. 따라서 옵저버블이 null이 아닌 값을 동기적으로 방출한다고 알고 있더라도 오류가 발생할 수 있습니다.

위 문제에는 두 가지 우회 방법이 있습니다:

  • 템플릿에서 null일 수 있는 표현식 끝에 null이 아님을 명시하는 연산자 !를 추가합니다. 예를 들면 다음과 같습니다.
<user-detail [user]="user!"></user-detail>

이 예제에서 컴파일러는 TypeScript 코드에서와 마찬가지로 null 허용 여부에 따른 타입 불일치를 무시합니다. async 파이프를 사용하는 경우에는 다음과 같이 표현식을 괄호로 감싸야 합니다.

<user-detail [user]="(user$ | async)!"></user-detail>
  • Angular 템플릿의 엄격한 null 검사를 완전히 비활성화합니다.

    strictTemplates가 활성화되어 있어도 타입 검사의 특정 부분을 비활성화할 수 있습니다. strictNullInputTypes 옵션을 false로 설정하면 Angular 템플릿에서 엄격한 null 검사를 비활성화합니다. 이 플래그는 애플리케이션에 포함된 모든 컴포넌트에 적용됩니다.

라이브러리 작성자를 위한 권장사항

라이브러리 작성자는 사용자에게 좋은 경험을 제공하기 위해 몇 가지 조치를 할 수 있습니다. 먼저 strictNullChecks를 활성화하고 필요한 입력 타입에 null을 포함하면 사용자가 null일 수 있는 값을 전달해도 되는지 알 수 있습니다. 또한 템플릿 타입 검사기에 특화된 타입 힌트를 제공할 수 있습니다. 커스텀 디렉티브의 템플릿 타입 검사 개선하기와 입력 세터의 타입 변환을 참고하세요.

입력 세터의 타입 변환

때로는 디렉티브나 컴포넌트의 input() 프로퍼티가 바인딩된 값을 변환하는 것이 좋습니다. 일반적으로 입력의 transform 함수를 사용합니다. 예를 들어 다음 커스텀 버튼 컴포넌트를 살펴봅시다:

다음 디렉티브를 살펴보세요:

@Component({
  selector: 'submit-button',
  template: `
    <div class="wrapper">
      <button [disabled]="disabled()">Submit</button>
    </div>
  `,
})
class SubmitButton {
  disabled = input.required({transform: booleanAttribute});
}

여기서 컴포넌트의 disabled 입력은 템플릿의 <button>으로 전달됩니다. 입력에 boolean 값을 바인딩하면 모두 예상대로 동작합니다. 하지만 사용자가 이 입력을 템플릿에서 어트리뷰트로 사용한다고 가정해 봅시다:

<submit-button disabled></submit-button>

이는 다음 바인딩과 같은 효과를 냅니다:

<submit-button [disabled]="''"></submit-button>

실행 시점에 입력은 boolean 값이 아닌 빈 문자열로 설정됩니다. 이 문제를 처리하는 Angular 컴포넌트 라이브러리는 세터에서 값을 올바른 타입으로 "강제 변환"하는 경우가 많습니다:

set disabled(value: boolean) {
  this._disabled = (value === '') || value;
}

세터가 실제로 받아들이는 값의 범위에 맞도록 value 타입을 boolean에서 boolean|''로 변경하는 것이 이상적입니다. TypeScript 4.3 이전에는 게터와 세터의 타입이 같아야 했으므로 게터가 boolean을 반환해야 한다면 세터도 더 좁은 타입을 사용할 수밖에 없습니다.

사용자가 Angular의 가장 엄격한 템플릿 타입 검사를 활성화하면 문제가 됩니다. 빈 문자열('')은 실제로 disabled 필드에 할당할 수 없기 때문에 어트리뷰트 형식을 사용할 때 타입 오류가 발생합니다.

이를 해결하기 위해 Angular는 입력 필드 자체에 선언된 것보다 더 넓고 허용 범위가 큰 타입으로 @Input()을 검사하는 기능을 지원합니다. 컴포넌트 클래스에 ngAcceptInputType_ 접두사가 붙은 정적 프로퍼티를 추가해서 활성화할 수 있습니다:

class SubmitButton {
  private _disabled: boolean;

  @Input()
  get disabled(): boolean {
    return this._disabled;
  }

  set disabled(value: boolean) {
    this._disabled = value === '' || value;
  }

  static ngAcceptInputType_disabled: boolean | '';
}

TypeScript 4.3부터는 세터가 boolean|'' 타입을 받도록 선언할 수 있으므로 입력 세터 타입 변환 필드가 필요하지 않습니다. 따라서 입력 세터 타입 변환 필드는 사용 중단을 권고합니다.

이 필드에 값을 지정할 필요는 없습니다. 필드가 존재하면 Angular 타입 검사기는 disabled 입력이 boolean|'' 타입에 맞는 바인딩을 받을 수 있다고 판단합니다. 접미사는 @Input 필드 이름이어야 합니다.

특정 입력에 ngAcceptInputType_ 재정의가 있다면 세터가 재정의된 타입의 모든 값을 처리할 수 있어야 한다는 점에 주의하세요.

$any()로 타입 검사 비활성화하기

바인딩 표현식을 $any() 타입 변환 의사 함수 호출로 감싸면 해당 표현식의 검사를 비활성화할 수 있습니다. 컴파일러는 TypeScript에서 <any>나 as any를 사용하는 것처럼 이를 any 타입으로의 변환으로 처리합니다.

다음 예제에서는 person을 any 타입으로 변환해서 Property address does not exist 오류를 억제합니다.

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