본문 바로가기

웹 프로그래밍/Angular JS

[Angular2] 컴포넌트에 대해서

Component

angular2의 component에 대해서 설명합니다.

Introduce

angular2는 기본적으로 CBD(Component Based Development)이다, 여기서 Component는 W3C의 Web Component를 의미하며 명세(Spec)에 따른 배포, 조립이 가능한 독립 구성 단위이다.

  • 컴포넌트는 명세(Spec)를 정확히 구현해야한다.
  • 컴포넌트는 배포 및 다른 컴포넌트에서 사용 가능해야한다.

Structure

컴포넌트 파일은 크게 3가지 영역으로 구분할 수 있다.

  • import : 관련 라이브러리를 호출한다.
  • @Component : 장식자(Decorator)로서 컴포넌트의 기본적인 메타데이터를 정의한다.
  • Class : 컴포넌트의 상태를 정의할 클래스이다.

Import

Angular 라이브러리의 모듈을 호출할 수 있다. angular 팀은 다른 모듈과 구분짓기 라이브러리의 접두사로 @를 붙혔다.

import { Component } from '@angular/core'

사용자 정의 모듈은 아래와 같이 상대경로로 호출한다. webpack.config.js와 같은 번들러 설정을 통해 root context를 지정하여 절대경로로도 호출가능하다.

import { LoggerService } from './logger.service'

@Component

스프링의 어노테이션처럼 Angular도 컴포넌트의 메타데이터를 설정할 수 있는 @Component 장식자(Decorator)를 제공한다.

@Component({
    selector: 'hello-world',
    template: '<div>template</div>',
    styles: ['div { background: blue; }']
})
export class HelloWorld { }

selector

위 예의 @Componentselector은 다른 컴포넌트에서 <hello-world>형식으로 사용할 수 있다.

template

템플릿을 속성은 두 가지가 있다.

  • template : 직접 정의
  • templateUrl: 외부 템플릿 파일, 상대경로

Class

클래스는 템플릿과 관련된 로직을 처리한다.

  • 서비스를 통한 데이터 송수신
  • 데이터 바인딩
  • 뷰 액션에 대한 이벤트 핸들링

사용하기

컴포넌트를 사용하려면 아래와 같은 과정을 거친다.

  • 컴포넌트 생성
  • 컴포넌트 모듈에 등록

모듈에 등록되지 않는 컴포넌트는 다른 컴포넌트에서 참조하거나 사용할 수 없다. 반드시 의존하는 컴포넌트는 모듈에 등록해줘야한다.

컴포넌트 생성

파일명에 대한 명명규칙에 대한 내용은 프로젝트나 개발자의 취향이지만 기본적으로 아래와 같은 룰을 가진다.

  • name.[component, interface, module].ts : 첫 이름은 대변할 이름이고 중간 이름은 구분자이다.
  • two-word.component.ts : 이름이 만약 2가지 단어 이상일 땐, 중간에 대쉬(-)를 붙혀 구분한다.

컴포넌트 파일을 위 Decorator부분의 샘플과 같이 만들 수 있다.

컴포넌트 모듈 등록

컴포넌트를 사용하려면 모듈에 생성한 컴포넌트를 등록해야한다. @NgModuledeclarations속성에 임포트한 컴포넌트를 등록한다.

import { MyComponent } from './my.component';

@NgModule({
    declarations : [
        MyComponent
    ],
    ...
})

상호작용

여러 컴포넌트는 각각의 독립적인 개체로써 서로간의 상호작용이 가능하다.

계층 (Hierarchy)

npm 라이브러리의 의존성, 메이븐의 dependency hierarchy 또는 html element처럼 컴포넌트도 동일하게 parent와 child가 있다.

child component

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

