WebComponent
외부 React 또는 Vue 컴포넌트를 Shadow DOM에 로드하여 렌더링하는 컴포넌트
개요
Cals WebComponent는 외부에서 빌드된 React 또는 Vue 컴포넌트 번들을 Shadow DOM 내에 로드·마운트합니다. 번들의 export를 분석하여 프레임워크를 자동 감지하므로, 프레임워크 타입을 명시할 필요가 없습니다. 로드·마운트 구현은 @cals/extension-plugin-react의 useBaseWebComponent입니다.
주요 특징
- ✅ 프레임워크 자동 감지: 번들이
createRoot·createElement를 export하면 번들의 React,createApp을 export하면 Vue, 둘 다 없으면 호스트 React로 마운트 - ✅ Shadow DOM 격리: 스타일 규칙은 서로 넘나들지 않음. CSS 변수 토큰은 들어감 (토큰)
- ✅ 동적 Props 동기화:
componentProps변경 시 React는 재마운트 없이 re-render, Vue는 앱을 새로 마운트 - ✅ 자동 스타일 주입: 외부 번들의
config.js+assets/style.css자동 로드 - ✅ depth 지원: 번들이
depth를 export하면 해당 값만큼 상위 디렉터리를 baseURL로 사용 (Extension-Plugin 호환) - ✅ 라이프사이클 관리: 마운트/언마운트 시 React root / Vue app 자동 정리
- ✅ children 지원: Shadow DOM 컨테이너 바로 아래에 추가 UI 렌더링 가능
기본 사용
src로 외부 컴포넌트 번들 URL을 지정합니다. 프레임워크(React/Vue)는 자동 감지됩니다.
Preview
WebComponent 렌더 영역 (외부 번들 필요)
동적 Props 전달
componentProps가 변경되면 React 번들은 재마운트하지 않고 re-render합니다. Vue 번들은 앱을 언마운트한 뒤 새로 마운트합니다. 1단계 얕은 비교로 값이 같으면 아무것도 하지 않습니다.
외부 컴포넌트는 componentProps에 elRef(Shadow DOM을 붙인 컨테이너 요소)가 더해진 props를 받습니다.
Code
const [theme, setTheme] = useState("light")
<WebComponent
src="https://cdn.example.com/extension/Dashboard.js"
componentProps={{ theme, screenId: "SCR-001", token: accessToken }}
height={400}
/>오버레이 (children)
children은 Shadow DOM 컨테이너 다음 형제로 렌더링됩니다. WebComponent는 위치를 지정하지 않으므로, 겹쳐 놓으려면 className으로 기준 위치를 줍니다.
Code
<WebComponent
src="https://cdn.example.com/extension/Widget.js"
height={250}
className="relative"
>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<span className="text-white">로딩 중...</span>
</div>
</WebComponent>useWebComponent Hook
WebComponent 컴포넌트 대신 훅을 직접 사용할 수도 있습니다.
import { useWebComponent } from "@vortex/ui-cals";
import { useRef } from "react";
function CustomContainer() {
const containerRef = useRef<HTMLDivElement>(null);
useWebComponent(containerRef, {
src: "/extension/MyWidget.js",
componentProps: { theme: "dark" },
mode: "open",
});
return <div ref={containerRef} style={{ height: 400 }} />;
}API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | (필수) | 외부 컴포넌트 번들의 전체 URL |
componentProps | Record<string, any> | {} | 컴포넌트에 전달할 props 객체 |
mode | "open" | "closed" | "open" | Shadow DOM 모드 |
tokenBridge | boolean | true | 호스트 [data-token] 토큰을 Shadow DOM으로 복사 |
loader | (url: string) => Promise<any> | 동적 import() | 번들·config.js 로더 |
height | string | number | - | Shadow DOM 컨테이너 높이 |
className | string | - | 외부 컨테이너 CSS 클래스 |
style | CSSProperties | - | 외부 컨테이너 인라인 스타일 |
children | ReactNode | - | 오버레이 등 추가 렌더 콘텐츠 |
useWebComponent Options
useWebComponent(containerRef, options)의 containerRef는 RefObject<HTMLElement | null>입니다.
| Option | Type | Default | Description |
|---|---|---|---|
src | string | (필수) | 외부 번들 URL |
componentProps | Record<string, any> | {} | 컴포넌트에 전달할 props |
mode | "open" | "closed" | "open" | Shadow DOM 모드 |
tokenBridge | boolean | true | 호스트 [data-token] 토큰 복사 |
loader | (url: string) => Promise<any> | 동적 import() | 번들·config.js 로더 |
projectId | string | "ext" | externalCss <style> id 접두어 |
mirrorHostTokens
@cals/extension-plugin-react의 mirrorHostTokens(shadowRoot): () => void를 그대로 다시 내보냅니다. WebComponent는 tokenBridge로 자동 호출하므로 직접 부를 일은 Shadow DOM을 직접 만드는 경우뿐입니다. 반환값은 복사를 멈추는 정리 함수입니다.
토큰
:root전역 토큰은 상속으로 Shadow DOM 안에 들어갑니다.[data-token]범위 토큰은tokenBridge(기본 켜짐)가 Shadow DOM 안으로 복사합니다. 효과는 외부 번들 화면에 같은data-token요소가 있을 때만 납니다..dark [data-token="x"]처럼 Shadow DOM 밖 조상과 결합한 규칙은 Shadow DOM 안에서 맞지 않습니다.@media·@layer안의[data-token]규칙은 조건 없이 적용됩니다.
외부 번들 규약
React 번들
React 컴포넌트를 default export합니다. createRoot·createElement도 export하면 호스트 React 대신 번들의 React로 마운트합니다(Extension-Plugin 규약).
// 번들 파일 (예: MyWidget.js)
export default function MyWidget(props: Record<string, unknown>) {
return <div>...</div>;
}Vue 번들 (Extension-Plugin)
@cals/extension-plugin의 calsExPlugin()으로 빌드된 번들은 자동으로 호환됩니다.
번들은 createApp, ref를 named export하며, CalsWrapper가 내장되어 있어
componentProps로 전달한 CALS 컨텍스트(CalsSdk, emitter, token 등)가 Vue provide/inject로 자동 배포됩니다.
// Extension-Plugin이 자동 생성하는 번들 구조
export { createApp, ref } from 'vue'
export default h(CalsWrapper, ...) // CalsWrapper가 props를 provide호스트에서 전달해야 하는 componentProps:
| Prop | Type | Description |
|---|---|---|
calsSdk | CalsSdk | 호스트 앱 API (Alert, Navigate 등) |
emitter | Emitter | mitt 기반 이벤트 버스 |
token | string | 인증 토큰 |
screenId | string | 화면 ID |
componentId | string | 컴포넌트 ID |
theme | string | 테마 (light/dark) |
lang | string | 언어 (ko/en) |
createApp이 export에 존재하면 자동으로 Vue로 감지됩니다.
componentProps가 바뀌면 앱을 언마운트한 뒤 새 props로 다시 마운트합니다.
Vue 번들 (직접 빌드)
Extension-Plugin 없이 직접 빌드한 Vue 번들도 지원됩니다:
// 번들 파일 (예: MyWidget.js)
import MyComponent from "./MyComponent.vue";
export { createApp, ref } from "vue";
export default MyComponent;선택적으로 동일 baseURL에 다음 파일을 배치할 수 있습니다:
| 파일 | 역할 |
|---|---|
config.js | globalCss 배열은 assets/style.css 앞에 붙여 Shadow DOM에, externalCss 배열은 document.head에 주입 |
assets/style.css | Shadow DOM 내에 주입할 스타일시트 |
depth (baseURL 보정)
번들이 depth를 named export하면, 해당 값만큼 상위 디렉터리를 baseURL로 사용합니다.
예를 들어 depth = 1이고 src = "https://cdn.example.com/plugins/v1/MyWidget.js"이면,
baseURL은 https://cdn.example.com/plugins가 됩니다 (기본값 0이면 https://cdn.example.com/plugins/v1).
이는 Extension-Plugin의 calsExPlugin() 빌드 구조와 호환됩니다.
// 번들 파일에서 depth export (선택)
export const depth = 1; // config.js가 1단계 상위 디렉터리에 위치접근성
권장 사항
- ✅ Shadow DOM 내부의 포커스 가능한 요소는 문서 순서에 자연스럽게 통합됩니다.
- ✅ 외부 컴포넌트 내에서 ARIA 속성을 적절히 사용하도록 번들 개발 시 고려합니다.
- ✅ 로딩 상태를
children오버레이로 제공할 때aria-live="polite"를 고려합니다.
관련 컴포넌트
- Iframe: 외부 웹 페이지를 iframe으로 임베드