Skip to content

3. 히어로 에디터


이전 튜토리얼에서는 애플리케이션의 제목을 수정해 봤다. 이번 튜토리얼에서는 히어로의 정보를 표시하는 컴포넌트를 생성하고 이 컴포넌트를 애플리케이션 셸에 추가해 보자.


Note


1. 히어로 컴포넌트 생성하기

다음 명령을 실행해서 Angular CLI로 heroes 컴포넌트를 생성한다.


ng generate component heroes


그러면 CLI가 src/app/heroes/ 폴더를 생성하고 HeroesComponent를 구성하는 파일 3개와 테스트 파일을 생성한다.


자동으로 생성된 HeroesComponent 클래스 파일의 내용은 다음과 같다.


app/heroes/heroes.component.ts (초기 버전)
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-heroes",
  templateUrl: "./heroes.component.html",
  styleUrls: ["./heroes.component.css"],
})
export class HeroesComponent implements OnInit {
  constructor() {}

  ngOnInit() {}
}


Angular 컴포넌트를 선언하려면 반드시 Angular 코어 라이브러리에서 Component 심볼을 로드하고 컴포넌트 클래스에 @Component와 같이 지정해야 한다.


이 때 @Component는 클래스에 메타데이터를 지정해서 Angular 컴포넌트로 선언하는 데코레이터 함수이다.


Angular CLI는 기본적으로 3개의 메타데이터 프로퍼티를 생성한다.


1] selector: 컴포넌트의 CSS 엘리먼트 셀렉터

2] templateUrl: 컴포넌트 템플릿 파일의 위치

3] styleUrls: 컴포넌트 CSS 스타일 파일의 위치


'app-heroes'는 CSS 엘리먼트 셀렉터이다. 엘리먼트 셀렉터는 DOM 트리에서 이 컴포넌트를 표현하는 이름이며, 부모 컴포넌트의 템플릿에 사용한다.


ngOnInit은 라이프싸이클 후킹 함수이다. Angular는 컴포넌트를 생성한 직후에 ngOnInit를 호출한다. 그래서 컴포넌트를 초기화하는 로직은 이 메서드에 작성하는 것이 좋다.


컴포넌트는 반드시 export 해야 AppModule와 같은 다른 모듈에서 import 할 수 있다.


1) hero 프로퍼티 추가하기

HeroesComponenthero 프로퍼티를 추가하고 이 값을 히어로의 이름 "Windstorm"로 할당한다.


heroes.component.ts (hero 프로퍼티)
hero = "Windstorm";


2) 히어로 표시하기

heroes.component.html 템플릿 파일을 연다. 이 파일에서 Angular CLI가 만든 코드를 삭제하고 hero 프로퍼티를 데이터 바인딩하는 코드로 수정한다.


heroes.component.html
<h2>{{ hero }}</h2>


2. HeroesComponent 뷰 표시하기

HeroesComponent를 표시하려면 이 컴포넌트를 AppComponent 셸의 템플릿에 추가해야 한다.


이전 단계에서 HeroesComponent의 엘리먼트 셀렉터는 app-heroes로 선언했다. <app-heroes> 엘리먼트를 AppComponent 템플릿 파일에서 타이틀 바로 밑에 추가한다.


src/app/app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>


Angular CLI 명령 ng serve가 실행되고 있는 상태라면 브라우저는 자동으로 화면을 갱신한다. 변경된 화면에서 애플리케이션 이름과 히어로 이름이 표시되는 것을 확인해 보자.


3. 히어로 인터페이스 생성하기

실제 데이터를 생각해보면 히어로를 표현하는 객체는 이름 외에 다른 정보도 있을 수 있다.


src/app 폴더에 Hero 인터페이스를 생성하고 이 클래스에 idname 프로퍼티를 추가한다.


src/app/hero.ts
export interface Hero {
  id: number;
  name: string;
}


그리고 HeroesComponent 클래스로 돌아가서 Hero 인터페이스를 로드한다.


컴포넌트의 hero 프로퍼티를 Hero 타입으로 리팩토링한다. 이 때 id1로, nameWindstorm으로 초기화한다.


