Angular

h232ch·2022년 11월 12일
0

frontend

목록 보기
1/3
post-thumbnail

시작하기


Angular란?

  • 확장 가능한 컴포넌트 구조로 웹 애플리케이션을 만드는 프레임 워크
  • 라우팅, 폼 관리, 클라이언-서버 통신 등 웹 개발에 필요한 라이브러리를 조화롭게 통합한 모음집
  • 애플리케이션 개발, 빌드, 테스트 수정에 필요한 개발자 도구를 제공

적은 노력으로 큰 생산성 향상 가능, 170만명 이상의 개발자, 컨턴츠 작성자가 Angular 생태계와 함께함


Angular 애플리케이션: 기초

컴포넌트 (Component)

컴포넌트는 애플리케이션을 구성하는 기본 단위이다. @Component() 데코레이터가 붙는 TypeScript 클래스, HTML 탬플릿, 스타일로 구성된다. @Component() 데코레이터는 아래와 같이 Angular에 필요한 정보를 지정하는 역할을 한다.

  • 컴포넌트가 템플릿에 사용될 CSS 셀렉터를 지정한다. 템플릿에서 이 셀렉터에 해당되는 HTML 엘리먼트마다 컴포넌트 인스턴스가 생성됨
  • Angular가 컴포넌트 내용으로 렌더링할 HTML 템플릿을 지정함
  • 템플릿 HTML 엘리먼트의 모습을 지정해야 한다면 이 때 필요한 CSS 스타일을 지정함
import { Component } from '@angular/core';

@Component({
  selector: 'hello-world',
  template: `
    <h2>Hello World</h2>
    <p>This is my first component!</p>
  `
})
export class HelloWorldComponent {
  // 여기에는 컴포넌트의 동작을 정의하는 코드가 들어갑니다.
}

위 컴포넌트는 템플릿에 아래 코드를 추가하여 사용 가능하다.

<hello-world></hello-world>

Angular가 컴포넌트를 렌더링하여 아래와 같은 DOM을 보여준다.

<hello-world>
    <h2>Hello World</h2>
    <p>This is my first component!</p>
</hello-world>

컴포넌트는 강력하게 캡슐화 되어있고 직관적이다. 이는 코드 가독성을 높이고 모듈화된 유닛 테스트를 용이하게 한다.


템플릿(Tempates)

컴포넌트가 어떻게 렌더링될지 정의하기 위해 HTML 탬플릿이 존재하며 인라인(template), 별도 파일(templateUrl)로 작성해서 불러올 수 있다.

템플릿은 HTML 문법을 기반으로 작성되며, 컴포넌트 값을 동적으로 반영하도록 구성한다. 컴포넌트의 상태가 변경되면 Angular가 자동으로 렌더링된 DOM을 갱신한다.

아래 코드는 문자열을 동적으로 렌더링하는 컴포넌트의 템플릿 코드이다.

// "./hello-world-interpolation.component.html"
<p>{{ message }}</p>

이 문자열은 컴포넌트 클래스에서 전달된다.

import { Component } from '@angular/core';

@Component ({
  selector: 'hello-world-interpolation',
  templateUrl: './hello-world-interpolation.component.html'
})
export class HelloWorldInterpolationComponent {
    message = 'Hello, World!';
}

템플릿에 이중 중괄호 {{ }}는 템플릿에 문자열을 바인딩하는 문법이다. (message 변수를 불러와서 렌더링 함)

문자열 외에도 Anguar는 HTML 엘리먼트의 프로퍼티, 어트리뷰트의 값을 할당하는 프로퍼티 바인딩(property binding) 문법도 제공한다.

<p
  [id]="sayHelloId"
  [style.color]="fontColor">
  You can set my color in the component!
</p>

아래 코드는 이벤트가 발생했을 때 실행될 메서드를 컴포넌트 클래스에 구현한 코드이다.

sayMessage() {
  alert(this.message);
}

Angular에서 문자열 바인딩과, 프로퍼티 바인딩, 이벤트 바인딩이 어떻게 사용되는지 확인할 수 있다.

// hello-world-bindings.component.ts

import { Component } from '@angular/core';
 
@Component ({
  selector: 'hello-world-bindings',
  templateUrl: './hello-world-bindings.component.html'
})
export class HelloWorldBindingsComponent {
  fontColor = 'blue';
  sayHelloId = 1;
  canClick = false;
  message = 'Hello, World';
 
  sayMessage() {
    alert(this.message);
  }
 
}
// hello-world-bindings.component.html

<button
  type="button"
  [disabled]="canClick"
  (click)="sayMessage()">
  Trigger alert message
</button>
<p
  [id]="sayHelloId"
  [style.color]="fontColor">
  You can set my color in the component!
</p>
<p>My color is {{ fontColor }}</p>