@Component({
    selector: 'child',
    template: '<div>자식</div>',
    styles: ['div { border: 2px solid black; margin: 10px; padding: 10px; }]
})
export class ChildComponent { }

parent component

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

@Component({
    selector: 'parent',
    template: `
                <div>
                    부모
                    <child></child>
                </div>`,
    styles: ['div { border: 2px solid red; margin: 10px; padding: 10px; }']
})
export class ParentComponent { }

module

import { ChildComponent } from './child.component';
import { ParentComponent } from './parent.component';

@NgModule({
    declarations : [
        ParentComponent,
        ChildComponent
    ],
    ...
    bootstrap: [ParentComponent]
})

먼저 component 생성 후 사용할 module에 등록하는 절차로 사용할 수 있다. 모듈에 등록한 후에 잘 보면 ParentComponent에는 ChildComponent와 관련된 선택자(Selector)만 존재할 뿐 직접적인 import하지 않았음에도 child 선택자를 통해서 자식 컴포넌트로써 사용했다.

@Input을 통한 부보 -> 자식 데이터 전달

컴포넌트는 독립적인 개체이기 때문에 하위 컴포넌트가 상위 컴포넌트로부터 데이터를 전달받아야하는 경우도 있다. 물론 직접적인 전달이 아닌 service 또는 event bus, data store 등을 통해서도 가능하다.

만약 부모 컴포넌트에서 자식 컴포넌트에게 아래와 같이 값을 전달하고 싶다고 가정한다.

@Component({
    selector: 'parent',
    template: `
    <div> 부모
        <child    [data1]="data"        // 필드 데이타
                    [data2]="data2()"    // 함수
                    [data3]="data3"    // 배열
                    [data4]="1+1"        // 연산
                    [data5]="data5"    // static
                    [data6]="data6">    // getter (in typescript)
        </child>
    </div>
    `
})
export class ParentComponent {
    data = 1;

    data2() {
        return "data2";
    }

    data3 = ['one', 'two', 'three'];

    static data5 = "Five";

    get data6() {
        return ParentComponent.data5;
    }
}

위와 같이 []기호를 통해 자식 컴포넌트에게 데이터를 바인딩할 수 있었다. 자식 컴포넌트는 부모가 전달한 데이터를 아래와 같이 받을 수 있다.

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

@Component({ selector: 'child', template: <div> <h2> 자식 </div> {{data1}}, {{data2}}, {{data3}}, {{data4}}, {{data5}}, {{data6}} </div> }) export class ChildComponent { @Input() data1: number; @Input() data2: string; @Input() data3: string[]; @Input() data4: number; @Input() data5: string; @Input() data6: string; }


위와 같이 받을 수 있지만 `@Component`의 `inputs` 속성을 통해서도 받을 수 있다.

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

@Component({
    selector: 'child',
    template: `
    <div>
        <h2> 자식 </div>
        {{data1}}, {{data2}}, {{data3}}, {{data4}}, {{data5}}, {{data6}}
    </div>`,
    inputs: ['data1', 'data2', 'data3', 'data4', 'data5', 'data6']
})
export class ChildComponent {
    data1: number;
    data2: string;
    data3: string[];
    data4: number;
    data5: string;
    data6: string;
}

Custom Event(@Output + EventEmitter)를 통한 자식 -> 부모 데이터 전달

위의 @Input, inputs는 모두 부모가 자식에게 데이터를 전달하는 방식의 데이터 바인딩이다. 자식도 부모에게 데이터를 전달해야하는 경우가 있는데 이런 경우에 이벤트를 통해서 해결할 수 있다.

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

@Component({
    selector: 'child',
    template: `
    <div>
        <button (click)="emitEvent()">부모에게 이벤트 전달</div> 
    </div>`
})
exprot class ChildComponent {
    active:boolean = false;
    @Output() outputProperty = new EventEmitter<boolean>();

    emitEvent() { // 위 button 객체의 (click) 이벤트 핸들러로 사용되는 함수이다.
        active = !active;
        outputProperty.emit(active); // 이 컴포넌트의 outputProperty라는 custom event를 발생(emit) 시킨다. 
    }
}

EventEmitter<Generic>타입인 속성을 @Output을 이용해 상위 컴포넌트에 공개한다.

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

@Component({
    selector: 'parent',
    template: `
    <div>
        <h1>부모</h1>
        <span> count : {{countClick}} </span>
        <child (outputProperty)="outputEvent($event)"></child>
    </div>`
})
export class ParentComponent {
    countClick:number = 0;

    outputEvent(active: boolean) { // handling child custom event
        if(active) this.countClick++; // increase count if active is true 
    }
}

부모(상위) 컴포넌트는 자식 컴포넌트가 공개한 이벤트에 한해서 이벤트 핸들러를 바인딩할 수 있고, 자식컴포넌트의 EventEmitter속성의 emit 메서드를 통해 이벤트를 발생시킬 수 있다. 이때, 기존의 상위 컴포넌트에서 바인딩한 메서드들이 실행한다.

추가로 @Input, @Output 장식자는 모두 one-way data binding이다. []는 부모 -> 자식, ()는 자식 -> 부모, [()]는 양방향 바인딩으로 보면된다.

자식 엘리먼트의 호출과 탐색

@ViewCHild를 이용해 자식의 상태가져오기

사용방법은 @ViewChild(ClassName) variable: ClassName;과 같으며 현재 DOM에 존재하는 첫 번째 지시자(컴포넌트)의 내부 상태나 정보를 가져온다.

이때 DOM이 모두 로드된 이후에 정상적으로 상태를 가져올 수 있으므로 아래 예와 같이 setTimeout 함수를 이용해 이벤트 큐의 맨 끝에 상태 접근에 관련한 로직(람다함수)을 위치하도록 한다.

@ViewChild(Item)
set item(v: Item){
    setTimeout(() => this.status = v.status, 0);    
}

전체 코드는 아래와 같다.

@Directive({ selector: 'item'})
export class item {
    @Input() status: boolean;
}

@Component({
    selector: 'parent',
    template: `
    <item status="false" *ngIf="isShow==false"></item>
    <item status="true" *ngIf="isShow==true"></item>
    <button (click)="toggle()">선택</button>
    <p>isShow : {{isShow}}</p>
    <p>status : {{status}}</p>`
})
export class ParentComponent {
    @ViewChild(Item)
    set item(v: Item) {
        setTimeou8t(() => this.status = v.status, 0);
    }

    isShow: boolean = true;
    status: boolean;

    toggle() {
        this.isShow = !this.isShow;
    }
}

첫 번째 자식컴포넌트가 <item status="false" *ngIf="isShow==false"></item>임에도 불구하고 결과는 statustrue이다. 기존에 DOM에 있는 첫 번째 자식 컴포넌트라고 했으므로 isShowtrue이므로 렌더링 되지 않아 @ViewChild로 받아오는 상태는 두 번째 컴포넌트가 된다. 또한 버튼을 누르면 상태 값이 변해 템플릿 중 item컴포넌트가 재 렌더링하게 되는데, 이 경우 DOM의 첫 번쨰 자식 컴포넌트가 바뀌게 되므로 @ViewChild로 인해 item 속성은 계속해서 다시 set해주게 되어 마치 isShow의 값과 동일하게 토글되는 것처럼 보임을 확인할 수 있다.

@ViewChild를 통해 접근하는 컴포넌트는 공개된 속성에 대해서 모두 접근이 가능하다.

@ViewChildren을 이용해 그룹 상태 얻기

템플릿에 button이 여러개 존재하듯이 @ViewChild와 다르게 @ViewChildren은 여러 지시자의 상태를 한번에 얻을 수 있다.

@ViewChildren('설명레이블' or ClassName) children: QuaryList<ClassName>;

Component의 지시자가 component이고 cmp라는 참조 변수가 붙어있다고 아래와 같이 가정한다면

<component #cmp></component>
<component #cmp></component>
<component #cmp></component>

이렇게 사용할 수 있다.

@ViewChildren('cmp') children: QueryList<Component>;

각각의 참조변수 이름이 다른 형태도 아래와 같이 사용할 수 있다.

<component #cmp1></component>
<component #cmp2></component>
<component #cmp3></component>
...
@ViewChildren('cmp1,cmp2,cmp3') children: QueryList<Component>;

또는 해당 컴포넌트 타입을 모두 참조하고 싶다면 label 대신 아래와 같이 쓸 수 있다

@ViewChildren(Component) children: QueryList<Component>;

다만 이 역시 @ViewChild와 마찬가지로 DOM이 모두 렌더링된 뒤에 참조가 가능하므로 만약 참조 즉시 무엇인가 해야하는 코드가 필요하다면, ngAfterViewInit에 정의해야 한다.

@Component({...})
export class Component {
    @ViewChild(...) children: QueryList<...>;

    ngAfterViewInit() {
        this.children.toArray().forEach(child => child.something());
    }
}

@ContentChild이용해 컨텐츠의 상태 얻기

@ContentChild는 콘텐츠 DOM을 탐색해 상태를 저장한다. 먼저, 콘텐츠 DOM은 아래와 같은 예에서 확인할 수 있다.

<component>
    <content>이건 컨텐츠야!</content> // component 내부에 있는 모든 것을 컨텐츠라고 보면 된다.
</component>

즉 특정 컴포넌트 내에 포함된 모든 컴포넌트, 태그 등을 컨텐츠라고 지칭한다.

이전의 @View...은 컴포넌트 내 템플릿에 존재하는 요소를 탐색했다면, @Content...는 외부에서 전달하는 컨텐츠를 탐색한다. 또한 이는 아래와 같이 내부에서 참조할 수 있다.

<ng-content select="content-selector"></ng-content>

@ContentChild는 콘텐츠 DOM에서 단 한건의 상태만을 담당한다. 예를 들어서 아래와 같은 컨텐츠로 쓰일 지시자가 있다고 하자.

@Directive({ selector: 'group-title'})
export class GroupTitle {
    @Input value:string;
}

그리고 이를 사용하는 컴포넌트를 만든다.

@Component({
    selector: 'group',
    template: `
    <h1>{{this.title.value}}</h1>
    <p> something group </p>`
})
export class Group {
    @ContentChild(GroupTitle) title: GroupTitle;
}

위 두 가지 컴포넌트를 사용하는 템플릿은 아래와 같다.

<group>
    <group-title [value]="'Something Group Title'"></group-title>
</group>

@ContentsChildren를 통해 그룹 값 상태 얻기

기본적으로 @ViewChildren과 같은 원리이다.

@Directive({
    selector: 'word'
})
export class Word {
    @Input() value:string;
}

@Component({
    selector: 'word-list',
    template: `<h1>{{this.words}}</h1>`
})
export class WordListComponent {
    @ContentChildren(Word) word: QueryList<Word>;

    get words(): string { return this.word ? this.word.map(v => v.value).join(", "):''; }
}

위와 같은 예제를 아래와 같이 사용할 수 있다.

<word-list>
    <word value="something1"></word>
    <word value="something2"></word>
    <word value="something3"></word>
    <word value="something4"></word>
</word-list>

컴포넌트 스타일링

컴포넌트의 스타일링은 기본적으로 css와 같다. 하지만 기본적으로 컴포넌트는 독립적인 개체의 단위로 보기 때문에 자신의 스타일링이 부모, 자식의 스타일에 영향을 주지 않는다. 따라서 아래와 같은 3가지 참조 옵션을 제공한다.

  • :host
  • :host-context
  • /deep/

자신의 컴포넌트 :host

다른 프론트엔드 프레임워크와 다르게 NG2는 template에 최상위 엘리먼트란 존재가 없는데, 그 이유는 selector가 되기 떄문이다. 따라서 selector에 스타일 별도의 스타일을 적용하려면 :host가 필요하다.

host는 자기 자신의 컴포넌트를 의미하며 사용법은 아래와 같다.

:host { /* CSS 정의 */ }

host를 주로 사용하는 컴포넌트 그 자체에 상태변화에 대한 스타일을 적용하기 위해서다. 예를들어 focus, active, hover등이 그 대상이 될 수 있다. 사용법은 아래와 같다.

:host(:hover) selector { /* styles */ }

현재 컴포넌트가 사용된 환경에 따른 내부 스타일 적용

현재 컴포넌트가 사용된 환경은 아래와 같은 뜻을 의미한다.

<context1>
    <context2>
        <component></component>
    </context2>
</context1>

이때 환경(컨텍스트)는 context1, context2가 될 수 있다. 만약 이 외부 엘리먼트의 클래스 속성이 primary라면 아래와 같다.

<context class="primary">
    <component></component>
</context>

실제 컴포넌트에서는 이런 경우에 대한 스타일을 아래와 같이 정의할 수 있다.

:host-context(.primary) selector { /* styles */ }

컨텍스트의 클래스가 primary가 되었으므로 selector에 특정한 styles가 적용되는 것이다. 잘 응용하면 modal, notice, alarm, popup, messagebox 등과 같은 곳에서 쓰일 수 있을 것 같다.

자식컴포넌트에 스타일 적용

우선 다양한 컴포넌트에서 의존하는 컴포넌트를 사용할 때 스타일을 변경하고 해당 컴포넌트에서만 특정한 스타일을 확장할 수 있다. 여기서 확장의 의미는 상위 컴포넌트보다 하위 컴포넌트의 스타일이 우선시 되기 떄문에 덮어씌우기(Override)가 아닌 확장(Extends)이다.

사용법은 아래와 같다.

:host /deep/ selector { /* styles */ }

/deep/ 키워드를 통해서 의존 컴포넌트까지 깊은 탐색을 허용하고 이중 selectorstyles를 적용하겠다는 내용이다.