npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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-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="[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 설치 및 설정

  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="[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 아키텍처를 구현할 수 있습니다:

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

라이선스

MIT