1.0.11 • Published 1 year ago

@makecode/component-manager v1.0.11

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

@makecode/component-manager

@makecode/component-manager는 커스텀 태그를 기반으로, 동적으로 커스텀 엘리먼트 스크립트를 로드하고 정의할 수 있는 라이브러리입니다. Micro-Frontend 아키텍처를 지원하며, 다양한 컴포넌트를 손쉽게 동적으로 로드하고 관리할 수 있습니다.


주요 기능

  • 커스텀 엘리먼트 동적 로드
    • HTML 속성 기반으로 필요한 컴포넌트 script 를 동적으로 로드합니다.
    • 컴포넌트에 필요한 style 를 로드합니다.
  • 버전 관리
    • define 속성에 tagName@version 형식을 사용하여 특정 버전의 컴포넌트를 로드할 수 있습니다. (서버에 관련 코드 구현 필요)
  • Micro-Frontend 지원
    • 독립적으로 배포된 컴포넌트를 동적으로 로드하여 마이크로프론트엔드 구조를 구축할 수 있습니다.
  • 중복 로드 방지
    • 이미 로드되었거나 로드 중인 컴포넌트 script 는 다시 로드(define)하지 않습니다.
  • HTML data-* 속성 동기화
    • layout-blockdata-* 속성을 로드된 커스텀 엘리먼트에 자동으로 동기화합니다.
  • Shadow DOM 지원
    • shadow 속성을 통해 Shadow DOM을 설정할 수 있습니다.
  • 최신 ES2015 브라우저 호환
    • 최신 브라우저 환경에서 안정적으로 작동하며, Vite 빌드 도구를 사용하여 배포 가능합니다.

설치

npm install @makecode/component-manager

사용법

1. 커스텀 엘리먼트 정의

React 기반 커스텀 엘리먼트 정의

기본적인 단일 컴포넌트 정의 방법

import React from 'react';
import ReactDOM from 'react-dom/client';

function ReactComponent({ name }) {
  return <div>Hello from React, {name}!</div>;
}

class ReactCustomElement extends HTMLElement {
  connectedCallback() {
    const name = this.getAttribute('data-name') || 'React';
    //ReactDOM.render(<ReactComponent name={name} />, this);
    ReactDOM.createRoot(this).render(<ReactComponent name={name} />);
  }
}
if (!customElements.get('react-custom-element')) {
  customElements.define('react-custom-element', ReactCustomElement);
}

여러 컴포넌트 정의 방법

import React from 'react';
import ReactDOM from 'react-dom/client';

// 첫 번째 React 컴포넌트
function FirstComponent({ name }) {
  return <div>Hello from First Component, {name}!</div>;
}

// 두 번째 React 컴포넌트
function SecondComponent({ name }) {
  return <div>Welcome to Second Component, {name}!</div>;
}

// 커스텀 엘리먼트 등록 함수
function defineReactCustomElement(tagName, Component) {
  class ReactCustomElement extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute('data-name') || 'React';
      //ReactDOM.render(<Component name={name} />, this);
      ReactDOM.createRoot(this).render(<Component name={name} />);
    }
  }

  if (!customElements.get(tagName)) {
    customElements.define(tagName, ReactCustomElement);
  }
}

// 여러 커스텀 엘리먼트 등록
defineReactCustomElement('first-component', FirstComponent);
defineReactCustomElement('second-component', SecondComponent);

Vue 기반 커스텀 엘리먼트 정의

Vue 3.x 기반 컴포넌트 정의

import { createApp, defineComponent, h } from 'vue';

// 첫 번째 Vue 컴포넌트
const FirstComponent = defineComponent({
  props: ['name'],
  render() {
    return h(
      'div',
      {},
      `Hello from Vue 3 First Component, ${this.name || 'Vue 3'}!`,
    );
  },
});

// 두 번째 Vue 컴포넌트
const SecondComponent = defineComponent({
  props: ['name'],
  render() {
    return h(
      'div',
      {},
      `Welcome to Vue 3 Second Component, ${this.name || 'Vue 3'}!`,
    );
  },
});

