: 이제 실제 앱을 웹 컴포넌트로 만들어 보면 더 좋을것 같아 해보려고 한다.
처음 부터 시작하는것 보다는 이미 만들어져 있는 앱을 변경하며 차이점을 보는것이 좋아 보여 아래와 같은 간단한 그림판 도구를 바꿔 보려한다.
먼저 위 그림의 html 모양은 이런식이다.
<div id="app">
<canvas id="canvas"></canvas>
<div class="tools">
<button type="button" class="button undo">Undo</button>
<button type="button" class="button clear">Clear</button>
<div class="color-field red"></div>
<div class="color-field blue"></div>
<div class="color-field green"></div>
<div class="color-field yellow"></div>
<input type="color" class="color-picker" />
<input type="range" min="1" max="100" class="pen-thickness" />
</div>
</div>
먼저 크게 canvas영역, tools영역이 상, 하단으로 크게 나뉘어 있고 tools 영역에 조작을 위한 컴포넌트들로 구분 할 수 있다.
그럼 컴포넌트를 나눠보자.
전체 적으로 보면 해당 앱은 canvas, tools 두가지로 나눠도 무방 하나 더 잘게 나눠 본다는 관점에서 위와 같이 나눠 볼 수 있을것 같다.
: 우리가 웹 컴포넌트를 만드는데 추상화 가능한 부분들이 있다.
위 항목들을 근거로 추상화 해보면 아래 코드와 비슷할 것 같다.
export default class Component extends HTMLElement {
private _shadowRoot: ShadowRoot;
private _template: string;
private _styles: string;
private _templateElement: HTMLTemplateElement;
constructor(template = "", styles = "") {
super();
this._template = styles;
this._styles = template;
// template 생성 하고 인자로 넘어온 template, styles들을 render 메소드에서 조합한다.
this._templateElement = document.createElement("template");
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
connectedCallback() {}
disconnectedCallback() {}
static get observedAttributes() {
return [];
}
attributeChangedCallback(name, oldValue, newValue) {}
get shadowRoot() {
return this._shadowRoot;
}
set shadowRoot(value: ShadowRoot) {
this._shadowRoot = value;
}
get template() {
return this._template;
}
set template(value: string) {
this._template = value;
}
get styles() {
return this._styles;
}
set styles(value: string) {
this._styles = value;
}
render() {
this._templateElement.innerHTML = `${this.template}${this.styles}`;
// 코드처럼 사용하기도 하지만 재사용을 위해서는
// this._templateElement.content.cloneNode(true) 하여 사용하기도 한다.
this.shadowRoot.appendChild(this._templateElement.content);
}
}
모든 컴포넌트에 Shadow Root
를 생성하게 되는데 사실 그럴 필요는 없다. Component
를 상속하는 ShadowComponent
컴포넌트를 별도로 생성해 주는 편이 더 유연하게 사용하기 좋아 보인다.
: 위 Component
를 상속하여 DrawingCanvas
를 만든 다면 아래와 같은 코드가 될것이다.
import Component from "./component";
const styles = `
<style>
canvas {
box-shadow: -3px 2px 9px 6px black;
}
</style>`;
const tpl = `<canvas id='canvas'></canvas>`;
export default class DrawingCanvas extends Component {
private _canvas: HTMLCanvasElement | null = null;
private _ctx: CanvasRenderingContext2D | null = null;
constructor() {
super(tpl, styles);
this.init();
}
init() {
this._canvas = this.shadowRoot.getElementById(
"canvas"
) as HTMLCanvasElement;
this._canvas.width = window.innerWidth - 60;
this._canvas.height = window.innerHeight / 1.7;
this._ctx = this._canvas.getContext("2d") as CanvasRenderingContext2D;
this._ctx.fillStyle = "white";
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
실제style, tpl
을 super의 인수로 함께 넘겨주어 부모 오브젝트에서 생성한 template
element에 포함 시킨다. 그러면 render를 통해 <canvas></canvas>
와 <style></style>
이 붙게 된다.
사용할때는 아래 처럼 DOM에 바로 붙여 사용한다.
const app = document.getElementById("app");
if (app) {
app.innerHTML = `
<drawing-canvas></drawing-canvas>
`;
}
여기서 drawing-canvas를 사용하려면 customElements.define
함수를 이용해 등록을 해야 한다. 그렇기 때문에 registComponents를 만들어서 해주면 구현
, 등록
의 관심사가 분리되어 관리가 좋을것 같다.
import ColorButton from "./colorButton";
import DrawingCanvas from "./drawingCanvas";
export function registComponents() {
customElements.define("color-button", ColorButton);
customElements.define("drawing", DrawingCanvas);
}
/// index.ts
import { registComponents } from "./components";
// 등록 함수
registComponents();
실제 웹 컴포넌트를 DOM에 붙이면 아래와 같은 형태를 가지게 된다.
이런 식으로 Web Component
를 설계한 대로 늘려 나가면 된다.
여기서 간단하지만 주의 할점 두 가지가 있다.
-
)을 꼭 포함 해야 한다.<drawing-canvas />
이런식의 줄임 표현은 사용하면 안된다. HTML parser는 관대하여 생략된 표현은 알아서 처리 해주지만 웹 컴포넌트에 대해서는 아니다. 만약 아래와 같이 표현 한다면 결과가 특이하게 된다.if (app) {
app.innerHTML = `
<color-button color='red' />
<color-button color='blue' />
<color-button color='green' />
<color-button color='yellow' />
`;
}
생략 된 태그 닫힘에 대한 처리를 안해 주는 것 처럼 자식으로 표현 된다.
그렇기 때문에 표준 대로 <color-button></color-button>
처럼 명확하게 닫아 주어야 한다.
: 앱을 웹 컴포넌트를 이용해 구현 하는 작업은 React, Vue와 같은 UI도구들과 비교해 보면 어떻까? 만약 프로젝트를 진행 한다면 어떤것을 선택해야 할까? 하는 생각이 들었다.
웹 컴포넌트는 기능과 스타일을 캡슐화하여 재사용성 측면에서 생산성에 기여한다. 물론 template을 이용하기 때문에 fragment와 비슷한 역할을 하여 DOM 변경 사항을 일일이 변경하지 않고 한꺼번에 처리하여 Rendering 성능에 도움이 된다. 물론 네이티브로 동작하는 점도 성능에 도움이 될것이다. 하지만 여타 vender library or framework들 보다 좋을까 싶은 생각이 든다.
가장 불편한 점이 template literal을 이용한 html 요소 수정 삭제 부분이다. 이 부분을 편하게 사용하려면 결국 template engine이나 이에 준하는 util 함수가 필요해 보인다. 물론 선언적으로 DOM을 Render 하기 위해서 필요한 부분이라 필요한 부분이다. 다른 vender들도 선언적인 Render를 지원하고 편하게 사용하기 위한 솔루션을 제공한다. (React -> jsx, Vue -> template, directive)
추상화를 통한 상속
이나 믹스인
등을 활용 할 수있는 점을 장점으로 볼 수 있다. 물론 다른 React 같은 library도 HOC
, 컴포넌트 합성
같은 기법을 이용해 비슷한 경험을 하게 해준다.
지금 까지 경험 해 본 바로 둘은 생산성
보다는 제공하는 장점
이 다르지 않을까 싶다. 케이스로 보면 vender free 하고 컴포넌트 설계 방식의 앱을 만들고 싶다면 충분히 웹 컴포넌트로 개발 가능 할것 같다. 다만 template으로 이루어진 DOM과 수시로 변경되는 데이터와의 싱크를 맞춰야 하는 부분은 제공되지 않아 스스로 해결해야 한다. 이런 부분에 대한 솔루션을 React는 가지고 있다. 상태관리를 위한 솔루션을 제공 함으로 데이터 동기화에 대한 부분을 신경 쓰지 않고 로직에만 신경 쓰도록 해준다.
결국 제공하는 장점이 달라 보인다.
결론 적으로 내가 뭔가 만들때 웹 컴포넌트를 고려한다고 하면 많은 부분에 대한 고민이 있겠지만 적어도 아래 세가지는 먼저 고민해 볼것 같다.
- Vender free 필요성이 있는가.
- 데이터 동기화에 대한 명확한 솔루션이 있는가.
- 이벤트 처리에 대한 일관된 솔루션이 있는가.
결격 사유가 있다면 React등의 UI Library를 선택 할 것 같다.
그런 의미로 다음은 이벤트 처리에 대해 생각해 보려고 한다.