Skip to content

5. 컴포넌트 만들기


지금까지 작성한 앱은 HeroesComponent가 히어로의 목록과 선택된 히어로의 상세정보를 동시에 표시한다.


하지만 모든 기능을 컴포넌트 하나가 담당하면 애플리케이션이 커질수록 이 컴포넌트를 관리하기 점점 힘들어진다. 그래서 컴포넌트가 복잡해지면 이 컴포넌트의 역할을 나눠서 일부 역할만 담당하도록 작은 컴포넌트로 나누는 것이 좋다.


이 문서에서는 히어로의 상세정보를 표시하는 부분을 분리해서 HeroDetailComponent로 만들어 본다.


그러면 HeroesComponent는 히어로의 목록만 표시하고 이 목록에서 선택된 히어로는 HeroDetailComponent가 표시할 것이다.


Note


1. HeroDetailComponent 생성하기

Angular CLI로 hero-detail 컴포넌트를 생성한다.


ng generate component hero-detail


이 명령은 다음 순서로 실행된다:


  • src/app/hero-detail 폴더를 생성한다.


그리고 이 폴더에 4개의 파일을 생성한다:


  • 컴포넌트 스타일을 지정하는 CSS 파일
  • 컴포넌트 템플릿을 정의하는 HTML 파일
  • 컴포넌트 클래스 HeroDetailComponent가 정의된 TypeScript 파일
  • HeroDetailComponent 클래스 파일을 테스트하는 파일


이 명령을 실행하면 HeroDetailComponent가 자동으로 src/app/app.module.ts 파일에 있는 @NgModule에 등록된다.


1) 템플릿 작성하기

HeroesComponent 아래쪽에 히어로의 상세정보를 표시하는 HTML 템플릿을 잘라내서 HeroDetailComponent 템플릿에 붙여넣는다.


이 때 붙여넣은 HTML에는 selectedHero를 참조하는 부분이 있다. 그런데 새로 만든 HeroDetailComponent는 선택된 히어로가 아니라 히어로 한 명의 상세정보를 표시한다. 템플릿에 있는 selectedHero는 모두 hero로 변경한다.


그러면 HeroDetailComponent의 템플릿이 다음과 같이 작성될 것이다:


src/app/hero-detail/hero-detail.component.html
<div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="hero.name" placeholder="name" />
  </div>
</div>


2) @Input() 히어로 프로퍼티 추가하기

HeroDetailComponent 템플릿에 바인딩된 hero는 컴포넌트의 hero 프로퍼티를 참조해야 한다.


HeroDetailComponent 클래스 파일을 열어서 Hero 심볼을 로드한다.


src/app/hero-detail/hero-detail.component.ts (Hero 심볼 로드하기)
import { Hero } from "../hero";


이 때 hero 프로퍼티의 값은 외부 컴포넌트인 HeroesComponent에서 바인딩되어 전달된다. 따라서 hero 프로퍼티는 @Input() 데코레이터를 사용해서 입력 프로퍼티로 선언해야 한다.


<app-hero-detail [hero]="selectedHero"></app-hero-detail>


@angular/core 패키지에서 Input 심볼을 로드한다.


src/app/hero-detail/hero-detail.component.ts (Input 로드하기)
import { Component, OnInit, Input } from "@angular/core";


그리고 @Input() 데코레이터와 함께 hero 프로퍼티를 선언한다.


src/app/hero-detail/hero-detail.component.ts
@Input() hero?: Hero;


HeroDetailComponent 클래스는 여기까지 수정하면 된다. 더 추가할 프로퍼티는 없으며 클래스에 추가할 로직도 없다. 이 컴포넌트는 단순하게 히어로 객체를 받아서 hero 프로퍼티에 할당하고, 템플릿에 이 히어로의 상세정보를 표시할 뿐이다.


2. HeroDetailComponent 표시하기

HeroesComponent는 템플릿의 해당 부분을 제거하기 전에 히어로 세부정보를 자체적으로 표시하는 데 사용되었다. 이 섹션에서는 HeroDetailComponent에 로직을 위임하는 방법을 안내한다.


이제 HeroesComponentHeroDetailComponent는 부모/자식 관계가 되었다. 부모 컴포넌트인 HeroesComponent는 자식 컴포넌트인 HeroDetailComponent를 관리한다. 부모 컴포넌트의 히어로 목록에서 히어로를 선택하면 이 히어로의 정보를 HeroDetailComponent로 보내서 히어로의 정보를 표시하게 할 것이다.


1) HeroesComponent 템플릿 수정하기

HeroDetailComponent의 셀렉터는 'app-hero-detail'이다. 원래 히어로의 상세정보를 표시하던 HeroesComponent 템플릿 아래쪽에 <app-hero-detail> 엘리먼트를 추가한다.


그리고 HeroesComponent.selectedHero 프로퍼티를 이 엘리먼트의 hero 프로퍼티에 다음과 같이 바인딩한다.


heroes.component.html (HeroDetail 바인딩)
<app-hero-detail [hero]="selectedHero"></app-hero-detail>


[hero]="selectedHero"는 Angular가 제공하는 프로퍼티 바인딩 문법이다.


이렇게 작성하면 HeroesComponentselectedHero 프로퍼티가 HeroDetailComponenthero 프로퍼티로 단방향 데이터 바인딩된다.


이제 사용자 목록에서 선택하면 selectedHero의 값이 변경된다. 그리고 selectedHero 값이 변경되면 프로퍼티 바인딩된 HeroDetailComponenthero 프로퍼티도 변경되면서 선택된 히어로의 상세정보가 화면에 표시된다.


이렇게 수정하고 나면 HeroesComponent 템플릿 코드는 다음과 같이 변경된다:


heroes.component.html
<h2>My Heroes</h2>

<ul class="heroes">
  <li
    *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelect(hero)"
  >
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

<app-hero-detail [hero]="selectedHero"></app-hero-detail>


브라우저가 갱신되고 나면 애플리케이션이 실행되면서 이전과 동일하게 동작한다.


3. 어떤 것이 변경되었을까?

이 앱은 이전과 동일하게 사용자가 히어로의 이름을 클릭하면 히어로 목록 아래에 히어로의 상세정보를 표시한다. 하지만 이제는 히어로의 상세정보를 HeroesComponent 대신 HeroDetailComponent가 표시한다.


이번 가이드에서는 HeroesComponent를 좀 더 효율적으로 관리하기 위해 컴포넌트 두 개로 분리했다:


1] HeroesComponent의 코드가 좀 더 간단해졌다.

2] HeroDetailComponent는 좀 더 다양한 기능으로 확장할 수 있지만, 이 때 부모 컴포넌트이 HeroesComponent는 신경쓰지 않아도 된다.

3] HeroesComponent를 수정할 때도 상세정보 화면은 신경쓰지 않아도 된다.

4] HeroDetailComponent는 다른 컴포넌트에서도 재사용할 수 있다.


4. 최종코드 리뷰

이 문서에서 다룬 코드는 다음과 같다.


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

@Component({
  selector: "app-hero-detail",
  templateUrl: "./hero-detail.component.html",
  styleUrls: ["./hero-detail.component.css"],
})
export class HeroDetailComponent implements OnInit {
  @Input() hero?: Hero;

  constructor() {}

  ngOnInit() {}
}
<div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="hero.name" placeholder="name" />
  </div>
</div>
<h2>My Heroes</h2>

<ul class="heroes">
  <li
    *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelect(hero)"
  >
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

<app-hero-detail [hero]="selectedHero"></app-hero-detail>
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";

import { AppComponent } from "./app.component";
import { HeroesComponent } from "./heroes/heroes.component";
import { HeroDetailComponent } from "./hero-detail/hero-detail.component";

@NgModule({
  declarations: [AppComponent, HeroesComponent, HeroDetailComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}


5. 정리

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


  • 기존 컴포넌트의 일부를 분리해서 HeroDetailComponent를 만들었다. 이 컴포넌트는 다른 곳에 재사용할 수 있다.
  • 부모 컴포넌트 HeroesComponent에서 자식 컴포넌트 HeroDetailComponent로 데이터를 전달하기 위해 프로퍼티 바인딩을 사용했다.
  • HeroDetailComponenthero 프로퍼티 값을 컴포넌트 외부인 HeroesComponent에서 가져오기 위해 @Input 데코레이터를 사용했다.

References