Renderless Component (이하 RC라고 부르겠다) 패턴은 말 그대로 렌더링을 하지 않는 컴포넌트를 두는 패턴을 의미한다.
RC는 렌더링은 다른 컴포넌트에게 위임하고 자기 자신은 컴포넌트를 제어하는 함수, 상태를 가져 재 사용 가능하도록 View와 Business를 분리하는 패턴을 의미한다.
쉽게 말해 Container & Presenter 패턴이다.
보통 Container & Presenter 패턴은 내부에 Presenter Component를 두고 Container Component는 Presenter Component를 wrapping하여 데이터를 전달, 조작하는 방식으로 구성되어져 있다.
때문에 부모컴포넌트와 자식 컴포넌트를 연결하는 코드가 생겨 다소 지저분해질 수 있는데
Angular에서는 이런 부분을 Directive를 사용, 분리하여 깔끔하게 떼어내는것이 가능하다.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-use-toggle',
template: `
<div *appToggle="let controller; on: false">
<button (click)="controller.setOn()">Blue pill</button>
<button (click)="controller.setOff()">Red pill</button>
<div>
<span *ngIf="controller.on">on</span>
<span *ngIf="!controller.on">off</span>
</div>
</div>
`
})
export class UseToggleComponent {}
위 코드는 BluePill을 누르면 스위치가 켜지고, RedPill을 누르면 스위치가 꺼지는 간단한 코드이다.
코드를 보면 알겠지만 UseToogleComponent에서 상태와 상태를 제어하는 함수는 존재하지 않고 모두 appToggle 디렉티브가 제공하는 함수들을 사용해서 사용하고 있다.
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
type Toggle = {
on: boolean;
setOn: Function;
setOff: Function;
toggle: Function;
};
@Directive({
selector: '[appToggle]'
})
export class ToggleDirective {
on = true;
@Input('appToggleOn')
initialState = true;
constructor(
private tpl: TemplateRef<{ $implicit: Toggle }>,
private vc: ViewContainerRef
) {}
ngOnInit() {
this.vc.createEmbeddedView(this.tpl, { //context를 선언하는 파라미터
$implicit: {
on: this.on,
setOn: this.setOn,
setOff: this.setOff,
toggle: this.toggle
}
});
}
setOn() {
this.on = true;
}
setOff() {
this.on = false;
}
toggle() {
this.on != this.on;
}
}
appToggle 디렉티브는 내부에 Toggle상태와 상태를 제어하는 함수를 가지고 있고 이를 context를 통해서 외부에 노출하여 어떤 컴포넌트에서도 재사용 가능하도록 구성되어져있다.
Directive로 로직을 분리하였기에 재사용성이 급격하게 높아졌고 부모,자식 컴포넌트를 연결하는 코드도 줄일 수 있어 가독성 또한 좋아졌다.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-use-mouse',
template: `
<div appMouse #mouse="appMouseF">
<p>The mouse position is {{ mouse.state.x }}, {{ mouse.state.y }}</p>
</div>
`
})
export class UseMouseComponent {}
2번째 케이스는 DOM element에 appMouse 디렉티브를 붙여 현재 마우스 위치값을 화면에 실시간으로 뿌리는 케이스이다.
// directive reference = directive exportAs
#mouse = "appMouseF"
디렉티브 appMouseF를 mouse로 참조하고 있는데 appMouseF는 appMouse directive의 새로운 이름이라고 보면 된다.
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[appMouse]',
exportAs: 'appMouseF' // 이 디렉티브를 사용하는 컴포넌트는 디렉티브를 appMouseF로 참조할 수 있다.
})
export class MouseDirective {
private _state = {
x: 0,
y: 0
};
get state() {
return this._state;
}
constructor() {}
@HostListener('mousemove', ['$event'])
handleMouseMove(event: MouseEvent) {
this._state = {
x: event.clientX,
y: event.clientY
};
}
}
exportAs를 눈여겨 보아야하는데. exportAs로 내보내는 값으로 컴포넌트에서 참조하여 해당 디렉티브에 접근하는것이 가능하다.
꽤 신박한 방법이지만 사실 상태는 @Input으로 받고 event는 @Output으로 핸들링하는것이 제일이다.
Case1 같은 경우 Presenter Component에 Directive로 직접적으로 선언되어져있기 때문에 다른 컴포넌트에서 재활용하는것이 어려워졌기 때문이다.
데이터를 주고 받는것은 전통적인 Presenter & Container 패턴을 따르는게 가장 좋다! ( 조금 귀찮더라도 ! )
또 나는 Directive는 DOM Element를 제어할때만 사용해야 한다고 알았는데 이 글을 보고 생각이 달라졌다.
Directive를 사용하면 기능들을 쉽게 컴포넌트에서 분리하여 재사용 가능하도록 만들 수 있다.
최근 Angular RoadMap에 Host Component에도 Directive를 붙여 Composition 효과를 누릴 수 있게 지원한다는 항목이 추가돼었는데
이 기능까지 지원되면 정말 Directive로 놀라운 것들을 할 수 잇을거라고 생각한다.