수정된 HeroesComponent 클래스 파일은 아래와 같다.


src/app/heroes/heroes.component.ts
import { Component, OnInit } from "@angular/core";
import { Hero } from "../hero";

@Component({
  selector: "app-heroes",
  templateUrl: "./heroes.component.html",
  styleUrls: ["./heroes.component.css"],
})
export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: "Windstorm",
  };

  constructor() {}

  ngOnInit() {}
}


이제 화면을 확인해보면 히어로가 제대로 표시되지 않는 것을 확인할 수 있다. 왜냐하면 히어로 프로퍼티 값을 문자열 타입에서 객체 타입으로 변경했기 때문이다.


4. 히어로 객체 표시하기

히어로의 이름이 제대로 표시되도록 템플릿을 수정하자. 상세정보를 표시하는 레이아웃에 다음과 같이 idname을 바인딩한다.


heroes.component.html (HeroesComponent 템플릿)
<h2>{{hero.name}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>


이제 브라우저가 갱신되면 히어로의 정보가 제대로 표시된다.


5. UppercasePipe로 표시형식 지정하기

다음과 같이 hero.name 바인딩 문법을 수정한다.


src/app/heroes/heroes.component.html
<h2>{{hero.name | uppercase}} Details</h2>


이제 브라우저가 갱신되면 히어로의 이름이 대문자로 표시된다.


문자열 바인딩(interpolation binding)에서 파이프 연산자(|) 바로 뒤에 있는 uppercase는 Angular의 기본 파이프인 UppercasePipe를 가리킨다.


파이프는 문자열의 형식을 지정하거나, 통화 단위를 변경하고, 날짜나 데이터가 표시되는 형식을 변경할 때 사용한다.


6. 히어로 정보 수정하기

<input> 텍스트박스를 사용해서 사용자가 히어로의 이름을 수정하게 하려고 한다.


텍스트박스는 히어로의 name 프로퍼티를 화면에 표시하면서, 동시에 유저가 입력한 값으로 프로퍼티를 업데이트해야 한다. 이 말은 데이터가 컴포넌트 클래스에서 화면으로, 그리고 반대 방향인 화면에서 클래스로 전달되어야 한다는 것을 의미한다.


이 데이터 흐름을 자동화하려면 <input> 엘리먼트와 hero.name 프로퍼티를 양방향 바인딩으로 연결하면 된다.


1) 양방향 바인딩

HeroesComponent 템플릿에서 상세 화면 영역을 아래 코드와 같이 리팩토링한다.


src/app/heroes/heroes.component.html (HeroesComponent 템플릿)
<div>
  <label for="name">Hero name: </label>
  <input id="name" [(ngModel)]="hero.name" placeholder="name" />
</div>


[(ngModel)]는 Angular의 양방향 바인딩 문법이다.


이렇게 작성하면 hero.name 프로퍼티가 HTML 텍스트박스와 양방향 바인딩되기 때문에, hero.name 프로퍼티 값이 텍스트박스로, 텍스트박스의 값이 다시 hero.name 프로퍼티로 전달될 수 있다.


2) FormsModule을 빠뜨렸다

하지만 [(ngModel)]를 추가했기 때문에 이 앱은 이제 동작하지 않는다.


이 때 브라우저에서 개발자도구를 열어 콘솔을 확인하면 다음과 같은 에러가 표시되는 것을 확인할 수 있다.


Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.


ngModel은 Angular에서 제공하는 디렉티브지만, 아무것도 설정하지 않은 상태에서 이 디렉티브를 바로 사용할 수는 없다.


이 디렉티브는 FormsModule에서 제공하는 디렉티브이기 때문에 이 디렉티브를 사용하려면 명시적으로 FormsModule을 로드해야 한다.


7. AppModule

개발자가 만든 Angular 구성요소나 서드파티 파일, 라이브러리를 Angular가 조합할 때는 이 구성요소들에 대한 정보가 필요하다. 이런 정보를 메타데이터(metadata)라고 한다.


