테마
시작하기 — 빈 디렉터리에서 포탈에 뜨는 BP까지
포탈 소스를 체크아웃하거나 포탈 인스턴스를 띄울 필요는 없다. 필요한 것은 Node 22 이상과 사설 레지스트리 접근 권한뿐이다.
만들 것은 화면 하나와 API 하나를 가진 hello-bp다.
1. 프로젝트 준비
bash
mkdir hello-bp && cd hello-bp
pnpm init.npmrc에 레지스트리를 지정한다. 계정도 토큰도 필요 없다 — 이 스코프는 익명 설치로 열려 있다.
ini
@starj-portal:registry=https://npm.mdpert.com/@starj-portal 스코프만 이 레지스트리를 보고, 나머지 의존성은 npmjs에서 그대로 받는다.
bash
pnpm add -D @starj-portal/bp-backend-sdk typescript esbuild
pnpm add @starj-portal/bp-frontend-sdk react react-dom2. 대상 포탈의 계약 버전 확인
무엇보다 먼저 할 일이다. 여기서 어긋나면 뒤의 모든 작업이 헛수고가 된다.
bash
node -e "import('@starj-portal/bp-backend-sdk').then(m => console.log(m.HOST_CONTRACT_VERSION, m.HOST_PROVIDED_VERSIONS))"출력된 HOST_PROVIDED_VERSIONS의 값들을 package.json에 캐럿 없이 고정한다. 이유는 host-contract.md §3.
3. 백엔드 — register() 하나
backend/src/register.ts:
ts
import type { BpHostRuntime, BpRegistration } from "@starj-portal/bp-backend-sdk";
import { Controller, Get, Inject, Query, Req, ForbiddenException } from "@nestjs/common";
const HELLO_HOST = Symbol.for("hello-bp:host");
const APP_ID = "hello:main";
@Controller("items")
class HelloController {
constructor(@Inject(HELLO_HOST) private readonly host: BpHostRuntime) {}
@Get()
async list(@Req() req: unknown, @Query("domainId") domainId: string) {
if (!(await this.host.checkAppAccess(req, domainId, APP_ID, "read"))) {
throw new ForbiddenException();
}
this.host.logger.info("목록 조회", { domainId });
return [{ id: 1, title: "첫 항목" }];
}
}
export function register(host: BpHostRuntime): BpRegistration {
return {
manifest: {
apps: [{ id: APP_ID, name: "Hello", uri: "hello", moduleCode: "HELLO" }],
menu: [{ id: "hello:menu-main", appId: APP_ID }],
permissions: ["hello:read"],
defaultAuthorities: [
{ authId: "HELLO_USER", authName: "Hello 사용자", apps: [{ appId: APP_ID, rw: 1 }] },
],
frontendDeliveryMode: "csr-widget",
},
controllers: [HelloController],
providers: [{ provide: HELLO_HOST, useValue: host }],
};
}여기 담긴 계약이 셋이다. 자세히는 backend-guide.md.
- 진입점은
register하나다. 데코레이터로 BP를 선언하지 않는다. - 권한 판정은
host.checkAppAccess()에 묻는다. 자체 판정하지 않는다. - 화면(
apps)과 메뉴 배치(menu)는 분리된 목록이다.
4. 프론트엔드 — 셸 안에 마운트되는 위젯
frontend/src/index.tsx:
tsx
import type { BpHostContext, BpMountFn } from "@starj-portal/bp-frontend-sdk";
import { createRoot } from "react-dom/client";
import { useEffect, useState } from "react";
function App({ host }: { host: BpHostContext }) {
const [items, setItems] = useState<{ id: number; title: string }[]>([]);
useEffect(() => {
// apiBasePath 뒤에 이어붙이고, domainId를 반드시 넘긴다(누락하면 403)
fetch(`${host.apiBasePath}/items?domainId=${encodeURIComponent(host.domainId)}`)
.then((r) => r.json())
.then(setItems);
}, [host.apiBasePath, host.domainId]);
return <ul>{items.map((i) => <li key={i.id}>{i.title}</li>)}</ul>;
}
const mount: BpMountFn = (container, host) => {
// 스타일 경계 — 없으면 셸의 CSS와 충돌한다
const shadow = container.attachShadow({ mode: "open" });
const el = document.createElement("div");
shadow.append(el);
const root = createRoot(el);
root.render(<App host={host} />);
return () => root.unmount();
};
// 키는 bp-manifest.yaml의 id와 정확히 같아야 한다
window.__BP_WIDGETS__ = { ...window.__BP_WIDGETS__, "hello-bp": { mount } };자세히는 frontend-guide.md.
5. 매니페스트
bp-manifest.yaml:
yaml
apiVersion: 1
id: hello-bp
name: Hello BP
version: 1.0.0
runtime:
hostContract: "^1.0.0"
backend:
entry: backend/index.mjs
frontend:
mode: csr-widget
bundle: frontend/bundle.jsDB를 쓰지 않으므로 backend.database가 없다. 전체 필드는 bp-manifest.md.
6. 빌드
HOST_PROVIDED_MODULES를 externals로 넘기는 것과 emitDecoratorMetadata를 살리는 것이 핵심이다. 빌드 스크립트 전문은 build-and-package.md에 있다.
bash
node scripts/build.mjs
cp bp-manifest.yaml dist/결과:
dist/
bp-manifest.yaml
backend/index.mjs
frontend/bundle.js7. 배포 전 자체 점검
포탈은 문제가 있으면 조용히 건너뛴다. 올리기 전에 확인한다.
bash
node -e "import('./dist/backend/index.mjs').then(m => console.log('register:', typeof m.register))"
grep -c "class BadRequestException" dist/backend/index.mjs # 0이어야 한다(호스트 모듈이 섞이지 않았나)8. 포탈에 올리기
bash
tar czf hello-bp-1.0.0.tgz -C dist .이 파일을 포탈 운영자에게 넘긴다. 설치 후 관리자가 도메인에서 활성화하고 사용자에게 권한을 부여해야 화면이 보인다 — install-to-portal.md.
다음에 읽을 것
| 하려는 것 | 문서 |
|---|---|
| 경계에서 지켜야 하는 것을 정확히 알기 | host-contract.md |
| DB 스키마와 마이그레이션 붙이기 | backend-guide.md §5 |
| 화면 여러 개, 홈 카드, 셸 오퍼레이션 | frontend-guide.md |
| 안 되는데 오류도 안 남 | troubleshooting.md |