// 커스텀 엘리먼트 등록 함수
function defineVue3CustomElement(tagName, Component) {
  class Vue3CustomElement extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute('data-name') || 'Vue';
      const app = createApp(Component, { name });
      app.mount(this);
    }
  }

  if (!customElements.get(tagName)) {
    customElements.define(tagName, Vue3CustomElement);
  }
}

// 여러 커스텀 엘리먼트 등록
defineVue3CustomElement('vue3-first-component', FirstComponent);
defineVue3CustomElement('vue3-second-component', SecondComponent);

Vue 2.x 기반 컴포넌트 정의

import Vue from 'vue';

// 첫 번째 Vue 컴포넌트
const FirstComponent = Vue.extend({
  props: ['name'],
  render(h) {
    return h(
      'div',
      `Hello from Vue 2 First Component, ${this.name || 'Vue 2'}!`,
    );
  },
});

// 두 번째 Vue 컴포넌트
const SecondComponent = Vue.extend({
  props: ['name'],
  render(h) {
    return h(
      'div',
      `Welcome to Vue 2 Second Component, ${this.name || 'Vue 2'}!`,
    );
  },
});

// 커스텀 엘리먼트 등록 함수
function defineVue2CustomElement(tagName, Component) {
  class Vue2CustomElement extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute('data-name') || 'Vue';
      this._instance = new Component({
        propsData: { name }, // props 값을 인스턴스에 전달
      }).$mount(this);
    }

    disconnectedCallback() {
      if (this._instance) {
        this._instance.$destroy();
        this._instance = null;
      }
    }
  }

  if (!customElements.get(tagName)) {
    customElements.define(tagName, Vue2CustomElement);
  }
}

// 여러 커스텀 엘리먼트 등록
defineVue2CustomElement('vue2-first-component', FirstComponent);
defineVue2CustomElement('vue2-second-component', SecondComponent);

Vue 2.x 싱글 파일 컴포넌트 방식으로 여러 커스텀 엘리먼트 정의

//store.js에서 Vuex 스토어를 생성
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    },
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    },
  },
  getters: {
    getCount: state => state.count,
  },
});

// 또는 모듈
// const moduleA = {
//   state: { count: 0 },
//   mutations: {
//     increment(state) {
//       state.count++;
//     },
//   },
//   getters: { getCount: state => state.count },
// };
// const store = new Vuex.Store({
//   modules: { moduleA },
// });
<template>
  <div>
    <h3>First Component</h3>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
// FirstComponent.vue
export default {
  computed: {
    count() {
      return this.$store.getters.getCount;
    },
  },
  methods: {
    increment() {
      this.$store.commit('increment');
    },
  },
};
</script>
<template>
  <div>
    <h3>Second Component</h3>
    <p>Count: {{ count }}</p>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script>
// SecondComponent.vue
export default {
  computed: {
    count() {
      return this.$store.getters.getCount;
    },
  },
  methods: {
    decrement() {
      this.$store.commit('decrement');
    },
  },
};
</script>
import Vue from 'vue';
import { store } from './store';
import FirstComponent from './FirstComponent.vue';
import SecondComponent from './SecondComponent.vue';

// 커스텀 엘리먼트 등록 함수
function defineCustomElement(tagName, Component) {
  class VueCustomElement extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute('data-name') || 'Vue';
      new Vue({
        store, // Vuex 스토어 주입
        render: h =>
          h(Component, {
            props: {
              name,
            },
          }),
      }).$mount(this);
    }
  }

  if (!customElements.get(tagName)) {
    customElements.define(tagName, VueCustomElement);
  }
}

// 컴포넌트 등록
defineCustomElement('first-component', FirstComponent);
defineCustomElement('second-component', SecondComponent);

2. HTML에서 사용

기본 사용 예제

<!-- React 커스텀 엘리먼트 -->
<layout-block
  define="react-custom-element"
  src="https://cdn.test.com/react.bundle.js"
