Angular Renderless Component 패턴

라코마코·2021년 9월 14일
1
post-thumbnail

Renderless Component (이하 RC라고 부르겠다) 패턴은 말 그대로 렌더링을 하지 않는 컴포넌트를 두는 패턴을 의미한다.

RC는 렌더링은 다른 컴포넌트에게 위임하고 자기 자신은 컴포넌트를 제어하는 함수, 상태를 가져 재 사용 가능하도록 View와 Business를 분리하는 패턴을 의미한다.

쉽게 말해 Container & Presenter 패턴이다.

보통 Container & Presenter 패턴은 내부에 Presenter Component를 두고 Container Component는 Presenter Component를 wrapping하여 데이터를 전달, 조작하는 방식으로 구성되어져 있다.

때문에 부모컴포넌트와 자식 컴포넌트를 연결하는 코드가 생겨 다소 지저분해질 수 있는데

Angular에서는 이런 부분을 Directive를 사용, 분리하여 깔끔하게 떼어내는것이 가능하다.

Case 1.

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로 로직을 분리하였기에 재사용성이 급격하게 높아졌고 부모,자식 컴포넌트를 연결하는 코드도 줄일 수 있어 가독성 또한 좋아졌다.

Case2.


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로 놀라운 것들을 할 수 잇을거라고 생각한다.


출처: https://netbasal.com/going-renderless-in-angular-all-of-the-functionality-none-of-the-render-1b105e001c8a

profile
여러가지 화면을 만드는걸 좋아합니다. https://codepen.io/raiden2

0개의 댓글