authoring-componentslisted
Install: claude install-skill relayax/relayagent
# 컴포넌트 수출과 소비
패키지 화면의 한 조각을 다른 패키지가 쓰게 하는 축이다. 결론부터 말하면 **수출물은 자립
ESM 번들 하나이고, 소비자는 프레임워크도 빌드도 필요 없다.**
## 계약
수출은 파일 하나로 좁혀진다. 산출 디렉토리의 `index.js` 가 진입점이고, 수출은 하나다:
```js
export function mount(el, props) {
// el 안에 그린다
return { unmount() { /* 정리 */ } };
}
```
**스타일도 이 파일 안에 탄다.** 옆에 CSS 를 따로 내지 마라 — 소비자가 그 주소를 알아야 하는데
주소에는 제공자의 설치 이름이 들어간다(`/pkg/<설치이름>/components/index.css`). 같은 패키지가
다른 이름으로 서면 바로 깨진다. 소비자가 아는 것은 이름 하나, 부르는 것은 `mount` 하나다.
## 선언
```yaml
surfaces:
components:
source: surfaces/components # 저작 트리
out: dist # 빌드 산출. 미선언이면 source/index.js 를 그대로 서빙
```
`out` 을 선언하면 설치와 발행이 그 자리에서 `npm install` + `npm run build` 를 돈다.
빌드가 실패하면 설치가 실패한다. 손으로 쓴 ESM 한 장이면 `out` 을 빼라 — 그러면 빌드가
아예 없고, 대신 `source/index.js` 가 지금 실재해야 한다(판정이 잡는다).
## React 로 저작하기
써도 된다. **React 는 번들 안에 들어가고 소비자에게는 보이지 않는다.** 채팅 위젯이 이미
그 모양이다. react 를 devDependencies 에 두고 번들러가 안으로 말아 넣게 하라:
```js
// build.mjs
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.tsx"],
bundle: true, format: "esm", outfile: "dist/index.js",
jsx: "automatic", minify: true,
loader: { ".css": "text" }, // CSS 를 문자열로 받아 번들 안에 태운다
});
```
```jsx
// src/index.tsx
import { createRoot } from "react-dom/client";
import css from "./panel.css";
import Panel from "./Panel";
let styled = false;
function ensureStyle() {
if (styled || typeof document === "undefined") return;
styled = true;
const tag = document.createElement("style");
tag.textCont