></layout-block>
<layout-block
  define="react-custom-element"
  src="http://localhost:4001/index.js"
  stylesheet="http://localhost:4001/assets/main.css"
  shadow="{ 'mode': 'closed', 'delegatesFocus': true }"
  data-name="Test React"
  >TEST</layout-block
>

<!-- Vue 커스텀 엘리먼트 -->
<layout-block
  define="vue-custom-element"
  src="https://cdn.test.com/vue.bundle.js"
></layout-block>
<layout-block
  define="vue2-custom-element"
  src="http://localhost:4002/index.js"
  data-name="Test Vue"
  lazy="true"
></layout-block>

Micro-Frontend 사용 예제

<!-- Micro-Frontend Components -->
<layout-block
  define="header-module"
  src="https://cdn.test.com/api/v1/header?test=true"
></layout-block>
<layout-block
  define="product-list-module"
  src="https://cdn.test.com/api/v1/product-list?test=true"
></layout-block>
<layout-block
  define="footer-module@1.0.0"
  src="https://cdn.test.com/api/v1/footer?test=true"
></layout-block>

3. JavaScript에서 사용

Import 방식

import '@makecode/component-manager';

속성 변경으로 컴포넌트 변경

const customTag = document.querySelector('layout-block');
customTag.setAttribute('src', 'https://cdn.example.com/api/v1/script'); // src 먼저 변경
customTag.setAttribute('define', 'example-component@1.2.0'); // 컴포넌트 로드 실행
customTag.setAttribute('data-title', 'Dynamic Title'); // data-* 속성 변경
document.body.appendChild(customTag);

4. Vite 빌드 도구와 함께 사용

@makecode/component-manager는 최신 브라우저 환경을 대상으로 설계되었습니다. Vite 빌드 도구를 사용하여 컴포넌트를 손쉽게 번들링할 수 있습니다.

Vite 설치 및 설정

  1. Vite 설치
npm install --save-dev vite
  1. vite.config.js 파일 생성
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    target: 'es2015', // 커스텀 엘리먼트 호환성 확보
  },
});
  1. Vite 빌드 실행
npx vite build
  1. dist 폴더에서 생성된 번들 파일을 배포합니다.

Shadow DOM 사용 예제

layout-block에 shadow 속성을 추가하면 Shadow DOM을 사용하여 커스텀 엘리먼트를 격리된 DOM 영역에 렌더링할 수 있습니다.

Shadow DOM을 open 모드로 설정:

<layout-block
  define="example-component"
  src="https://cdn.test.com/test.js"
  shadow="open"
></layout-block>

Shadow DOM이 closed 모드인 경우:

<layout-block
  define="example-component"
  src="https://cdn.test.com/test.js"
  shadow="closed"
></layout-block>

Shadow DOM 옵션을 JSON 형식으로 설정할 수 있습니다.

<layout-block
  define="example-component"
  src="https://cdn.test.com/test.js"
  shadow='{"mode": "open", "delegatesFocus": true}'
></layout-block>

shadow 속성을 변경하면 새롭게 Shadow DOM을 생성하지 않으며, 이미 생성된 경우 경고 메시지가 표시됩니다.

<layout-block
  define="example-component"
  src="https://cdn.test.com/test.js"
  shadow="open"
></layout-block>

<script>
  const customTag = document.querySelector('layout-block');

  // 이미 Shadow DOM이 attach되었기 때문에 아래 변경은 무시됩니다.
  customTag.setAttribute('shadow', 'closed');
</script>

Shadow DOM 활용 시 주의사항

  • Shadow DOM은 한 번 생성되면 제거하거나 재생성할 수 없습니다.
  • shadow 속성이 설정되지 않으면 Shadow DOM 없이 커스텀 엘리먼트가 DOM에 추가됩니다.
  • Shadow DOM을 사용하면 CSS 격리가 가능하며, 컴포넌트 스타일링에 독립성을 제공합니다.