컴포넌트 클래스에 지정해야 하는 메타데이터는 @Component 데코레이터에 지정한다. 그리고 애플리케이션 동작에 필요한 메타데이터는 보통 @NgModule 데코레이터에 지정한다.


이 중에서 가장 중요한 데코레이터는 애플리케이션 최상위 모듈인 AppModule 클래스에 지정하는 @NgModule 데코레이터이다.


Angular CLI로 프로젝트를 생성하면 src/app/app.module.ts 파일에 AppModule 클래스가 생성된다. FormsModule은 이 모듈에 등록한다.


1) FormsModule 등록하기

AppModule(app.module.ts)를 열고 @angular/forms 라이브러리에서 제공하는 FormsModule 심볼을 로드한다.


app.module.ts (FormsModule 심볼 로드하기)
import { FormsModule } from "@angular/forms"; // <-- NgModel은 이 패키지가 제공합니다.


그리고나서 이 FormsModule@NgModule 메타데이터의 imports 배열에 추가한다. 이 배열에는 애플리케이션이 동작할 때 필요한 외부 모듈을 등록한다.


app.module.ts (@NgModule 로드하기)
imports: [
  BrowserModule,
  FormsModule
],


이제 브라우저를 새로고침하면 앱이 제대로 동작한다. 화면에서 히어로의 이름을 변경할 수 있으며 이렇게 변경된 이름이 텍스트박스 위에 있는 <h2> 태그에 바로 반영되는 것도 확인할 수 있다.


2) HeroesComponent 선언하기

컴포넌트는 반드시 NgModule 중 하나에 등록되어야 한다.


하지만 HeroesComponent는 어디에도 등록하지 않았다. 그런데 이 애플리케이션이 동작하는 이유는 Angular CLI로 HeroesComponent를 생성할 때 Angular CLI가 이 컴포넌트를 AppModule에 자동으로 등록했기 때문이다.


src/app/app.module.ts 파일을 열어서 HeroesComponent가 로드되는 것을 확인해 보자.


src/app/app.module.ts
import { HeroesComponent } from "./heroes/heroes.component";


이렇게 로드한 컴포넌트 HeroesComponent@NgModule.declarations 배열에 등록되어 있다.


src/app/app.module.ts
declarations: [
  AppComponent,
  HeroesComponent
],


이 코드에서 AppModule에는 AppComponentHeroesComponent가 등록되어 있다.


8. 최종 코드 리뷰

이번 튜토리얼에서 다룬 코드의 내용을 확인해 보자.


import { Component, OnInit } from "@angular/core";
import { Hero } from "../hero";

@Component({
  selector: "app-heroes",
  templateUrl: "./heroes.component.html",
  styleUrls: ["./heroes.component.css"],
})
export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: "Windstorm",
  };

  constructor() {}

  ngOnInit() {}
}
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
  <label for="name">Hero name: </label>
  <input id="name" [(ngModel)]="hero.name" placeholder="name" />
</div>
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms"; // <-- NgModel은 이 패키지가 제공합니다.

import { AppComponent } from "./app.component";
import { HeroesComponent } from "./heroes/heroes.component";

@NgModule({
  declarations: [AppComponent, HeroesComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  title = "Tour of Heroes";
}
<h1>{{title}}</h1>
<app-heroes></app-heroes>
export interface Hero {
  id: number;
  name: string;
}


9. 정리

이번 튜토리얼을 진행하면서 다음과 같은 내용에 대해 알아봤다.


  • Angular CLI를 사용해서 두 번째 컴포넌트인 HeroesComponent를 생성했다.
  • HeroesComponentAppComponent 셸에 추가해서 화면에 표시했다.
  • 화면에 표시되는 이름의 형식을 지정하기 위해 UppercasePipe를 사용했다.
  • ngModel 디렉티브를 사용해서 양방향 데이터 바인딩을 연결했다.
  • AppModule에 대해 알아봤다.
  • Angular가 ngModel 디렉티브를 제대로 인식하고 동작할 수 있도록 AppModuleFormsModule을 로드했다.
  • 컴포넌트는 반드시 @NgModule 중 하나에 등록되어야 한다. 이 때 Angular CLI를 사용하면 더 편하다.

References