템플릿에 추가 기능을 구현하려면 디렉티브(directive를 사용해야 한다. 가장 많이 사용되는 디렉티브는 *ngIf*ngFor가 있다. 디렉티브를 이용하면 DOM 구조를 동적으로 변경하고 다양하게 사용할 수 있다. 아래 코드는 *ngIf 디렉티브를 사용하는 예제 코드이다.

// hello-world-ngif.component.ts

import { Component } from '@angular/core';
 
@Component({
  selector: 'hello-world-ngif',
  templateUrl: './hello-world-ngif.component.html'
})
export class HelloWorldNgIfComponent {
  message = "I'm read only!";
  canEdit = false;
 
  onEditClick() {
    this.canEdit = !this.canEdit;
    if (this.canEdit) {
      this.message = 'You can edit me!';
    } else {
      this.message = "I'm read only!";
    }
  }
}
<!-- hello-world-ngif.component.html -->

<h2>Hello World: ngIf!</h2>
 
<button type="button" (click)="onEditClick()">Make text editable!</button>
 
<div *ngIf="canEdit; else noEdit">
    <p>You can edit the following paragraph.</p>
</div>
 
<ng-template #noEdit>
    <p>The following paragraph is read only. Try clicking the button!</p>
</ng-template>
 
<p [contentEditable]="canEdit">{{ message }}</p>

Angular는 선언적 템플릿 문법을 사용하기 때문에 화면에 표시되는 단위로 애플리케이션 로직을 분리할 수 있다. 템플릿에는 표준 HTML 문법을 활용하기 때문에 구성하기 쉽고, 관리하고 쉬우며, 수정하기 쉽다. (유지보수성 용이)


의존성 주입(Dependency injection, DI)

Angular는 TypeScirpt 클래스를 활용하는 의존성 주입 시스템을 제공하기 때문에 컴포넌트에 필요한 객체의 인스턴스를 어떻게 생성하는지 직접 신경쓸 필요가 없다. (TypeScript로 작성된 코드를 Component에서 Import하고 constructor를 통해 인스턴스를 생성하는 과정) 의존성 주입을 반드시 알아야하는 것은 아니지만 모범 사례를 보면서 의존성 주입 시스템을 활용했을 때 얻는 장점이 무엇인지 알아보는것을 적극 권장한다.

의존성 시스템이 동작하는 방식을 간단한 예제로 확인해보자. logger.service.ts에는 Logger 클래스가 정의되있으며 이 클래스에는 인자로 받은 숫자를 콘솔에 출력하는 writeCount 함수가 정의되어 있다.

import { Injectable } from '@angular/core';

@Injectable({providedIn: 'root'})
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

hello-world-di.component.ts 파일에는 Angular 컴포넌트가 정의되어 있다. 이 컴포넌트에는 버튼이 하나 있는데 이를 클릭하면 Logger 클래스에 있는 writeCount 함수를 실행하려고 한다. 그러면 HelloWorldDI 클래스 생성자에 private logger: Logger 코드를 추가해서 Logger 서비스가 의존성 객체로 주입되도록 요청할 수 있다.

import { Component } from '@angular/core';
import { Logger } from '../logger.service';

@Component({
  selector: 'hello-world-di',
  templateUrl: './hello-world-di.component.html'
})
export class HelloWorldDependencyInjectionComponent  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

Angualr CLI

Angular 애플리케이션을 가장 빠르고 쉽게 모범 사례로 개발하려면 Angular CLI를 사용하면 된다.

명령설명
ng buildAngular 앱을 컴파일한다.
ng serve애플리케이션을 빌드하고 개발서버로 서비스한다. 파일이 변경되면 바로 반영한다.
ng generate스키매틱에 정해진 대로 Angular 구성요소를 생성하거나 수정한다.
ng test유닉 테스트를 실행한다.
ng e2e애플리케이션을 빌드하고 개발 서버로 띄운 후에 엔드-투-엔드 테스트를 실행한다.

Angular CLI에 대해 자세하게 알아보려면 CLI [문서]:(https://angular.kr/cli) 참고

Angular 이해하기


컴포넌트

개요

컴포넌트 기본 요소

  • 화면을 렌더링하는 HTML 템플릿
  • 동작을 정의하는 TypeScript 클래스
  • 컴포넌트를 템플릿에 추가할 때 사용하는 CSS 셀렉터
  • 추가로 컴포넌트가 표시되는 모습을 정의하는 CSS 스타일

컴포넌트 라이프싸이클

컴포넌트 인스턴스는 컴포넌트 클래스의 인스턴스를 생성한 시점부터 미리 정의된 라이프싸이클에 따라 동작하며 라이프싸이클 단계에 따라 화면에 렌더링되고 자식 컴포넌트를 화면에 추가한다. 컴포넌트가 동작하는 동안 프로퍼티로 바인딩된 데이터가 변경되었는지 감지하고, 값이 변경되면 화면과 컴포넌트 인스턴스에 있는 데이터를 갱신하기도 한다. 라이프싸이클은 Angular가 컴포넌트 인스턴스를 종료하고 DOM에 템플릿을 제거할 때까지 이어진다. 그리고 디렉티브도 컴포넌트와 비슷하게 Angular가 인스턴스를 생성하고 갱신하며 종료하는 라프싸이클을 따른다.

애플리케이션에서 라이프싸이클 후킹 메서드를 사용하면 컴포넌트나 디렉티브가 동작하는 라이프싸이클에 개입할 수 있으며 인스턴스가 생성되는 시점, 데이터 변화가 감지되는 시점, 데이터 변화가 감지된 이후 시점, 이스턴스가 종료되는 시점에 원하는 동작을 할 수 있다.


라이프싸이클 이벤트에 반응하기

라이프싸이클 이벤트 순서

애플리케이션이 컴포넌트나 디렉티브 클래스의 생성자를 실행하면서 인스턴스를 초기화하고 나면 정해진 시점에 라프싸이클 메서드가 실행된다.

Angular 라이프싸이클 후킹 메서드는 정해진 순서대로 실행한다. 그래서 원하는 용도에 맞게 절절히 사용하는 것이 중요하다.

후킹 메서드용도실행시점
ngOnChanges()바인딩된 입력 프로퍼티 값이 처음 설정되거나 변경될 때 실행된다. 이 메서드는 프로퍼티의 이전 값과 현재 값을 표현하는 SimpleChanges 객체를 인자로 받는다. 참고로 이 메서드는 매우 자주 사용되기 때문에 복잡한 로직을 작성하여 오버라이딩 할 경우 성능에 큰 영향을 줄 수 있다(컴포넌트에 입력 프로퍼티가 바인딩된 후)ngOnInit()이 실행되기 전에 한 번 실행되며 입력 프로퍼티로 바인딩된 값이 변경될 때마다 실행된다.
ngOnInit()디렉티브나 컴포넌트에 바인딩된 입력 프로퍼티 값이 처음 할당된 후에 실행된다.ngOnChanges()가 처음 실행된 후에 한 번 실행된다. ngOnInit() 템플릿에 입력 프로퍼티가 연결되지 않아 ngOnChanges()가 실행되지 않아도 실행된다.
ngDoCheck()Angular가 검출하지 못한 변화에 반응하거나, Angular가 변화를 감지하지 못하게할 때 사용한다.ngOnInit()이 실행된 직후에 한 번 실행되며, 변화 감지 싸이클이 실행되면서 ngOnChanges()가 실행된 이후에 매번 실행된다.
ngAfterContentInit()Angular가 외부 컨텐츠를 컴포넌트나 디렉티브 뷰에 프로젝션한 이후에 실행된다.ngDoCheck()가 처음 실행된 이후 한 번 실행된다.
ngAfterContentChecked()Angular가 디렉티브나 컴포넌트에 프로젝션된 컨텐츠를 검사하고 난 후에 실행된다.ngAfterContentInit()이 실행된 후 ngDoCheck()가 실행된 이후마다 실행된다.
ngAfterViewInit()Angular가 컴포넌트나 디렉티브 화면과 자식 컴포넌트 화면을 초기화한 후에 실행된다ngAfterContentChecked()가 처음 실행된 후에 한 번 실행된다.
ngAfterViewChecked()Angular가 컴포넌트나 디렉티브 화면과 자식 화면을 검사한 후에 실행된다.ngAfterViewInit()가 실행된 후, ngAfterContentChecked()가 실행된 이후마다 실행된다.
ngOnDestroy()Angular가 디렉티브나 컴포넌트 인스턴스를 종료하기 전에 실행된다. 이 메서드는 옵저버블을 구독 해지하거나 이벤트 핸들러를 제거하는 등 메모리 누수를 방지하는 로직을 작성하는 용도로 사용한다.Angular가 디렉티브나 컴포넌트 인스턴스를 종료하기 직전에 실행된다.

라이프싸이클 활용 예제

예제 코드에서 최상위 컴포넌트 AppComponent안에 있는 컴포넌트들을 보면 라이프싸이클 후킹 함수를 어떻게 활용하는지 확인할 수 있다. 이 예제 프로젝트에서 AppComponent는 모든 자식 컴포넌트의 테스트 베드로 동작하며 자식 컴포넌트는 개별 라이프싸이클 후킹 메서드를 다룬다.

컴포넌트설명
Peek-a-boo전체 라이프싸이클 후킹 메서드가 어떻게 동작하는지 보여준다. 개별 후킹 메서드가 실행되는 것을 화면에서 확인할 수 있다
Spy커스텀 디렉티브로 라이프싸이클 후킹 메서드를 활용하는 방법에 대해 다룬다. spyDriective에는 ngOnInit()과 ngonDestroy() 후킹 메서드가 정의되어 있으며, 이 디렉티브를 사용해서 엘리먼트가 화면에 추가되고 제거되는 것을 확인할 수 있다.
OnChanges컴포넌트의 입력 프로퍼티 값이 변경될 때 ngOnChanges()가 어떻게 실행되는지에 대해 다룬다. 후킹 메서드에 전달되는 changes 객체를 어떻게 활용할 수 있는지도 확인 가능하다.
ngDoCheck()메서드로 커스텀 변화감지 로직을 구현하는 방법에 대해 다룬다. ngDoCheck() 메서드가 얼마나 많이 실행되는지 확인 가능하다.
AfterViewAngular에서 의미하는 화면이 무엇인지에 대해 다룬다. ngAfterViewInit() 메서드와 ngAfterViewChecked() 메서드에 대해 다룬다.
AfterContent외부 커텐츠를 컴포넌트에 프로젝션하는 것에 대해 다룬다. 컴포넌트 자식 뷰와 프로젝션된 컨텐츠를 구분하는 방법도 설명하며 ngAfterContentInit() 메서드와 ngAfterContentChecked() 메서드에 대해 다룬다.
Counter컴포넌트와 디렉티브를 함께 사용할 때 라이프싸이클 후킹 함수를 각각 어떻게 적용하는지 알수 있다.

컴포넌트, 디렉티브 초기화하기

ngOnInit() 메서드를 활용하면 다음과 같은 초기화 작업이 가능하다.

초기화 작업설명
생성자 외부에서 초기화되는 복잡한 초기화 작업일반적으로 컴포넌트는 가볍고 간단하게 생성해야 한다 (성능상). 로직이 복잡하거나 외부 데이터를 받아와야하는 상황이라면 초기화 로직을 ngOnInit()에 작성하는 것이 좋다.
입력프로퍼티 값 할당 후 초기화 작업생성자에는 지역 변수를 할당하는 것 외의 로직은 작성하지 않는게 좋다. 디렉티브에 바인딩되는 입력 프로퍼티 값은 생성자가 실행된 후 할당된다. 이 프로퍼티 값에 따라 디렉티브를 초기화해야 한다면 생성자가 아니라 ngOnInit()에서 해야한다.

입력 프로퍼티에 데이터가 전달되는 것을 가장 먼저 확인할 수 있는 메서드는 ngOnChanges() 메서드이다. 하지만 ngOnChanges()ngOnInit() 이전뿐 아니라 그 이후에도 여러번 실행된다. ngOnInit()은 한번만 실행되기 때문에 초기화 로직은 이 메서드를 작성하는 것이 좋다.


인스턴스 종료하기

디렉티브나 컴포넌트를 종료하기 전에 실행되어야 하는 로직은 ngOnDestroy()에 작성한다. 해당 메서드는 컴포넌트나 디렉티브가 종료된가는 것을 애플리케이션 다른 영역으로 전달하는 용도로 사용할 수 있다.

ngOnDestory() 메서드를 활용하여 자동으로 정리되지 않는 메모리를 정리할 수 있다

  • 옵저버블이나 DOM 이벤트 구독 해지
  • 인터벌 타이머 중단
  • 디렉티브가 전역이나 애플리케이션 서비스에 등록한 콜백 정리

활용 예제

예제 코드를 통해 라이프싸이클 이벤트를 알아보자

라이프싸이클 이벤트 발생 순서, 빈도

Angular가 라이프싸이클 후킹 메서드를 어떤 순서로 실행하는 지 확인하려면 PeekABootComponet를 확인하면 된다

실제 앱에서 이 컴포넌트처럼 모든 라이프싸이클 메서드를 정의할 일은 거의 없으며, 데모를 위해 구성한 것이다.

이 컴포넌트에서 Create 버튼을 누르고 Destory 버튼을 누르면 아래와 같은 로그가 화면에 표시된다.

실행 순서로그 메세지
1OnChanges
2OnInit
3DoCheck
4AfterContentInit
5AfterContentChecked
6AfterViewInit
7AfterViewChecked
8OnDestroy

입력 프로퍼티(예제의 name 프로퍼티)의 값은 생성자가 실행되는 시점에 할당되지 않았다는 것에 주의해야 한다. 그래서 입력 프로퍼티를 활용해서 컴포넌트를 초기화하는 로직은 onInit() 메서드 안에 작성해야 한다.

그리고 Update Hero 버튼을 누르면 OnChanges 로그와 DoCheck, AfterContentChecked, AfterViewChecked 로그도 함께 출력된다. 이 인터페이스로 구현하는 라이프싸이클 후킹 메서드는 자주 실행되기 때문에 간단한 로직만 작성하는 것이 좋다.

DOM을 추적하는 디렉티브

Spy 예제를 보면 디렉티브에 라이프싸이클 메서드를 정의해서 컴포넌트처럼 사용하는 방법을 확인할 수 있다. SpyDirective에는 엘리먼트가 화면에 표시되는 시점을 확인하기 위해 ngOnInit(),ngOnDestroy() 메서드를 구현했다.

// src/app/spy.directive.ts

let nextId = 1;

// 엘리먼트에 스파이 디렉티브를 자유롭게 적용합니다.
// 사용방법: <div appSpy>...</div>
@Directive({selector: '[appSpy]'})
export class SpyDirective implements OnInit, OnDestroy {
  private id = nextId++;

  constructor(private logger: LoggerService) { }

  ngOnInit() {
    this.logger.log(`Spy #${this.id} onInit`);
  }

  ngOnDestroy() {
    this.logger.log(`Spy #${this.id} onDestroy`);
  }
}

이 스파이 디렉티브는 네이티브 엘리먼트(HTML)나 컴포넌트 엘리먼트에도 자유롭게 적용할 수 있고 동시에 여러 엘리먼트에 적용할 수도 있다.

// src/app/spy.component.html

<p *ngFor="let hero of heroes" appSpy>
  {{hero}}
</p>

Add Hero 버튼을 누르면 스파이 디렉티브에서 정의한 ngOnInit (바인딩된 입력값이 처음 입력될 때 실행)이 실행되며 로그를 기록한다. Reset 버튼을 누르면 히어로 엘리먼트를 모두 삭제하고 스파이 디렉티브에서 정의한 ngOnDestroy()가 실행되며 로그를 기록한다.


변화 감지 후킹 함수 활용하기

컴포넌트나 디렉티브에 바인딩된 입력 프로퍼티 값이 변경된 것을 감지하면 Angular가 ngOnChanges() 메서드를 실행한다. 해당 함수에서 값이 어떻게 변경되었는지 확인하려면 아래와 같이 작성하면 된다.

// on-Changes.component.ts (일부)

ngOnChanges(changes: SimpleChanges) {
  for (const propName in changes) {
    const chng = changes[propName];
    const cur  = JSON.stringify(chng.currentValue);
    const prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

ngOnChanges() 메서드는 SimpleChange 객체를 인자로 받는데 이 객체에는 개별 입력 포로퍼티가 객체의 프로퍼티 이름으로 선언되어 이전값과 현재값을 전달한다. 그래서 객체 프로퍼티를 순회하면 어떤 값이 변경되었는지 알 수 있다.

예제로 사용하는 OnChangesCompoent에는 입력 프로퍼티가 2개 존재한다.

// src/app/on-changes.component.ts

@Input() hero!: Hero;
@Input() power = '';

이 입력 프로퍼티들은 OnChangesParentComponent에서 이렇게 바인딩 된다.

// src/app/on-changes-parent.component.html

<on-changes [hero]="hero" [power]="power"></on-changes>

사용자가 입력프로퍼티 값을 변경할 때를 확인해보자

power 프로퍼티의 값이 변경될 때마다 로그가 출력된다. 하지만 hero.name 프로퍼티가 변경된 것은 감지하지 못하고 ngOnChanges() 메서드도 실행되지 않는다. Angular는 기본 상태에서 입력 프로퍼티 객체 자체가 변경된 것만 감지하지 때문에 hero와 같이 객체 자체가 아닌 객체 참조값인 경우 변경을 감지하지 못하는 것이다. (실제 값의 메모리 주소가 아닌 참조 값을 가지는 경우)

화면이 갱신될 때까지 기다리기

doSomeThing() 메서드는 히어로의 이름이 10글자를 넘어갔을 때 화면에 관련 메세지를 표시하는데 comment 프로퍼티를 갱신하기 전에 한 싸이클(tick) 기다린다.

// 동작을 확인하기 위해 `comment` 값을 변경해 봅니다.
private doSomething() {
  const c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
  if (c !== this.comment) {
    // 컴포넌트의 뷰는 방금 검사를 마쳤기 때문에 한 싸이클 뒤에 실행합니다.
    this.logger.tick_then(() => this.comment = c);
  }

후킹 메서드 ngAfterViewInit()ngAfterViewChecked()는 모두 컴포넌트 뷰가 갱신된 후에 실행된다. 이따 컴포넌트에 바인딩되는 comment 프로퍼티 값을 즉시 변경하면 Angular가 에러를 발생 시키기 때문에 LoggerService.tick_then()을 사용하여 브라우저의 JavaScript 싸이클을 한 번 지연시킨 후 새로운 변화 감지 싸이클을 시작하는 방식으로 구현하는 것이 좋다.

성능 저하 주의

AfterView 예제를 실행해보면 별다른 변화가 없어도 AfterViewChecked() 메서드가 자주 실행되는 것을 확인할 수 있다. 이렇게 자주 실행되는 라이프싸이클 후킹 메서드는 복잡한 로직을 작성하지 않아야 성능 저하를 피할 수 있다.

외부 컨텐츠 변경사항 감지하기

컨텐츠 프로젝션(content projection)은 컴포넌트 밖에서 가져온 HTML 컨텐츠를 컴포넌트 템플릿 안에 표시하는 것을 의미한다. 템플릿에 사용된 컨텐츠 프로젝션은 이런 경우이다.

  • 컴포넌트 엘리먼트 태그 안에 들어있는 HTML
  • 컴포넌트 템플릿에서 <ng-content>가 사용된 부분

AfterContent 예제에서 다루는 AfterContentInitAfterContentChecked 후킹 함수는 Angular가 외부 컨텐츠를 컴포넌트 안에 프로젝션한 후 실행된다.


뷰 캡슐화

Angular에서는 컴포넌트에 적용된 CSS 스타일이 컴포넌트 뷰를 대상으로 캡슐화가 진행되어 애플리케이션에 존재하는 다른 컴포넌트에 영향을 주지 않는다.

이 캡슐화 정책은 컴포넌트마다 다르게 지정 가능하나 권장하지 않는다.

뷰 캡슐화 모드

모드설명
ViewEncapsulation.ShadowDom브라우저 기본 섀도우 DOM을 활용하여 뷰를 캡슐화 한다. ShadowDom 모드는 브라우저가 섀도우 DOM을 지원할때만 정상 동작하므로 지원하는 브라우저 목록 확인이 필요하다 (Can I use 사이트 문서 참고). 섀도우 DOM은 아직 모든 환경에 사용할 수 없기 때문에 Emulated 모드가 기본값이다.
ViewEncapsulation.Emulated컴포넌트 CSS 셀렉터를 조정해서 섀도우 DOM처럼 캠슐화한다. 상세 내용
ViewEncapsulation.None뷰 캠슐화를 하지 않는다. 지정한 스타일은 컴포넌트 뿐 아니라 전역 범위에 적용된다. 컴포넌트는 캡슐화되지 않고 HTML 문서에 직접 컴포넌트 스타일을 지정한 것과 같은 효과를 낸다.

컴포넌트 통신

컴포넌트 상호작용

컴포넌트가 서로 데이터를 주고받는 방법을 다룬다.

부모 컴포넌트에서 자식 컴포넌트로 데이터 전달하기 : 입력 바인딩

@input 데코레이터를 사용하여 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 수 있다.

@Input 데코레이터는 부모 - 자식 관계에서만 사용 가능하다.

// component-interaction/src/app/hero-child.component.ts

import { Component, Input } from '@angular/core';

import { Hero } from './hero';

@Component({
  selector: 'app-hero-child',
  template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
  `
})
export class HeroChildComponent {
  @Input() hero!: Hero;
  @Input('master') masterName = '';
}

위 코드에서 @Input을 이용해서 masterName을 외부에서 바인딩 할 때 master라는 이름으로 사용하기 위한 선언이 추가된다.

HeroParentComponent*ngFor를 사용해서 HeroChildComponent를 만들고 각 컴포넌트마다 master 문자열 프로퍼티를 자식 컴포넌트의 master로 연결하여 반복되는 hero 인스턴스를 자식 컴포넌트의 hero 프로퍼티로 바인딩한다.

here! : Hero; 코드 해석
There will be a scenario when TypeScript believes that certain property, variable will be null or undefined. But if you are sure that this variable cannot be null, then you can use this operator known as Definite Assignment Assertion.

Consider the example:

let a = document.getElementById('hello');

if (a) {
    a.style.width = '100px';
}

TypeScript assumes that variable a may be null since there is no guarantee for this element to exists. So before you can access that variable, you have put in if guard. But if you know that your application is always going to have an HTML element with id #hello, then you can rewrite above code as:

const a = document.getElementById('hello');

a!.style.width = '100px';
// component-interaction/src/app/hero-parent.component.ts

import { Component } from '@angular/core';

import { HEROES } from './hero';

@Component({
  selector: 'app-hero-parent',
  template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>

    <app-hero-child
      *ngFor="let hero of heroes"
      [hero]="hero"
      [master]="master">
    </app-hero-child>
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}

이 앱을 실행하면 다음과 같이 세명의 히어로가 표기된다.

app.compoent.html 코드에서 hero-parent.component.ts를 호출 hero-parent.component.ts 코드에서는 hero-child.compoenent.ts 코드를 호출하는 구조이다.

// component-interaction/src/app/app.component.html

<div id="parent-to-child">
  <app-hero-parent></app-hero-parent>
</div>

입력 프로퍼티를 세터(Setter)로 가로채기

부모 컴포넌트에서 값이 전달될 때 추가 로직을 실행하기 위해 입력 프로퍼티에 Setter를 사용해 보자

NameChildComponent에서는 name 값을 템플릿으로 표기하기 전에 name이 존재하거나 트림 수행이 참일 경우 (name && name.trim()) 값을 변경하여 리턴한다.

이때 NameParentComponent에서 [name]="name"으로 Setter가 먼저 수행되고 이어 *ngFor 디텍티브를 통해 Getter가 호출되는 형태이다.

NameParentComponent 코드의 names에 중간 값에는 ' '와 같은 공백값으로 no name set으로 name 값이 변경된 것을 알 수 있다.

// component-interaction/src/app/name-child.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  @Input()
  get name(): string { return this._name; }
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }
  private _name = '';
}
// component-interaction/src/app/name-parent.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-name-parent',
  template: `
    <h2>Master controls {{names.length}} names</h2>

    <app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
  `
})
export class NameParentComponent {
  // 'Dr IQ', '<빈 값>', 'Bombasto'를 표시합니다.
  names = ['Dr IQ', '   ', '  Bombasto  '];
}

ngOnChanges()로 입력 프로퍼티 가로채기

위 방법외 OnChanges 라이프싸이클 후킹 인터페이스를 사용하는 ngOnChanges() 메소드로도 가로챌 수 있다.

가로채야 할 입력 프로퍼티가 여러개라면 해당 방법이 더 편할 수 있다.

VersionChildCompoenetmajorminor 두 입력 프로퍼티 값이 변경되는 것을 감지하고 이 내용을 로그로 출력한다. 부모 컴포넌트에 전달하는 두개의 값 앞에는 @Input 애노테이션을 붙여준다. 값이 변경되면 감지하는 메소드인 ngOnChanges()를 사용해서 값이 변경될 때마다 로그를 기록하고 템플릿을 통해 출력하도록 한다.

// component-interaction/src/app/version-child.component.ts

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major = 0;
  @Input() minor = 0;
  changeLog: string[] = [];

  ngOnChanges(changes: SimpleChanges) {
    const log: string[] = [];
    for (const propName in changes) {
      const changedProp = changes[propName];
      const to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        const from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}
// component-interaction/src/app/version-parent.component.ts
content_copy
import { Component } from '@angular/core';

@Component({
  selector: 'app-version-parent',
  template: `
    <h2>Source code version</h2>
    <button type="button" (click)="newMinor()">New minor version</button>
    <button type="button" (click)="newMajor()">New major version</button>
    <app-version-child [major]="major" [minor]="minor"></app-version-child>
  `
})
export class VersionParentComponent {
  major = 1;
  minor = 23;

  newMinor() {
    this.minor++;
  }

  newMajor() {
    this.major++;
    this.minor = 0;
  }
}

자식 컴포넌트에서 보내는 이벤트 받기

자식 컴포넌트에서 외부로 이벤트를 보내려면 EventEmitter 타입으로 선언한 프로퍼티에 @Output() 데코레이터를 사용해서 출력 프로퍼티로 지정하면 된다.

VoteComponent에서는 name 값을 부모 컴포넌트로부터 입력받고 voted 값을 부모 컴포넌트로 보낸다. voted 값은 VoteComponent 템플릿의 button을 클릭할 경우 Agree, Disagree 결과에 따라 boolean 값을 갖는다. 부모 컴포넌트인 VoteTrackerComponent는 해당 값을 $event라는 이름으로 받아서 onVoted() 메서드를 호출하고 해당 메서드는 결과에 따라 agreed, disagreed 값을 증가시키고 템플릿에 반영한다. 기존 컴포넌트 라이프싸이클에 의해서 값이 변경됨에 따라 템플릿 화면이 변경되는 것을 확인할 수 있다.

// component-interaction/src/app/voter.component.ts

content_copy
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button type="button" (click)="vote(true)"  [disabled]="didVote">Agree</button>
    <button type="button" (click)="vote(false)" [disabled]="didVote">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name = '';
  @Output() voted = new EventEmitter<boolean>();
  didVote = false;

  vote(agreed: boolean) {
    this.voted.emit(agreed);
    this.didVote = true;
  }
}
// component-interaction/src/app/votetaker.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>

    <app-voter
      *ngFor="let voter of voters"
      [name]="voter"
      (voted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Dr IQ', 'Celeritas', 'Bombasto'];

  onVoted(agreed: boolean) {
    if (agreed) {
      this.agreed++;
    } else {
      this.disagreed++;
    }
  }
}

콜백 함수란?

템플릿 지역 변수로 자식 컴포넌트에 접근하기

부모 컴포넌트에서 자식 컴포넌트의 프로퍼티나 메소드에 직접 접근이 불가능하다. 하지만 자식 컴포넌트의 탬플릿을 부모 컴포넌트의 지역 변수로 선언하면 자식 컴포넌트의 프로퍼티나 메소드에 직접 접근이 가능하다.

예제에서는 <countdown-titmer> 자식 템플릿을 부모 컴포넌트인 <app-counter-parent-lv>에서 지역 변수인 #timer로 지정하여 자식 컴포넌트의 프로퍼티(seconds) 나 메서드(start(), stop())에 접근 가능한 것을 확인할 수 있다.


// component-interaction/src/app/countdown-timer.component.ts

content_copy
import { Component, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-countdown-timer',
  template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnDestroy {

  intervalId = 0;
  message = '';
  seconds = 11;

  ngOnDestroy() { this.clearTimer(); }

  start() { this.countDown(); }
  stop()  {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
  }

  private clearTimer() { clearInterval(this.intervalId); }

  private countDown() {
    this.clearTimer();
    this.intervalId = window.setInterval(() => {
      this.seconds -= 1;
      if (this.seconds === 0) {
        this.message = 'Blast off!';
      } else {
        if (this.seconds < 0) { this.seconds = 10; } // reset
        this.message = `T-${this.seconds} seconds and counting`;
      }
    }, 1000);
  }
}

// component-interaction/src/app/countdown-parent.component.ts

content_copy
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-lv',
  template: `
    <h3>Countdown to Liftoff (via local variable)</h3>
    <button type="button" (click)="timer.start()">Start</button>
    <button type="button" (click)="timer.stop()">Stop</button>
    <div class="seconds">{{timer.seconds}}</div>
    <app-countdown-timer #timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }

인터벌 함수란?

화살표 함수란?

@ViewChild()로 자식 컴포넌트 접근하기

템플릿 변수를 이용해 부모 컴포넌트에서 자식 컴포넌트의 프로퍼티, 메서드에 접근하는 방식은 간단하게 구현 가능하지만 부모 컴포넌트의 템플릿에서만 자식 컴포넌트에 접근할 수 있기 때문에 제한이 발생한다. 부모 컴포넌트의 클래스에서는 자식 컴포넌트에 접근이 불가능하기 때문이다.

컴포넌트의 부모-자식 관계는 컴포넌트가 완전히 생성된 이후(컴포넌트가 인스턴스화 된 이후) 구성되기 때문에 부모 컴포넌트의 클래스에서는 자식 컴포넌트의 프로퍼티를 읽거나 메소드를 실행할 수 없다.

부모 컴포넌트의 클래스에서 자식 컴포넌트에 접근하기 위해서는 ViewChild를 사용해서 부모 컴포넌트로 주입(injection) 해야 한다. (인스턴스화 이전에 자식 컴포넌트를 주입 받아 사용)

테플릿 지역 변수를 사용하는 방식과 ViewChild를 사용하는 방식은 거의 비슷하다. 사용 목적에 따라 구현 방식을 선택하면 된다.

component-interaction/src/app/countdown-parent.component.ts
content_copy
import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-vc',
  template: `
    <h3>Countdown to Liftoff (via ViewChild)</h3>
    <button type="button" (click)="start()">Start</button>
    <button type="button" (click)="stop()">Stop</button>
    <div class="seconds">{{ seconds() }}</div>
    <app-countdown-timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {

  @ViewChild(CountdownTimerComponent)
  private timerComponent!: CountdownTimerComponent;

  seconds() { return 0; }

  ngAfterViewInit() {
    // `seconds()` 메소드는 `CountdownTimerComponent.seconds`에서 다시 구현합니다.
    // 이 때 개발 모드에서 출력하는 단방향 바인딩 검사 에러를 방지하기 위해
    // 한 싸이클 기다려야 합니다.
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
  }

  start() { this.timerComponent.start(); }
  stop() { this.timerComponent.stop(); }
}

ViewChild 데코레이터와 AfterViewInit 라이프싸이클 후킹 인터페이스를 로드 한다.

이후 CountdownTimerComponenttimerComponent 프로퍼티로 선언하면서 @ViewChild 데코레이터를 사용했다. (해당 코드는 부모 컴포넌트에서 자식 컴포넌트를 주입 받는 내용이다.)

지역 변수 #timer를 활용했을때와는 다르게 부모 컴포넌트 클래스에 구현되어 있는 seconds(), start(), stop() 함수를 직접 이용한다. 각각의 메서드는 자식 컴포넌트와 연결되도록 구현되어 있다.

이 때 ngAfterViewInit() 라이프싸이클 후킹 함수가 중요하다. 자식 컴포넌트인 CountdownTimerComponent는 Angular가 부모 컴포넌트의 뷰를 화면에 모두 표시한 이후 사용 가능하다. 그래서 뷰가 완전히 준비되기 전까지는 0을 표기한다.

부모 컴포넌트의 뷰가 준비되면 자식 컴포넌트에서 시간을 가져오기 위해 ngAfterViewInit() 라이프싸이클 후킹 함수를 실행하는데 Angular는 단방향 데이터 흐름을 권장하기 떄문에 부모 컴포넌트의 뷰를 같은 JavaScript 실행 싸이클 안에서 갱신하는 것을 금지한다.

그래서 ngAfterViewInit()에서 자식 컴포넌트의 시간을 가져와서 부모 컴포넌트 프로퍼티에 할당하는 것은 setTimeout()으로 한싸이클 늦추었다.

단방향 바인딩이란?

서비스를 사용해서 상호작용하기

부모 컴포넌트와 자식 컴포넌트가 동일한 서비스를 주입받는다면 이 서비스를 활용해서 양방향으로 데이터를 주고받을 수 있다.

컴포넌트에 주입되는 서비스는 그 컴포넌트에서 자유롭게 사용 가능하다. 이 때 주입되는 서비스의 인스턴스가 동일해야 하기 때문에 서비스 프로바이더를 별도로 지정하면 컴포넌트 통신에 활용할 수 없다.

MissionControlCompoenet, MissionService, AstronautComponent 참고

옵저버와 옵저버블

옵저버블의 서브젝트


컴포넌트 스타일

Shadow DOM?

Angular 애플리케이션의 스타일인 표준 CSS를 사용해서 지정한다. 추가로 개별 컴포넌트에 컴포넌트 스타일을 적용할 수 있고 CSS 스타일 외에 다른 스타일 도구도 활용 가능하다.

컴포넌트 스타일 사용하기

CSS를 사용하는 가장 간단한 방법은 styles 프로퍼티를 사용하는 것이다.


// src/app/hero-app.component.ts

@Component({
  selector: 'app-root',
  template: `
    <h1>Tour of Heroes</h1>
    <app-hero-main [hero]="hero"></app-hero-main>
  `,
  styles: ['h1 { font-weight: normal; }']
})
export class HeroAppComponent {
/* . . . */
}

컴포넌트 스타일 지정하기

컴포넌트에 스타일을 지정하려면 아래와 같은 방법이 존재한다.

  • 컴포넌트 메타데이터에 style이나 styleUrls 사용하기
  • 템플릿 HTML에 인라인으로 지정하기
  • 외부 CSS 파일 불러오기
컴포넌트 메타데이터로 스타일 지정하기

@component 데코레이터에는 style 프로퍼티를 지정할 수 있는데 컴포넌트에 이정될 CSS 스타일을 문자열로 지정할 수 있다.

// src/app/hero-app.component.ts (CSS inline)

@Component({
  selector: 'app-root',
  template: `
    <h1>Tour of Heroes</h1>
    <app-hero-main [hero]="hero"></app-hero-main>
  `,
  styles: ['h1 { font-weight: normal; }']
})
export class HeroAppComponent {
/* . . . */
}

위 방법으로 지정하는 경우 해당 컴포넌트에만 적용되며 템플릿 안에 있는 자식 컴포넌트나 이 컴포넌트에 프로젝트되는 다른 컨텐츠에도 적용되지 않는다.

컴포넌트 메타데이터에 외부 스타일 파일 불러오기

컴포넌트의 @component 데코레이터에 styleUrls 프로퍼티를 사용하면 컴포넌트 외부에 있는 CSS 파일을 불러와서 컴포넌트에 적용할 수 있다.

// src/app/hero-app.component.ts (CSS in file)

@Component({
  selector: 'app-root',
  template: `
    <h1>Tour of Heroes</h1>
    <app-hero-main [hero]="hero"></app-hero-main>
  `,
  styleUrls: ['./hero-app.component.css']
})
export class HeroAppComponent {
/* . . . */
}
// src/app/hero-app.component.css

h1 {
  font-weight: normal;
}

위 방법도 컴포넌트에만 CSS가 적용된다. stylesstyleUrls 프로퍼티에는 한번에 여러 스타일을 지정하거나 여러 파일을 지정할 수 있다.

템플릿 인라인 스타일

템플릿 자체에 <style> 태그를 이정해서 사용할 수도 있다.

// src/app/hero-controls.component.ts

@Component({
  selector: 'app-hero-controls',
  template: `
    <style>
      button {
        background-color: white;
        border: 1px solid #777;
      }
    </style>
    <h3>Controls</h3>
    <button type="button" (click)="activate()">Activate</button>
  `
})

컴포넌트 HTML 템플릿에는 <link> 태그를 사용할 수도 있다.

// src/app/hero-team.component.ts

@Component({
  selector: 'app-hero-team',
  template: `
    <!-- We must use a relative URL so that the AOT compiler can find the stylesheet -->
    <link rel="stylesheet" href="../assets/hero-team.component.css">
    <h3>Team</h3>
    <ul>
      <li *ngFor="let member of hero.team">
        {{member}}
      </li>
    </ul>`
})

Angular CLI가 애플리케이션을 빌드할 때 assets 폴더에 링크로 연결된 스타일파일이 존재하는지 꼭 확인해야 한다.

전역 스타일 파일

Angular CLI로 애플리케이션의 빌드 설정 파일인 angular.json 파일은 빌드에 포함될 모든 외부 자원을 지정하는데 이 때 외부 스타일 파일을 지정할 수도 있다.

이 때 styles 파일을 활용하면 전역으로 지정될 스타일 파일을 지정할 수 있으며 CLI로 생성한 프로젝트라면 styles.css 파일이 초기값으로 지정된다.

스타일 환경설정 가이드

부모, 자식 디렉티브/컴포넌트 끼리 데이터 공유하기

Angular에서는 부모 자식 컴포넌트가 데이터를 주고받는 패턴이 자주 사용되며 @Input, @Output 데코레이터로 구현한다.

<parent-component>
  <child-component></child-component>
</parent-component>

위 코드에서 <parent-component><child-component>의 컨텍스트를 제공하는 역할을 한다.

이때 @Input() 데코레이터는 자식 컴포넌트가 부모 컴포넌트와 통신할 수 있게 해주고 @Output() 데코레이터는 부모 컴포넌트로 데티터를 전달할 때 사용한다.

자식 컴포넌트로 데이터 전달하기

@Input() 데코레이터는 자식 컴포넌트/디렉티브에 있는 특정 프로퍼티가 부모 컴포넌트/디렉티브에서 값을 받는다는 것을 지정하는 데코레이터이다.

@Input() 데코레이터는 부모-자식 관계에서만 사용 가능하다.

자식 컴포넌트 설정하기
// src/app/item-detail/item-detail.component.ts

import { Component, Input } from '@angular/core'; // Input 심볼을 로드합니다.
export class ItemDetailComponent {
  @Input() item = ''; // 프로퍼티에 @Input() 데코레이터를 지정합니다.
}

위 코드에서는 자식 컴포넌트 ItemDetailComponent에서 @Input() 데코레이터를 사용하여 string 타입의 item 프로퍼티를 지정한다. 이렇게 @Input() 데코레이터로 지정된 프로퍼티는 부모 컴포넌트에서 사용 사능하다.@Input() 프로퍼티에는 number, string, boolean, object 등 어떠한 타입이라도 자유롭게 전달 가능하다. item 프로퍼티에 맞는 타입으로 데이터를 전달(부모 -> 자식)하는 것은 부모 컴포넌트의 몫이다.

// src/app/item-detail/item-detail.component.html

<p>
  Today's item: {{item}}
</p>

위와 같이 자식 컴포넌트의 템플릿에 item 프로퍼티를 활용하는 코드를 작성한다.

부모 컴포넌트 설정하기

부모 컴포넌트의 템플릿 파일인 app.componenet.html에 아래 코드를 추가한다.

// src/app/app.component.html

<app-item-detail [item]="currentItem"></app-item-detail>

부모 컴포넌트 클래스에 crrentItem 값을 할당한다.

// src/app/app.component.ts

export class AppComponent {
  currentItem = 'Television';
}

@Input() 데코레이터를 사용하면 부모 컴포넌트의 currentItem 값이 자식 컴포넌트의 item 프로퍼티로 전달되기 때문에 자식 컴포넌트 템플릿에 정의된 대로 Television 이라는 값이 화면에 렌더링된다.

이때 프로퍼티 바인딩(부모 컴포넌트 템플릿에서 자식 컴포넌트 클래스 값을 할당하는 행위) 대상이 되는 프로퍼티는 부모 컴포넌트 템플릿에 대괄호([])로 감싼 프로퍼티이고 자식 컴포넌트 클래스 @Input() 데코레이터를 지정한 프로퍼티이기도 하다. 바인딩 하는 대상은 등호(=) 오른쪽에 있는 부모 컴포넌트 클래스 프로퍼티이다.


// src/app/app.component.ts


import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {
  currentItem = 'Television';

  lastChanceItem = 'Beanbag';
  items = ['item1', 'item2', 'item3', 'item4'];
  wishlist = ['Drone', 'Computer'];


  addItem(newItem: string) {
    this.items.push(newItem);
  }


  crossOffItem(item: string) {
    console.warn(`Parent says: crossing off ${item}.`);
  }

  buyClearanceItem(item: string) {
    console.warn(`Parent says: buying ${item}.`);
  }

  saveForLater(item: string) {
    console.warn(`Parent says: saving ${item} for later.`);
  }

  addToWishList(wish: string) {
    console.warn(`Parent says: adding ${this.currentItem} to your wishlist.`);
    this.wishlist.push(wish);
    console.warn(this.wishlist);
  }
}


<app-item-detail [item]="currentItem"></app-item-detail>

@Input() 변화 감지하기

@Input() 프로퍼티로 전달되는 값이 변경되는 것을 감지하려면 ngOnChanges 라이프싸이클 후킹 함수를 사용하면 된다. OnChanges 섹션 참고

부모 컴포넌트로 데이터 전달하기

자식 컴포넌트/디렉티브에 @Output() 데코레이터를 사용하면 부모 컴포넌트/디렉티브로 데이터를 전달할 수 있다.

@Output() 데코레이터는 자식 컴포넌트 프로퍼티 중 부모 컴포넌트로 데이터를 보내는 프로퍼티를 지정하는 역할을 한다.

부모 컴포넌트로 데이터를 전달하기 위해 자식 컴포넌트에서 이벤트를 발생시키는데 이때 @Output() 데코레이터와 EventEmitter 타입을 프로퍼티에 선언하여야 한다. EventEmitter dlqpsxm zmffotmsms @angular/core 패키지에서 제공하는 클래스이다.

이번 예제에서는 자식 컴포넌트 템플릿에 있는 HTML <input> 엘리먼트에서 데이터를 받아 부모 컴포넌트로 전달하는 예제를 알아보자.

@Output() 데코레이터를 사용하려면 부모, 자식 컴포넌트 모두를 수정해야 한다.

자식 컴포넌트 설정하기

자식 컴포넌트 템플릿의 <input> 엘리먼트에 문자열을 입력 후 버튼을 클릭하면 이벤트가 발생되는 코드를 작성한다. 해당 이벤트는 EventEmitter를 사용해서 부모 컴포넌트로 전달된다.

  1. 자식 컴포넌트 클래스 파일에 Output, EventEmitter 심볼을 로드한다.
// src/app/item-output/item-output.component.ts

import { Output, EventEmitter } from '@angular/core';
  1. 컴포넌트 클래스에서 프로퍼티에 @Output() 데코레이터를 지정하고 EevnetEmitter 타입으로 선언된 newItemEvent 프로퍼티에 @Output() 데코레이터를 지정한다.
// src/app/item-output/item-output.component.ts

@Output() newItemEvent = new EventEmitter<string>();
선언부설명
@Output()자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달하는 프로퍼티를 지정한다.
newItemEvent@Output() 데코레이터가 지정된 프로퍼티이다.
EventEmitter<string>@Output 데코레이터의 타입이다.
new EventEmitter<string>()문자열 타입으로 이벤트를 전달하는 이벤트 이미터 인스턴스를 생성한다.

EventEmitter API 문서 참고

  1. 자식 컴포넌트 클래스에 addNewItem() 메서드를 추가한다.

// src/app/item-output/item-output.component.ts

export class ItemOutputComponent {

  @Output() newItemEvent = new EventEmitter<string>();

  addNewItem(value: string) {
    this.newItemEvent.emit(value);
  }
}

addNewItem() 함수는 @Output() 데코레이터가 지정된 newItemEvent를 활용해서 이벤트를 발생시키며 이 때 <input> 엘리먼트에 사용자가 입력한 값을 함께 전달한다.

자식 컴포넌트 템플릿 설정하기

자식 컴포넌트 템플릿에는 폼 컨트롤이 2개 존재한다. 하나는 사용자가 입력할 수 있는 <input> 엘리먼트이다. 해당 엘리먼트에서는 템플릿 참조 변수newItem이 지정되어 있으며 사용자가 <input> 엘리먼트에 입력한 값을 참조하려면 #newItem 변수의 value 프로퍼티를 참조하면 된다.

// src/app/item-output/item-output.component.html

<label for="item-input">Add an item:</label>
<input type="text" id="item-input" #newItem>
<button type="button" (click)="addNewItem(newItem.value)">Add to parent's list</button>

또 다른 엘리먼트는 click 이벤트가 바인딩된 <button> 엘리먼트이다. 이 엘리먼트의 (click) 이벤트는 자식 컴포넌트 클래스의 addNewItem() 메서드와 바인딩되어 있다. addNewItem() 메서드는 #newItem.value 값을 인자로 받는다. (newItem.value는 결국 input 엘리먼트에 입력된 값을 의미한다.)

(click) 이벤트가 발생되면 <input>의 입력값인 newItem.value 값이 addNewItem을 호출하고 해당 메서드는 newItemEvent 값을 이용하여 이벤트를 생성한다. 이렇게 생성된 이벤트는 부모 컴포넌트로 전달된다.

부모 컴포넌트 설정하기

// src/app/app.component.ts

export class AppComponent {
  items = ['item1', 'item2', 'item3', 'item4'];

  addItem(newItem: string) {
    this.items.push(newItem);
  }
}

부모 컴포넌트의 addItem() 메서드는 인자로 받은 문자열을 items 배열에 저장한다.

부모 컴포넌트 템플릿 설정하기

  1. 부모 컴포넌트 메서드와 자식 컴포넌트의 이벤트는 부모 컴포넌트 템플릿에서 바인딩 한다.

  2. 부모 컴포넌트 템플릿 파일 app.componenet.html에 자식 컴포넌트 셀렉터 <app-item-output>를 추가한다.

// src/app/app.component.html 

<app-item-output (newItemEvent)="addItem($event)"></app-item-output>

이벤트 바인딩 (newItemEvent)='addItem($event)'에 사용된 문법을 보면 자식 컴포넌트에서 newItemEvent 이벤트가 발생하면 부모 컴포넌트 메서드 addItem()을 실행하게 되어있다. $event 객체에는 자식 컴포넌트가 전달한 데이터가 담겨져 있다. 이 예제에서는 자식 컴포넌트의 <input>에 사용자가 입력한 값이 전달된다.

@Output() 데코레이터가 동작하는 것을 확인하기 위해 부모 컴포넌트에 이런 코드를 추가한다.

// src/app/app.component.html 

<ul>
  <li *ngFor="let item of items">{{item}}</li>
</ul>

*ngForitems 배열을 순회하며 템플릿을 반복해서 렌더링 한다. 자식 컴포넌트 템플릿 <input>에서 값을 입력하고 버튼을 클릭하는 경우 자식 컴포넌트에서 이벤트가 발생하고 해당 이벤트와 바인딩된 부모 컴포넌트 addItem() 메서드가 실행되면서 items 배열에 새로운 항목이 추가되고 화면에 렌더링 된다. (이때 바인딩된 프로퍼티 변화 감지는 ngOnChanges 라이프싸이클 함수로 가능하다.)

@Input(), @Output() 함께 사용하기

@Input(), @Output() 데코레이터는 자식 컴포넌트에서 함께 사용 가능하다.


// src/app/app.component.html

<app-input-output
  [item]="currentItem"
  (deleteRequest)="crossOffItem($event)">
</app-input-output>

@Input() 데코레이터가 지정된 item 프로퍼티는 부모 컴포넌트 currentItem 프로퍼티에서 값을 받아온다. 이후 사용자가 삭제 버튼을 클릭하면 자식 컴포넌트에서 deleteRequest 이벤트가 발생하는데 이 이벤트를 부모 컴포넌트가 감지하고 있다가 crossOffItem() 메서드를 실행하고 console 메세지에 자식 컴포넌트에서 받아온 item 프로퍼티를 보여준다.

아래 그림을 보면 <app-input-output>에 사용된 @input() 데코레이터와 @Output() 데코레이터가 어떻게 연결되는지 확인 할 수 있다.

자식 컴포넌트에 item 프로퍼티와 deleteRequest 프로퍼티는 각각 @Input() 데코레이터와 @Output() 데코레이터가 지정되어 있다. 그리고 currentItem() 메서드는 부모 컴포넌트 클래스에 정의되어 있다.

프로퍼티 바인딩과 이벤트 바인딩을 결합한 상자안에 있는 바나나 ([()]) 문법에 대한 내용은 양방향 바인딩 문서를 참고하면 된다.

컨텐츠 프로젝션

컨텐츠 프로젝션은 어떤 내용을 다른 컴포넌트 안으로 넣는 개발 패턴이다. 이 팬턴을 활용하면 어떤 컴포넌트가 전달하는 컨텐츠를 Card 컴포넌트가 받아서 표시할 수 있다.

프로젝션 방식설명
단일 슬롯 컨텐츠 프로젝션컴포넌트 외부에서 컨텐츠를 하나만 받는다.
다중 슬롯 컨텐츠 프로젝션컴포넌트 외부에서 컨텐츠를 여러 개 받는다.
조건별 컨텐츠 프로젝션특정 조건에 맞는 컨텐츠만 프로젝션 해서 렌더링 한다.

단일 슬롯 컨텐츠 프로젝션

컴포넌트 외부에서 컨텐츠를 하나만 받아 렌더링하는 방식이다.

  1. 컴포넌트 생성
  2. 컴포넌트 템플릿에 <ng-content> 엘리먼트를 추가한다. 외부에서 받아온 컨텐츠는 이 엘리먼트 안에 렌더링된다.

<ng-content> 엘리먼트 안에 외부에서 받아온 메세지를 표시한다면 아래처럼 구현하면 된다.


// content-projection/src/app/zippy-basic/zippy-basic.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-zippy-basic',
  template: `
    <h2>Single-slot content projection</h2>
    <ng-content></ng-content>
  `
})
export class ZippyBasicComponent {}

<ng-content> 엘리먼트를 추가하고 나면 이 컴포넌트를 사용하면서 컴포넌트 안으로 전달할 메세지를 지정하면 된다.


// content-projection/src/app/app.component.html

<app-zippy-basic>
  <p>Is content projection cool?</p>
</app-zippy-basic>

<ng-content>엘리먼트는 컨텐츠가 표시될 위치만 지정하는 엘리먼트이고 DOM 트리에 실제로 생성되는 DOM 엘리먼트는 아니다. <ng-content>에 사용된 커스텀 어트리뷰트는 무시된다.

다중 슬롯 컨텐츠 프로젝션

컴포넌트에 슬롯을 여러개 둘 수도 있다. CSS 셀럭터를 통해 이것을 지정할 수 있다. 이러한 방식을 다중 슬롯 컨텐츠 프로젝션이라고 한다. <ng-content>에서 select 어트리뷰트를 지정하여 컨텐츠가 들어갈 위치를 정확히 지정 가능하다.

  1. 컴포넌트를 생성한다.
  2. 컴포넌트 템플릿에 <ng-content> 엘리먼트를 추가한다. 외부에서 받아온 컨텐츠는 이 엘리먼트 안에 렌더링된다.
  3. <ng-content>select 어트리뷰트를 추가한다. Angular는 태그 이름, 어트리뷰트, CSS 클래스, :not과 같은 가상 클래스를 모두 셀렉터로 지원한다.
// content-projection/src/app/zippy-multislot/zippy-multislot.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-zippy-multislot',
  template: `
    <h2>Multi-slot content projection</h2>

    Default:
    <ng-content></ng-content>

    Question:
    <ng-content select="[question]"></ng-content>
  `
})
export class ZippyMultislotComponent {}

question 어트리뷰트가 지정된 컨텐츠가 렌더링되는 ng-content 엘리먼트는 select=[question] 어트리뷰트가 지정된 엘리먼트가 된다.

// content-projection/src/app/app.component.html

<app-zippy-multislot>
  <p question>
    Is content projection cool?
  </p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>

ng-contentselect가 지정되지 않으면 지정되지 않은 모든 컨텐츠가 ng-content에 프로젝션된다. 이 예제에서는 두 번째 ng-content 엘리먼트에만 select 어트리뷰트가 선언되었기 때문에 첫 번째 ng-content 엘리먼트에는 렌더링될 위치가 지정되지 않은 컨텐츠가 모두 표시된 것이다.

// content-projection/src/app/app.component.html

<p>Let's learn about content projection!</p>

조건별 컨텐츠 프로젝션

컴포넌트가 조건에 따라 렌더링 해야하거나 한 컨텐츠를 여러번 렌더링해야할 경우 <ng-template> 엘리먼트를 사용해서 조건을 지정할 수 있다.

컨텐츠 프로젝션을 이런 용도로 사용하는 경우 <ng-content>를 권장하지 않는다. 왜냐하면 컨텐츠를 받아서 표시하는 컴포넌트 입장에서는 이 컨텐츠가 반드시 초기화가 끝난 상태여야 하는데 이런 제약은 <ng-content>*ngIf가 적용되었거나 *ngIf 구문 안쪽으로 <ng-content> 엘리먼트가 있는 경우에도 해당하기 떄문이다.

<ng-template> 엘리먼트를 사용하면 자유로운 조건으로 렌더링될 컨텐츠를 지정할 수 있고 한 컨텐츠를 여러 번 렌더링할 수도 있다. <ng-template> 안에 있는 컨텐츠는 실제로 렌더링되는 시점에 Angular가 초기화를 수행한다.

  1. 컴포넌트를 생성한다.
  2. 컴포넌트 탬플릿에 <ng-container> 엘리먼트를 추가한다. 이 엘리먼트에는 <ng-template> 엘리먼트 내용이 표시된다.

// content-projection/src/app/example-zippy.template.html

<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>

위 내용에서 조건에 맞는 <ng-template> 엘리먼트를 선택하기 위해 ngTemplateOutlet 디렉티브를 사용했다. 디렉티브는 모든 엘리먼트에 사용할 수 있다. 이 예제에서는 실제 DOM 엘리먼트에 렌더링되지 않는 ng-container 엘리먼트에 사용했다.

<ngTemplateOutlet은 설정된 TemplateRef의 내장된 뷰형태를 제공하는 역할을 한다. (준비된 TemplateRef를 뷰에 실어줌)

  1. <ng-container> 엘리먼트를 div와 같은 엘리먼트로 감싸고 이 엘리먼트에 조건을 지정한다. <ng-content></ng-content> 코드에는 <ng-template>로 지정한 컨텐츠가 입력된다.
// content-projection/src/app/example-zippy.template.html

<ng-content></ng-content>
<div *ngIf="expanded" [id]="contentId">
    <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>
  1. 이제 프로젝션될 컨텐츠는 <ng-template> 안에 이렇게 구성하면 된다.
// content-projection/src/app/app.component.html

<ng-template appExampleZippyContent>
  It depends on what you do with it.
</ng-template>

<ng-template> 엘리먼트 내부에는 컴포넌트가 받아서 렌더링할 컨텐츠를 추가했다. 컴포넌트 <TemplateRef>@ContentChild, @ContentChildren 데코레이터를 사용해서 이 템플릿 컨텐츠를 참조할 수 있다. 이 예제에서는 템플릿을 참조하기 위해 appExampleZippyContent라는 커스텀 디렉티브를 사용했다. 이렇게 <ng-template>을 컴포넌트 클래스에서 TemplateRef 타입으로 참조하면 ngTemplateOutlet 디렉티브나 ViewContainerRef.createEmbeddedView를 사용해서 컨텐츠를 렌더링할 수 있다.

  1. 템플릿에 사용할 어트리뷰트 디렉티브를 생성한다. 이 디렉티브에는 TemplateRef 인스턴스를 의존성으로 주입한다.
// content-projection/src/app/example-zippy.component.ts

@Directive({
  selector: '[appExampleZippyContent]'
})
export class ZippyContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

이전 단계에서 <ng-template> 엘리턴트에 커스텀 디렉티브 appExampleZippyDirective를 지정했다. 이 디렉티브 코드는 Angular가 커스텀 어트리뷰트로 템플릿을 참조하기 위해 정의한 것이다.

  1. 컨텐츠가 렌더링될 컴포넌트에서 @ContentChild를 사용해서 프로젝션될 컨텐츠 템플릿을 참조한다.
// content-projection/src/app/example-zippy.component.ts

@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;

여기까지 진행하고 나면 컴포넌트는 특정 조건을 만족하는 템플릿에만 인스턴스를 생성한다. 그리고 대상이 되는 템플릿은 커스텀 디렉티브를 지정해두고 @ComponentChild 데코레이터를 사용해서 컴포넌트 클래스에 할당했다.

다중 슬롯 컨텐츠 프로젝션을 활용하는 경우 @ContentChildren는 프로젝션된 엘리먼트를 모은 QueryList를 반환한다.

애플리케이션 동작 순서
1. 컴포넌트, 디렉티브 생성

// content-projection/src/app/example-zippy.component.ts

import { Component, Directive, Input, TemplateRef, ContentChild, HostBinding, HostListener } from '@angular/core';

@Directive({
  selector: 'button[appExampleZippyToggle]',
})
export class ZippyToggleDirective {
  @HostBinding('attr.aria-expanded') ariaExpanded = this.zippy.expanded;
  @HostBinding('attr.aria-controls') ariaControls = this.zippy.contentId;
  @HostListener('click') toggleZippy() {
    this.zippy.expanded = !this.zippy.expanded;
  }
  constructor(public zippy: ZippyComponent) {}
}

let nextId = 0;

@Directive({
  selector: '[appExampleZippyContent]'
})
export class ZippyContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

@Component({
  selector: 'app-example-zippy',
  templateUrl: 'example-zippy.template.html',
})
export class ZippyComponent {
  contentId = `zippy-${nextId++}`;
  @Input() expanded = false;
  @ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
}
  1. Template 생성

// content-projection/src/app/example-zippy.template.html

<ng-content></ng-content>
<div *ngIf="expanded" [id]="contentId">
    <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>

  1. app.component.html 코드에 의해 디렉티브 내의 클래스가 인스턴스화 된다.

<app-example-zippy>
  <button type="button" appExampleZippyToggle>Is content project cool?</button>
  <ng-template appExampleZippyContent>
    It depends on what you do with it.
  </ng-template>
</app-example-zippy>

appExampleZippyToggle 클래스인 ZippyToggleDirective가 초기화되어 해당 클래스의 생성자인 ZippyComponent를 생성한다. ZippyComponent는 컴포넌트로 해당 클래스인 ZippyComponent를 초기화 하면서 @ContentChild 디렉티브를 이용하여 TemplateRef 형태의 ZippyContentDirectiveexample-zippy.template.htmlngTemplateOutlet이 가르키고 있는 content.templateRef를 지정한다. 이것은 div 태그에 적용된 ngIf 디렉티브에 의해 false일 경우 렌더링하지 않고 true일 경우 렌더링하는 구조이다.

  1. ZippyContentDirectiveZippyComponent 클래스에서 생성되고 @ContentChild에 의해 지정되었다. 이때 app.component.html<ng-template> 코드의 내용이 ZippyContentDirective 인스턴스 내용으로 지정되고 해당 인스턴스는 example-zippy.template.html<ng-content>에 프로젝션 되는데 ngTemplateOutlet에 지정된 content.templateRef에 의해 ngIf 조건에 따라 렌더링 여부가 결정된다.

# 디렉티브 사용 예제
# 어트리뷰트 디렉티브
# NgIf
# DOM 엘리먼트 추가 없이 디렉티브 적용하기

템플릿

사용자의 인터페이스를 구성한다. 템플릿은 HTML로 작성하며 여기에 Angular가 제공하는 문법을 적용해서 기능을 추가한다.

템플릿 실행문

템플릿 실행문은 HTML 템플릿 내부에서 메서드를 실행하거나 프로퍼티를 조작하는 로직을 의미한다. 템플릿 실행문을 활용하면 사용자의 이벤트에 맞춰서 컨텐츠를 동적으로 변경하거나 폼을 제출할 수 있다.

아래 예제에서 deleteHero()라고 작성된 것처럼 템플릿 실행문은 (이벤트)="실행문"이라는 형식으로 사용된다.

// src/app/app.component.html

<p>Component statement context ( (click)="onSave() )
<div class="context">
  <button type="button" (click)="deleteHero()">Delete hero</button>
</div>

Delete Hero 버튼을 클릭하면 deleteHero() 메서드가 실행된다.


deleteHero(hero?: Hero) {
	this.alert(`Delete ${hero ? hero.name : 'the hero'}.`);
  }

템플릿 실행문은 엘리먼트, 컴포넌트, 디렉티브에 모두 사용 가능하다.

실행문의 컨텍스트

템플릿 실행문은 애플리케이션의 특정 컨텍스트 안에서 동작한다. 일반적으로 템플릿 실행문의 컨텍스트(범위)는 컴포넌트 인스턴스와 같으며(컴포넌트 클래스) 템플릿 실행문은 실행문 컨텍스트 안에 있는 것만(컴포넌트 클래스) 참조할 수 있다.

템플릿 실행문의 컨텍스트 안에서 템플릿 컨텍스트에 있는 프로퍼티 참조 코드


<button type="button" (click)="onSave($event)">Save</button>
<button type="button" *ngFor="let hero of heroes" (click)="deleteHero(hero)">{{hero.name}}</button>
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>

바인딩

Angular 템플릿 안에서 바인딩을 활용하면 템플릿의 UI조각(DOM 엘리먼트, 디렉티브, 컴포넌트)와 모델(템플릿과 연결된 컴포넌트 인스턴스)을 연결할 수 있다. 모델과 화면을 동기적으로 연결하고, 화면에서 이벤트가 발생하면 모델에 알릴 수 있으며 그 반대 방향도 가능하다. 이 역할은 Angular가 제공하는 변화 감지 알고리즘이 담당한다.

바인딩 종류

  • 문자열 바인딩
  • 프로퍼티 바인딩
  • 이벤트 바인딩
  • 양방향 바인딩

바인딩은 바인딩된 값을 받을 대상(target)과 모델에서 값을 전달하는 템플릿 표현식으로 구성한다.

표현식의 컨텍스트

템플릿에 사용되는 표현식은 독립적인 컨텍스트를 갖는다. 이 컨텍스트는 컴포넌트의 인스턴스 범위이다.

recommendeditemImageUrl2AppComponent 안에 있는 프로퍼티를 가르킨다.

// src/app/app.component.html

<h4>{{recommended}}</h4>
<img alt="item 2" [src]="itemImageUrl2">

표현식은 템플릿 안쪽 컨텍스트에 있는 템플릿 입력 변수나 템플릿 차조 변수를 참조할 수 있다.

아래 코드에서 customer템플릿 입력 변수이다.'

// src/app/app.component.html (템플릿 입력 변수)

<ul>
  <li *ngFor="let customer of customers">{{customer.name}}</li>
</ul>

아래 코드에서 #customerInput템플릿 참조 변수이다.

// src/app/app.component.html (템플릿 참조 변수)

<label>Type something:
  <input #customerInput>{{customerInput.value}}
</label>

문자열 바인딩

기본적으로 문자열 바인딩은 이중 중괄호({{, }})를 구분자로 사용한다.

Angular 컴포넌트 내부에 currentCustomer라는 프로퍼티가 있다고 가정하자.

currentCustomer = 'Maria';

이 프로퍼티 값(애트리뷰트)을 템플릿에 표시하려면 이렇게 작성하면 된다.

<h3>Current customer: {{ currentCustomer }}</h3>

이 예제에서 currentCustomer값은 Maria로 대체된다. 위와 동일하게 아래의 예제 코드도 작성되었다.


<p>{{title}}</p>
<div><img alt="item" src="{{itemImageUrl}}"></div>

프로퍼티 바인딩

프로퍼티 바인딩을 활용하면 HTML 엘리먼트나 디렉티브의 프로퍼티 값을 지정할 수 있으며 버튼을 비활성화 하거나 이동할 주소를 프로그램 로직으로 할당할 수 있고 컴포넌트 데이터를 전달할 수 있다.

데이터 흐름 이해하기

프로퍼티 바인딩에서 데이터는 컴포넌트 프로퍼티에서 대상 엘리먼트 프로퍼티로 한방향으로만 움직인다.

Component property -> Element property

프로퍼티에 바인딩 하기

엘리먼트 프로퍼티에 바인딩하려면 대상 프로퍼티를 대괄호로 감싸는 문법을 사용해야 한다. 이 때 대상 프로퍼티는 값을 지정하고자 하는 DOM 프로퍼티이다. 아래 예제에서 <img> 엘리먼트의 src 프로퍼티가 대상 프로퍼티가 된다.

// src/app/app.component.html

<img alt="item" [src]="itemImageUrl">
// src/app/app.component.ts

... 중략

export class AppComponent {
  itemImageUrl = '../assets/phone.png';

...

위 예제에서 src<img> 엘리먼트의 프로퍼티이다. 대괄호가 사용되면 Angular는 프로퍼티 바인딩 표현식의 등호 오른쪽에이 있는 표현식을 평가하고 그렇지 않은 경우에는 등호 오른쪽에 있는 문자열을 단순한 문자열 리터럴로 인식하여 프로퍼티에 바인딘된다.

// src/app.component.html

<app-item-detail childItem="parentItem"></app-item-detail>

위 예제에서 바인딩되는 값은 patterItem 프로퍼티 값이 아니라 patternItem이라는 문자열이다.

엘리먼트 프로퍼티 설정하기

<img> 엘리먼트의 src 프로퍼티를 컴포넌트 itemImageUrl 프로퍼티의 값으로 바인딩하려면 대괄호를 사용해서 아래와 같이 작성하면 된다.

// src/app/app.component.html

<img alt="item" [src]="itemImageUrl">

itemImageUrl 프로퍼티는 AppComponent에 정의되어 있다.

// src/app/app.component.ts

itemImageUrl = '../assets/phone.png';
colspan과 colSpan

colspan 어트리뷰트와 colSpan 프로퍼티는 다르다. 바인딩하는 대상이 같은 철자로 헷갈릴 수 있으니 주의하자

colSpan 프로퍼티 바인딩은 이렇게 사용한다.


<!-- colSpan 프로퍼티는 캐멀 케이스입니다. -->
<tr><td [colSpan]="1 + 1">Three-Four</td></tr>

// src/app/app.component.html

컴포넌트에 있는 isUnchanged 프로퍼티를 사용해서 버튼을 비활성화 시켜보자

//src/app/app.component.html

<!-- 버튼의 활성화 여부를 `isUnchanged` 프로퍼티와 바인딩합니다. -->
<button type="button" [disabled]="isUnchanged">Disabled Button</button>

디렉티브에 있는 프로퍼티 값을 설정하려면 아래와 같이 사용하면 된다.

// src/app/app.component.html

<p [ngClass]="classes">[ngClass] binding to the classes property making this blue</p>
// src/app/app.component.ts

classes = 'special';

커스텀 컴포넌트의 값을 전달할 수도 있다. 부모/자식 컴포넌트끼리는 이렇게 데이터를 전달한다.

// src/app/app.component.html

<app-item-detail [childItem]="parentItem"></app-item-detail>

// src/app/app-item-detail.component.ts

import { Component, OnInit, Input } from '@angular/core';
// import { Item } from '../item';
// import { ITEMS } from '../mock-items';

@Component({
  selector: 'app-item-detail',
  templateUrl: './item-detail.component.html',
  styleUrls: ['./item-detail.component.css']
})
export class ItemDetailComponent implements OnInit {

  @Input() childItem = '';

  // items = ITEMS;


  currentItem = 'bananas in boxes';

  constructor() { }

  ngOnInit() {
  }

}
// src/app/app-item-detail.component.html

<p>Your item is: {{ childItem }} </p>

ItemDetailComponent 클래스의 childItem 프로퍼티를 부모 컴포넌트에 전달하고 부모 컴포넌트에서 해당 프로퍼티에 값에 parentItem을 지정하여 자식 템플릿 화면에 표기하고 있다.

버튼 비활성화하기

버튼을 비활성화하려면 불리언 값으로 DOM disabled 프로퍼티 값을 지정하면 된다.

// src/app/app.component.html

<!-- 버튼의 활성화 여부를 `isUnchanged` 프로퍼티와 바인딩합니다. -->
<button type="button" [disabled]="isUnchanged">Disabled Button</button>
// src/app/app.component.ts

isUnchanged = true;

어트리뷰트 바인딩

어트리뷰트 바인딩을 활용하면 엘리먼트 어트리뷰트의 값을 직접 지정할 수 있다. 이 문법을 활용하면 웹 접근성 향상, 애플리케이션 스타일 동적 조정, CSS 클래스 여러개와 CSS 스타일 여러개를 한 번에 적용 가능하다.

클래스, 스타일 바인딩

클래스 바인딩, 스타일 바인딩을 활용하면 엘리먼트의 class 어트리뷰트나 스타일을 동적으로 변경할 수 있다.

CSS Class 하나 바인딩하기

[class.sale]="oSale"

onSale이 참으로 평가될 때 sale 클래스를 엘리먼트에 추가하고 거짓일 경우 sale 클래스는 엘리먼트에서 제거한다.

CSS 클래스 여러개 바인딩하기

[class]="classExpression"

  • 클래스 이름을 공백으로 구분하는 문자열 ("my-class-1 my-class-2..)
  • 클래스 이름을 키로하고 참/거짓으로 평가되는 표현식을 값으로 하는 객체 ({my-class-1:true, my-class-2:false})
  • 클래스 이름으로 구성된 배열 (['my-calss-1', 'my-class-2'...])

클래스 바인딩 문법

바인딩 타입문법입력값예제
클래스 하나 바인딩[class.sale]="onSale"`booleanundefined
클래스 여러개 바인딩[class]="classExpression"string "my-class-1 my-class-2 my-class-3"
클래스 여러개 바인딩[class]="classExpression" Record<string, booleanundefined
클래스 여러개 바인딩[class]="classExpression"Array<string>['foo', 'bar']
스타일 하나 바인딩하기

style.뒤에 원하는 css 스타일을 지정하면 된다.
[style.width]="width" width의 값은 일반적으로 문자열을 사용하지만 em이나 % 단위를 명시하면 숫자 타입을 사용할 수도 있다.

  1. 대시 케이스로는 이렇게 지정한다.

<nav [style.background-color]="expression"></nav>
  1. 캐멀 케이스로는 이렇게 지정한다.

<nav [style.backgroundColor]="expression"></nav>
스타일 여러개 바인딩하기

@Component({
  selector: 'app-nav-bar',
  template: `
<nav [style]='navStyle'>
  <a [style.text-decoration]="activeLinkStyle">Home Page</a>
  <a [style.text-decoration]="linkStyle">Login</a>
</nav>`
})
export class NavBarComponent {
  navStyle = 'font-size: 1.2rem; color: cornflowerblue;';
  linkStyle = 'underline';
  activeLinkStyle = 'overline';
  /* . . . */
}
  • 문자열로 지정하는 방식 : "width: 100px; height: 100px; background-color: cornflowerblue;"
  • 스타일 이름을 키로 하고 그 값을 객체의 값으로 하는 객체 : {width: '100px', height: '100px', backgroundColor: 'cornflowerblue'}.

스타일 바인딩 문법

바인딩 타입문법입력 값예제
스타일 하나 바인딩하나 바인딩 [style.width]="width"`stringundefined
스타일 하나를 단위와 함께 바인딩[style.width.px]="width"100
스타일 여러게 바인딩[style]="styleExpression"string"width: 100px; height: 100px"
스타일 여러개 바인딩[style]="styleExpression" `Record<string, stringundefined

이벤트 바인딩(Event binding)

이벤트 바인딩 문법을 활용해서 키입력, 마우스 이동, 클릭, 터치 이벤트에 반응할 수 있다.

이벤트 바인딩하기

이벤트 바인딩 문법을 활용하면 마우스 이동, 클릭, 터치 이벤트에 반응 할 수 있다. 이벤트 바인딩 문법은((,)), 안에 대상이 되는 이벤트의 이름을 지정하고 등호(=)를 붙인 후 템플릿 실행문을 작성하면 된다.


<button (click)="onSave()">Save</button>

이벤트 바인딩 문구 (click)에 따라서 클릭 이벤트가 발생하는지 감지하다가 이벤트가 발생했을 때 컴포넌트에 정의된 onSave() 메서드를 실행한다.

이벤트 대상 확인하기

이벤트 대상을 확인하기 위해 대상 이벤트 이름이 디렉티브의 이벤트 프로퍼티 이름과 같은지 검사한다. 아래 예제처럼 구현하면 myClick 이벤트가 ClickDirective에서 발생한 커스텀 이벤트라는 것을 확인할 수 있다.

// src/app/app.component.html

<h4>myClick is an event on the custom ClickDirective:</h4>
<button type="button" (myClick)="clickMessage=$event" clickable>click with myClick</button>
{{clickMessage}}
// src/app/click.directive.ts

import { Directive, ElementRef, EventEmitter, Output } from '@angular/core';

@Directive({selector: '[myClick]'})
export class ClickDirective {
  @Output('myClick') clicks = new EventEmitter<string>(); //  @Output(alias) propertyName = ...

  toggle = false;

  constructor(el: ElementRef) {
    el.nativeElement
      .addEventListener('click', (event: Event) => {
        this.toggle = !this.toggle;
        this.clicks.emit(this.toggle ? 'Click!' : '');
      });
  }
}

엘리먼트 이벤트나 ClickDirective 프로퍼티에서 myClick을 찾지 못할경우 "unknown directive" 에러가 발생한다.

위 코드는 AppComponent의 템플릿에서 ClickDirective를 호출하고 이 과정에서 ClickDirectiveclicks 변수로 초기화된다. 초기화를 통해 Click!이라는 문자열이 EventEmitter<string>() 형태로 AppComponent의 템플릿으로 전달되며 전달된 값(Click!)은 AppComponent의 프로퍼티인 clickMessage=$event와 같이 저장되고 toggle 값(boolean)에 따라 화면에 표기되거나 사라진다.

양방향 바인딩

양방향 바인딩을 활용하면 컴포넌트끼리 데이터를 공유할 수 있다. 양방향 바인딩은 자식 컴포넌트에서 발생하는 이벤트를 감지하는 바인딩(Output()) 자식 컴포넌트에 데이터를 전달하는 프로퍼티 바인딩(Input())이 결합된 형태이다.

양방향 바인딩 추가하기
// src/app/app.component.html

<app-sizer [(size)]="fontSizePx"></app-sizer>
양방향 바인딩이 동작하는 과정

양방향 바인딩이 동작하려면 @Input() 데코레이터와 @Output() 데코레이터가 지정되어 있어야 한다. 해당 데코레이터는 각각 부모->자식, 자식->부모 방향으로 데이터를 전달한다. 예제 size 프로퍼티의 경우 @Input() 데코레이터가 지정되어 부모 컴포넌트의 템플릿 app.component.html로 전달되고 부모 컴포넌트 클래스 프로퍼티인 fontSIzePx 값으로 지정되면서 다시 자식 컴포넌트의 클래스에 전달됨과 동시에 자식 컴포넌트의 템플릿 sizer.component.html에 반영된다.

자식 컴포넌트 클래스에는 dec(), inc() 기능이 존재하는데 resize() 메서드가 실행되면서 변경된 size 값을 부모 컴포넌트 클래스에 전달하기 위해 sizeChange 프로퍼티에 @Output 데코레이터를 적용하였다. sizeChange 프로퍼티는 변경된 size 값이 입력되어 부모 컴포넌트 템플릿에 전달되는데 이 데이터를 받기 위해 아래와 같은 코드를 사용한다.

// src/app/app.component.html

<app-sizer [(size)]="fontSizePx"></app-sizer>

[size]의 경우 @Input()을 통해 부모 컴포넌트가 자식 컴포넌트로 데이터를 전달하기 위함이고 (size)의 경우 자식 컴포넌트의 데이터를 부모 텀포넌트로 전달히기 위함이다.

위 코드를 아래와 같이 작성할 수도 있다.

// src/app/app.component.html (expanded)

<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

자식 컴포넌트의 sizeChange 이벤트가 생성되면 해당 이벤트 값을 fontSizePx에 반영한다는 의미로 위 코드와 동일한 의미다.

파이프

파이프틑 템플릿 표현식에 사용하는 간단함 함수인데 입력값을 받아서 이 값을 변환한다. 파이프는 한번만 선언해두면 애플리케이션 전역에서 자유롭게 사용 가능하기 때문에 유용하다. 예를들면 날짜를 문자열 그대로 표현하는 대신 April 15, 1988과 같은 형식으로 변환 가능하다.

기본 파이프

  • DatePipe: 날짜를 원하는 형식으로 변환합니다.
  • UpperCasePipe: 문자열을 대문자로 변경합니다.
  • LowerCasePipe: 문자열을 소문자로 변경합니다.
  • CurrencyPipe: 숫자를 지역에 맞는 통화단위로 변경합니다.
  • DecimalPipe: 숫자를 지역에 맞는 숫자 표현으로 변경합니다.
  • PercentPipe: 숫자를 지열에 맞는 퍼센트 문자열로 변경합니다.

템플릿에 파이프 사용하기

<p>The hero's birthday is {{ birthday | date }}</p>
import { Component } from '@angular/core';

@Component({
  selector: 'app-hero-birthday',
  template: "<p>The hero's birthday is {{ birthday | date }}</p>"
})
export class HeroBirthdayComponent {
  birthday = new Date(1988, 3, 15); // April 15, 1988 -- 월은 0부터 시작합니다.
}

<div>
  The chained hero's birthday is
  {{  birthday | date:'fullDate' | uppercase}}
</div>

<div>
  The chained hero's birthday is
  {{ birthday | date | uppercase}}
</div>

 import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor() { }
  fontSizePx = 16;

  birthday = new Date(1988, 3, 15);
  toggle = true; // 기본 형식을 shortDate로 지정하기 위해 true 값을 할당합니다.

  get format()   { return this.toggle ? 'shortDate' : 'fullDate'; }
  toggleFormat() { this.toggle = !this.toggle; }
<p>The hero's birthday is {{ birthday | date:format }}</p>
<button type="button" (click)="toggleFormat()">Toggle Format</button>

템플릿 변수 이해하기

템플릿 변수를 활용하면 템플릿 안에 다른 영역에 존재하는 데이터를 참조할 수 있다. 템플릿 변수는 사용자가 입력한 내용에 반응하는 동작을 구현하거나 애플리케이션 폼을 사용하기 좋게 튜닝할 때 주로 사용한다.

템플릿 변수는 이런 항목을 가리킬 수 있다.

  • 템플릿 안에 있는 DOM 엘리먼트
  • 디렉티브나 컴포넌트
  • ng-template을 가리키는 TemplateRef (중요!)
  • 웹 컴포넌트

문법

템플릿 내부에 해시 기호 #를 사용하면 템플릿 변수를 선언할 수 있다. 아래 코드처럼 #phone이라고 지정하면 <input> 엘리먼트에 phone 변수를 템플릿 변수로 선언한 것이다.

// src/app/app.component.html
<input #phone placeholder="phone number" />

템플릿 변수는 컴포넌트 템플릿 안에서 자유롭게 참조할 수 있고 아래 코드에서 <button>은 컴포넌트 메서드를 실행할 때 인자를 전달하기 위해 phone 변수를 참조했다.

// src/app/app.component.html

<input #phone placeholder="phone number" />

<!-- 다른 엘리먼트들 -->

<!-- input 엘리먼트는 이벤트 핸들러에 phone 엘리먼트의 `value`를 전달합니다. -->
<button type="button" (click)="callPhone(phone.value)">Call</button>

템플릿 변수에 할당되는 값은 어떻게 결정되는가?

Anguar에서 템플릿 변수를 선언하면 이 템플릿 변수가 선언된 위치에 따라 참조하는 인스턴스의 타입이 결정된다.

  • 컴포넌트에 템플릿 변수를 선언하면 컴포넌트 인스턴스를 가리킵니다.
  • 표준 HTML 태그에 템플릿 변수를 선언하면 엘리먼트를 가리킵니다.
  • <ng-template> 엘리먼트에 템플릿 변수를 선언하면 TemplateRef 인스턴스를 가리킵니다. <ng-template>에 대해 자세하세 알아보려면 구조 디렉티브 문서의 Angular가 아스테리스크 *를 활용하는 방법을 참고하세요..

이름이 지정된 변수

  • #var="ngModel"와 같이 변수 오른쪽에 이름을 지정하면 이 변수는 엘리먼트에 지정된 디렉티브나 컴포넌트 중 exportAs 이름에 해당하는 것을 가르킨다.

NgForm에 템플릿 변수 사용하기

NgForm : You optionally export the directive into a local template variable using ngForm as the key (ex: #myForm="ngForm"). This is optional, but useful. Many properties from the underlying FormGroup instance are duplicated on the directive itself, so a reference to it gives you access to the aggregate value and validity status of the form, as well as user interaction properties like dirty and touched.

보통 템플릿 변수가 선언된 엘리먼트가 템플릿 변수의 값이 된다. (<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">) 이전 예제에서도 템플릿 변수 phone은 전화번호가 입력되는 ``<input>`` 엘리먼트를 가르킨다. 그래서 버튼을 클릭하면 컴포넌트에 있는 callPhone()` 메서드를 실행하면서 <input> 엘리먼트의 값을 인자로 전달한다.

아래 코드는 NgForm 디렉티브를 직접 참조하기 위해 디렉티브에서 exportAs로 지정된 값을 템플릿 변수 값으로 할당받는 예제 코드이다. 이 코드에서 템플릿 변수 itemForm은 템플릿 안에서 세 번 사용된다. ngForm의 변수로 지정된 ItemForm은 Summit 과정에서 데이터를 담고있는 ItemForm으로 사용되고 데이터를 표시하기도 한다.

<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">
  <label for="name">Name</label>
  <input type="text" id="name" class="form-control" name="name" ngModel required />
  <button type="submit">Submit</button>
</form>

<div [hidden]="!itemForm.form.valid">
  <p>{{ submitMessage }}</p>
</div>

해당 ngForm 어트리뷰트 값은 AppComponent 클래스의 onSubmit 메서드에서 해당 form 프로퍼티를 받아서 동작을 수행한다.


  get submitMessage() { return this._submitMessage; }
  private _submitMessage = '';

  onSubmit(form: NgForm) {
    this._submitMessage = 'Submitted. Form value is ' + JSON.stringify(form.value);
  }

ngForm 어트리뷰트 값을 사용하지 않으면 itemForm이 참조하는 객체는 HTMLFormElement, <form> 엘리먼트가 된다. 그리고 어트리뷰트 지정 없이 Angular 컴포넌트에 지정하면 컴포넌트 인스턴스 자체를 가르키게된다. 이 경우가 아니라면 엘리먼트에 지정된 디렉티브는 무시하고 DOM 엘리먼트를 가르킨다.

중첩된 템플릿 안에서 접근하기


<input #ref1 type="text" [(ngModel)]="firstExample" />
<span *ngIf="true">Value: {{ ref1.value }}</span>

위 코드의 ngModel을 사용하면 도메인 모델의 값이 변경되는 것을 실시간으로 감지할 수 있고 해당 값을 이용하여 {{ ref.value }}와 같이 문자열 바인딩을 통해 뷰에 표현할 수 있다. <input>의 값이 변경되면 <span> 값이 동시에 변경되는 것을 확인할 수 있는 두 엘리먼트가 부모 엘리먼트인 ref1에 연결되어 있기 때문이다.

위 코드에서 <span>에 있는 *ngIf는 새로운 템플릿 스코프를생성하는데, 이 스코프에서는 부모 스코프에 있는 ref1 변수에 접근 가능하다. 반면 부모 스코프에서 자식 스코프에 선언된 변수로 접근은 불가능하다.

템플릿 입력 변수 (Template input variable)

템플릿 입력 변수는 템플릿 인스턴스마다 값을 받는 변수이다.
작세한 내용은 구조 디렉티브 만들기 문서 참고

<ng-template>

ngFor를 이용하여 <ng-template>을 사용해보자

<ul>
  <ng-template ngFor let-hero [ngForOf]="heroes">
    <li>{{hero.name}}
  </ng-template>
</ul>

ngFor 디렉티브는 heroes 배역에 있는 개별 히어로마다 <ng-template> 인스턴스를 생성하는데 이때 배열의 항목마다 hero 변수 값을 할당한다. 그리고 <ng-template> 인스턴스를 만들 때 템플릿 변수를 여러개 선언하면 여러 값을 동시에 전달할 수 있다. let- 문법을 사용하면 된다.

ngFor를 사용하여 index를 함께 전달하려면 이렇게 사용하면 된다.

<ul>
  <ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
    <li>Hero number {{i}}: {{hero.name}}
  </ng-template>
</ul>

디텍티브

드디어 디렉티브.....
https://angular.kr/guide/built-in-directives

의존성 주입

0개의 댓글