stylesheet 속성 사용 예제

stylesheet 속성을 사용하면 동적으로 로드된 커스텀 엘리먼트에 외부 CSS 파일을 연결할 수 있습니다.

<layout-block
  define="example-component"
  src="https://cdn.example.com/api/v1/component"
  stylesheet="https://cdn.example.com/styles.css"
></layout-block>

주의사항

  • stylesheet 속성은 반드시 CSS 파일의 URL을 가리켜야 합니다.
  • 로드 실패 시 브라우저의 onerror 이벤트를 통해 에러가 로깅됩니다.
  • stylesheet 속성은 Shadow DOM과 일반 DOM 모두에서 사용 가능합니다. Shadow DOM을 사용하는 경우, CSS는 Shadow DOM 내부에 적용됩니다.

API

Layout-Block 속성

속성 이름설명예제
define로드할 컴포넌트의 태그 이름. tagName@version 형식을 사용할 수 있습니다.<layout-block define="example-component@1.0.0"></layout-block>
src컴포넌트를 로드할 Script SRC 입니다.<layout-block define="example-component" src="https://cdn.example.com/api/v1/component">
shadowShadow DOM을 사용할지 여부를 설정합니다. open, closed, JSON 형식 가능.<layout-block shadow="open"></layout-block>
stylesheet커스텀 엘리먼트에 연결할 외부 CSS 파일 경로를 설정합니다. (해당 속성 사용할 경우, shadow 속성과 함께 사용 추천)<layout-block define="example-component" stylesheet="https://cdn.example.com/styles.css">
lazy화면에 보이는 시점에 렌더링 합니다.<layout-block define="example-component" lazy="true">
data-*사용자 지정 데이터 속성으로, 동적으로 로드된 커스텀 엘리먼트에 전달됩니다.<layout-block define="example-component" data-title="Dynamic Title"></layout-block>

서버 설정 예제

아래는 Express를 사용하여 컴포넌트를 제공하는 예제 서버 코드입니다.

const express = require('express');
const path = require('path');
const app = express();

const PORT = process.env.PORT || 3000;
const MODULES_DIR = path.resolve(__dirname, 'modules');
const allowedModules = {
  'header-module': path.join(MODULES_DIR, 'header.js'),
  'product-list-module': path.join(MODULES_DIR, 'product-list.js'),
  'footer-module': path.join(MODULES_DIR, 'footer.js'),
  'footer-module@1.0.0': path.join(MODULES_DIR, 'footer.js'),
};

app.get('/api/component', (req, res) => {
  const { define } = req.query;

  if (!define) {
    res.status(400).send('Component query parameter is required.');
    return;
  }

  const [tagName, version] = define.includes('@')
    ? define.split('@')
    : [define, undefined];
  const filePath = allowedModules[define] || allowedModules[tagName];
  if (filePath) {
    res.sendFile(filePath, err => {
      if (err) {
        console.error(`Failed to send module "${define}":`, err);
        res.status(500).send('Internal Server Error.');
      }
    });
  } else {
    res.status(404).send('Module not found.');
  }
});

app.use(express.static('public', { maxAge: '1d' }));

app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`);
});

Micro-Frontend 아키텍처 지원

@makecode/component-manager를 사용하면 독립적으로 배포된 컴포넌트를 손쉽게 로드하고 관리할 수 있습니다. 이를 통해 다음과 같은 Micro-Frontend 아키텍처를 구현할 수 있습니다:

  1. 모듈별 독립 배포
    • 각 컴포넌트를 독립적으로 개발하고 CDN 또는 서버에 배포.
  2. 동적 로드 및 통합
    • layout-block 태그를 사용하여 필요한 컴포넌트를 동적으로 로드하고 DOM에 통합.
  3. 버전 관리
    • define 속성을 통해 원하는 버전의 컴포넌트를 선택적으로 로드.

라이선스

MIT

1.0.11

1 year ago

1.0.10

1 year ago

1.0.9

1 year ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago