@makecode/component-manager
v1.0.10
Published
마이크로프론트 컴포넌트 로더
Downloads
646
Readme
@makecode/component-manager
@makecode/component-manager는 커스텀 태그를 기반으로, 동적으로 커스텀 엘리먼트 스크립트를 로드하고 정의할 수 있는 라이브러리입니다. Micro-Frontend 아키텍처를 지원하며, 다양한 컴포넌트를 손쉽게 동적으로 로드하고 관리할 수 있습니다.
주요 기능
- 커스텀 엘리먼트 동적 로드
- HTML 속성 기반으로 필요한 컴포넌트 script 를 동적으로 로드합니다.
- 컴포넌트에 필요한 style 를 로드합니다.
- 버전 관리
define
속성에tagName@version
형식을 사용하여 특정 버전의 컴포넌트를 로드할 수 있습니다. (서버에 관련 코드 구현 필요)
- Micro-Frontend 지원
- 독립적으로 배포된 컴포넌트를 동적으로 로드하여 마이크로프론트엔드 구조를 구축할 수 있습니다.
- 중복 로드 방지
- 이미 로드되었거나 로드 중인 컴포넌트 script 는 다시 로드(define)하지 않습니다.
- HTML
data-*
속성 동기화layout-block
의data-*
속성을 로드된 커스텀 엘리먼트에 자동으로 동기화합니다.
- 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="[email protected]"
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', '[email protected]'); // 컴포넌트 로드 실행
customTag.setAttribute('data-title', 'Dynamic Title'); // data-* 속성 변경
document.body.appendChild(customTag);
4. Vite 빌드 도구와 함께 사용
@makecode/component-manager
는 최신 브라우저 환경을 대상으로 설계되었습니다. Vite 빌드 도구를 사용하여 컴포넌트를 손쉽게 번들링할 수 있습니다.
Vite 설치 및 설정
- Vite 설치
npm install --save-dev vite
vite.config.js
파일 생성
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'es2015', // 커스텀 엘리먼트 호환성 확보
},
});
- Vite 빌드 실행
npx vite build
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="[email protected]"></layout-block>
|
| src
| 컴포넌트를 로드할 Script SRC 입니다. | <layout-block define="example-component" src="https://cdn.example.com/api/v1/component">
|
| shadow
| Shadow 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'),
'[email protected]': 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 아키텍처를 구현할 수 있습니다:
- 모듈별 독립 배포
- 각 컴포넌트를 독립적으로 개발하고 CDN 또는 서버에 배포.
- 동적 로드 및 통합
layout-block
태그를 사용하여 필요한 컴포넌트를 동적으로 로드하고 DOM에 통합.
- 버전 관리
define
속성을 통해 원하는 버전의 컴포넌트를 선택적으로 로드.
라이선스
MIT