Alpine.js + Tailwind.css + Web Components

2022. 8. 4. 21:37Tutorial & Training/JavaScript

728x90

Alpine.js

Alpine.js는 MarkUp(HTML) Template에 작성하여 Reactivity (재활동)이 가능하게끔 하여, Vue 스러운 문법으로 가장 이상적이면서 간단하고 간결하고 쉬운 라이브러리입니다.

 

Gzip 압축 기준 무려 8KB 밖에 안되는 적은 용량으로 굉장히 강력한 기능들을 가지고 있습니다.

 

  • state Management (상태관리)
  • global Store (페이지내 전역 상태 관리)
  • Data / Binding (데이터 바인딩 및 액션)
  • teleport

 

이외에도 다양한 기능이 있으며, Vue의 문법과 유사하게 x- 라는 prefix 를 사용합니다.

x-if, x-for, x-teleport 등등

 

그런데 아쉽게도 x-if는 존재하지만 else나 else-if는 존재하지 않습니다.

 

 

그럼에도 불구하고 Alpine은 Vue CDN과 마찬가지로 이미 존재하는 동적/정적 HTML에 기능을 추가하여 SPA처럼 활동하게 만들어주며, 최초 렌더링을 직접적으로 수행하지 않기때문에 적은 용량과 빠른 수행이 가능합니다.

 

 

 

하지만 Alpine.js만 보았을때는 프론트와 풀스택 개발자 입장에서는 React, Vue, Angular, Svelte, Solid 대비 다소 약소한 강점입니다. 굳이? 이걸 써야할까? 싶을 정도지만, 백엔드 개발자 측면에서 보게된다면 어떠한 백엔드에서도 사용할 수 있기 때문에 굉장히 강력한 장점입니다.

 

 

 Alpine.js를 사용하면 문제점은 크게 3가지가 존재합니다.

  • template 문법에서 if는 존재하지만, else, else-if가 없는점
  • 재사용 컴포넌트를 사용할 수 없는 점
  • History Router를 사용하지 않는 점

 

먼저 if 관련 단점은 위에서 언급하였으니 패스하고, History Router의 경우는 사실 광고로 주 수익모델을 가져가는 웹앱 서비스들은 많이들 아시는 내용입니다.

 

구글 애드센스의 경우 자동광고는 페이지 전환시 전면 모달로 나타나는데 클릭률이나 수익률이 꽤나 높은 축에 속합니다.

그런데 이 자동광고의 전면광고는 닫기 클릭시 리다이렉트가 발생합니다. 때문에 페이지가 새로접근하게 되므로, 히스토리 라우터가 의미가 없어집니다.

이러한 이유로 히스토리 라우터를 사용하는 프론트 프레임워크 대부분은 사실 강점을 하나씩 버리고 가야합니다.

 

또한 히스토리 라우터를 사용할 경우 API로부터 데이터를 수신 해야하기 때문에, 그 대기시간동안 스켈레톤 뷰를 보여준다던지, SEO 처리를 위해 Prerender를 하던지 SSR로 만들어서 Hydration을 한다던지 등 손이 굉장히 많이 갑니다.

 

 

마지막으로 재 사용 컴포넌트입니다. Alpinejs는 MarkUp 템플릿에 사용하는 만큼 백엔드가 Template에서 재사용 가능한 컴포넌트를 만들어주지 않는 이상 사용하기가 어렵습니다.

하지만 그럼에도 불구하고 SEO가 필요없는 알림, 모달, 다이얼로그, 스낵바, 토스트, 버튼 등의 공용으로 자주쓰이는 재사용 컴포넌트는 사용하고 싶기 마련입니다.

 

그래서 이 글의 핵심은 Alpine.js와 Web Components를 합쳐서 재 사용 가능한 컴포넌트를 만드는 방법인데

또 Tailwind.css 를 사용하여야 굉장히 편리하고 협업에서 유리한 강점을 지니게 되기때문에 

Alpine.js + Tailwind.css + Web Components 를 사용하는 방법입니다.

 

 

 

여기서 짚고 넘어가야할 핵심은 Lit Element나 Light DOM 등을 사용하지 않는 이유가 될 수 있는데, 그 두가지를 사용하면 Svelte Solid 와 똑같습니다. 즉 Alpine.js를 사용하는 의미가 퇴색되기도하고, 라이브러리 의존성이 너무 짙어져서 사용하지 않았습니다.

 

 

 

 

 

Tailwind.css

Web Components를 사용하면 Slot을 사용하고 싶어지는데, Slot 을 사용하려면 Shadow DOM이 필연적으로 사용하게 됩니다. 그런데 Shadow DOM을 사용하면 DOM이 격리되기 때문에 Style Sheet를 사용할 수 없게 되죠.

 

그러면 일일이 Tailwind 클래스의 Key values를 찾아서 넣어줘야 하는 치명적인 문제가 생깁니다.

이때 이걸 해결해줄 수 있으며, 백엔드 개발자 칭구들이 좋아하시는 CDN도 가능한 라이브러리가 있습니다.

 

Twind 라고 하는데 요친구가 아주 괴물입니다.

twind.js는 동적으로 추가되는 tailwind 클래스를 파악해서 스타일시트를 만들고 추가해줄 수 있는 강력한 기능을 가지고 있습니다.

 

Alpine.js 보다는 쬐끔 큰 용량을 지녀서 gzip 압축 기준 최대 13KB를 사용합니다. 두친구를 합쳐도 21KB기 때문에 Vue 보다 적은 용량을 사용합니다.

 

 

Tailwind도 마찬가지로 사실 SFC(Single File Component) 기반의 코드 작성방식을 사용하는 Vue, Svelte의 경우 굳이 이걸 써야해? 라는 의문이 들 수 있는데, Alpine.js와 조합하면 엄청난 시너지가 됩니다.

 

 

간단한 서비스의 경우는 HTML에다가 다 때려박아서 코드를 작성할 수 있으니까요!

 

 

 

 

결과

See the Pen Alpine.js + Tailwind .css (Twind CDN) => Modal by GM Yankee (@GMyankee) on CodePen.

이렇게 JS에 보시면 제가 Component 클래스를 작성해두었습니다.

 

import { create, cssomSheet } from 'https://cdn.skypack.dev/twind';

const sheet = cssomSheet({ target: new CSSStyleSheet() });
const { tw } = create({ sheet });

class Component extends HTMLElement {
  tw;
  shadow;
  props = {};

  constructor() {
    super();
    this.tw = tw;
    this.shadow = this.attachShadow({ mode: 'open' });
    this.shadow.adoptedStyleSheets = [sheet.target];
    
    // props init
    const attributes = this.getAttributeNames();
    attributes.forEach((attr) => {
      this.props[attr] = this.getAttribute(attr);
    });

    // render
    this.shadow.innerHTML = this.render();
  }

  connectedCallback() {
    window.Alpine.initTree(this.shadowRoot);
  }
  
  render() {
    const template = this.template();
    const parser = new DOMParser();
    const doc = parser.parseFromString(template, 'text/html');
    const classes = [...doc.getElementsByTagName('*')]
      .map((x) => x.getAttribute('class'))
      .join(' ')
      .trim();

    this.tw(classes);
    return template;
  }

  template() {
    return ``; // inherit
  }
}

render 함수에서 DOM Parser를 이용해서 모든 태그의 class 를 추출해서 Twind를 통해 한번에 등록합니다.

 

호출하는 쪽에서는 단지 클래스를 상속받고 JSX를 작성하듯이, template() 함수만 template iteral로 원하는 HTML을 작성해주기만 하면 끝입니다!

 

 

 

728x90