Icednut's Note

Angular 2 - 사례로 살펴보는 @Input과 @Output의 이해 1

2016-12-09

sample app


문제

app.component.html
1
2
3
4
5
<my-toolbar></my-toolbar>
<div style="padding:10px;">
<router-outlet></router-outlet>
</div>

Angular 2 기반의 프로젝트에서 위와 같은 컴포넌트 구성일 경우 메뉴1, 2를 클릭할 때마다 빨간색 네모 영역에 메뉴의 서브타이틀을 표시하고 싶을 경우 Angular 2에서는 어떻게 풀 수 있을까?
화면에 보이는 바와 같이 상단에 툴바가 있고, router를 통해 각 메뉴에 해당하는 내용이 출력되는 상황이다.

프로젝트 파일 구조는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
├── ...
├── package.json
├── src
│ ├── app
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── index.ts
│ │ ├── menu1
│ │ │ ├── menu1.component.css
│ │ │ ├── menu1.component.html
│ │ │ ├── menu1.component.spec.ts
│ │ │ └── menu1.component.ts
│ │ ├── menu2
│ │ │ ├── menu2.component.css
│ │ │ ├── menu2.component.html
│ │ │ ├── menu2.component.spec.ts
│ │ │ └── menu2.component.ts
│ │ └── my-toolbar
│ │ ├── my-toolbar.component.css
│ │ ├── my-toolbar.component.html
│ │ ├── my-toolbar.component.spec.ts
│ │ └── my-toolbar.component.ts
│ ├── ...
│ ├── index.html
│ ├── main.ts
│ ├── styles.css
│ └── ...
└── ...


문제 풀이

단순하게 생각하면 메뉴가 바뀔 때마다 my-toolbar 컴포넌트에 서브타이틀 문자열을 넘겨주고 my-toolbar는 넘겨받은 서브타이틀 문자열을 출력하면 될 것 같다.
그런데 서브타이틀을 어떻게 넘겨줄 것인가? 이 때 사용하는 것이 Data Binding 이다. 여러가지 방법이 있겠지만 일단 지금의 컴포넌트 구성을 깨지 않고 진행을 해보자.

1. 먼저 my-toolbar에서 서브타이틀을 넘겨 받을 수 있게 Input 데이터 바인딩을 해준다.

app.component.html
1
2
3
4
5
<my-toolbar [currentMenuSubTitle]="menuSubTitle"></my-toolbar>
<div style="padding:10px;">
<router-outlet></router-outlet>
</div>

my-toolbar 앨리먼트에 쓰여진 어트리뷰트를 살펴보면 currentMenuSubTitle에 대괄호가 둘러쌓여져 있는데 이는 my-toolbar 컴포넌트의 Input 필드를 의미한다.
위의 상황으로 보면 app.component의 menuSubTitle 이라는 필드를 my-toolbar 컴포넌트의 currentMenuSubTitle 이라는 필드로 데이터 바인딩을 하겠다는 것을
의미한다.

2. 데이터 바인딩이 잘 될 수 있도록 my-toolbar 컴포넌트에도 동일한 이름의 @Input이 붙은 필드를 선언해준다.

my-toolbar.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Input, Component, OnInit} from '@angular/core';
@Component({
selector: 'my-toolbar',
templateUrl: './my-toolbar.component.html',
styleUrls: ['./my-toolbar.component.css']
})
export class MyToolbarComponent implements OnInit {
@Input()
currentMenuSubTitle: string;
constructor() { }
ngOnInit() {
}
}

3. app.component에도 menuSubTitle 라는 필드를 선언하고 문자열을 넣어본다.

app.component.ts
1
2
3
4
5
6
7
8
9
10
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
menuSubTitle = 'Hello, world!!';
}

이렇게 하면 아래와 같이 서브타이틀이 표시되는 것을 볼 수 있다.

sample app

자, 그럼 Menu1Component와 Menu2Component가 AppComponent의 menuSubTitle 필드로 데이터를 넣어줄 것인가?
여러가지 방법이 있겠지만 service 클래스를 사용하여 문제 해결을 진행해보자.

4. 서브타이틀 문자열의 변화를 감지할 옵저버를 선언한다.

여기서는 Service 클래스를 사용하여 옵저버 역할을 하게 한다.

my-toolbar.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class MyToolbarService {
subTitleChangeEvent: EventEmitter<string> = new EventEmitter<string>();
constructor() { }
setSubTitle(subTitle: string) {
this.subTitleChangeEvent.emit(subTitle);
}
onChangeSubTitle(handler: any) {
this.subTitleChangeEvent.subscribe(handler);
}
}

5. AppComponent에서는 subTitle의 변화가 있을 때 자신의 menuSubTitle 필드에 데이터를 바인딩한다.

이렇게 서브타이틀 변화에 대한 핸들러를 생성자에서 선언해준다. 이 핸들러에서는 서브타이틀 변화가 있을 때 마다 menuSubTitle 필드에 데이터를 바인딩하는 로직이 담겨 있다.
실수하지 말아야 할 부분이 있는데 providers에 서브타이틀 이벤트 처리가 담긴 MyToolbarService 클래스를 선언해줘야지 정상적으로 이벤트 핸들링을 할 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit } from '@angular/core';
import {MyToolbarService} from './my-toolbar.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [MyToolbarService]
})
export class AppComponent {
menuSubTitle;
constructor(private myToolbarService: MyToolbarService) {
this.myToolbarService.onChangeSubTitle(newMenuSubTitle => {
this.menuSubTitle = newMenuSubTitle;
});
}
}

6. Menu1Comopnent와 Menu2Component에서는 서브타이틀 변경 이벤트 발생 로직을 작성한다.

Menu1Component에서는 아래와 같이 서브타이틀 값을 변경하면 앞에 선언한 서비스 코드에 나와 있다시피 서브타이틀 변경 이벤트가
발생하게 된다. 그러면 AppComponent에서 이를 감지하여 AppComponent.menuSubTitle 필드에 변경된 값을 할당하게 될 것이고,
AppComponent.menuSubTitle는 my-toolbar의 인풋 필드와 바인딩이 되어 있기 때문에 Menu1Component와 Menu2Component의 ngOnInit에서
setSubTitle 메소드를 실행하여 서브타이틀 값을 변경하면 툴바의 서브타이틀 부분이 갱신되게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Output, Component, OnInit, EventEmitter} from '@angular/core';
import {MyToolbarService} from '../my-toolbar.service';
@Component({
selector: 'app-menu1',
templateUrl: './menu1.component.html',
styleUrls: ['./menu1.component.css']
})
export class Menu1Component implements OnInit {
constructor(private myToolbarService: MyToolbarService) { }
ngOnInit() {
this.myToolbarService.setSubTitle("menu1's subtitle");
}
}
sample app 결과 sample app 결과

전체 결과 소스코드

https://github.com/icednut/angular2-exercise

위 방법이 정답이라 생각하지 않고 또 다른 효율적인 방법이 있을 것이라 생각한다. 일단 router-outlet 밑에서 동작하는 컴포넌트들에 대해
AppComponent와 연결할 수 있는 마땅한 방법이 떠오르지 않아 서비스 클래스를 사용했지만, 좀 더 코드도 줄이면서 데이터 바인딩도 잘 될 수 있는
효율적인 방법을 찾는 것에 대해 계속 노력해야겠다.

Tags: angular2 input output