pireel/pireel · 생성 2026-07-20(분석 시점 3일 차) · ⭐ 약 411 · 언어 TypeScript · 라이선스 AGPL-3.0-only(공유 UI 패키지만 MIT) · 스택 React 19 · Vite 8 · mediabunny(WebCodecs) · MediaPipe · onnxruntime-web(MODNet) · Vercel AI SDK · pnpm 모노레포 pireel-studio v0.1.0 · shallow clone 소스 직접 분석)
유튜브 쇼츠나 발표 영상을 만들 때 흐름은 늘 비슷하다. 말하는 사람을 찍고, 중요한 대목에 숫자·차트·강조 자막을 얹고, 지루한 부분을 잘라낸다. 이 반복 작업을 Pireel은 "연속된 영상(track 0) + 그 위에 시간·위치가 정해진 그래픽 블록들"이라는 단순한 데이터 구조로 모델링한다.
여기에 두 가지 반전이 있다. 첫째, 이 모든 게 브라우저 탭 하나 안에서 돈다 — 서버 렌더링도, 클라우드 업로드도 없다. 둘째, 에디터가 하는 모든 동작(자막 켜기, 블록 추가, 컷 편집, 익스포트)이 MCP 툴로 노출돼 있어서, 외부 AI 에이전트가 "이 부분에 매출 그래프 넣고, 앞의 3초 잘라줘"를 직접 실행할 수 있다.
Pireel의 정체를 정확히 잡으려면 세 단어를 기억하면 된다. backend-free(백엔드리스) · talking-head(토킹헤드 영상) · agent-drivable(에이전트가 운전 가능). README 원문은 이렇게 시작한다 — "An open-source, backend-free AI video editor for talking-head video."
이 저장소는 사실 사설 모노레포에서 단방향으로 동기화된 공개 미러다(README 명시, 단일 스쿼시 커밋). 그래서 "브라우저에서 즉시 되는 것"과 "호스티드 서비스 전용인 것"의 경계가 뚜렷하다 — 이 구분은 4장에서 정확히 짚는다. 먼저, 왜 3일 만에 400개 넘는 별을 받았는지부터 보자.
Pireel이 트렌딩에 오른 건 "또 하나의 AI 영상툴"이라서가 아니다. 시장에 널린 AI 비디오 에디터들과 설계 철학 자체가 다른 지점이 세 곳 있다. 하나씩 뜯어보자.
계정도 서버도 필요 없다. 편집 중인 드래프트는 브라우저 localStorage에, 영상 원본 바이트는 OPFS(Origin Private File System, 브라우저 전용 파일시스템)에 저장된다. 익스포트조차 서버 렌더팜이 아니라 브라우저의 WebCodecs로 mp4를 뽑는다. CapCut·Descript가 "클라우드에 올려야 시작"인 것과 정반대다. 프라이버시가 중요한 사내 영상·미공개 자료를 다루는 사람에게 이건 결정적 장점이다.
이게 Pireel의 심장이다. Remotion(코드로 영상 정의)이나 Descript(자체 내장 AI)와 달리, Pireel은 에디터 자체를 MCP 툴 서버로 노출한다. 그래서 Claude Code·Codex 같은 외부 에이전트가 자기 구독의 모델 지능으로 편집을 지휘한다. 핵심 비즈니스 설계도 여기서 나온다 — 편집 명령을 오케스트레이션하는 건 공짜, LLM 생성(블록 HTML·나레이션 플래닝)은 사용자의 Codex/Claude 구독으로 돌아가 Pireel 크레딧을 안 태운다. 소스의 mcp.ts 헤더가 이걸 한 줄로 요약한다: "orchestration is free, generation still charges".
세로든 가로든 소스 영상의 종횡비를 그대로 캔버스로 삼는다. 말하는 사람을 트랙 0에 두고, 그 위에 디자인된 그래픽 블록(메트릭 카드·차트·다이어그램·콜아웃)을 얹는다. 이때 MediaPipe·MODNet으로 화자의 얼굴·세이프존을 검출해 그래픽이 화자를 가리지 않게 자동 배치한다. 게다가 25개의 완성된 테마(frame)를 내장해, 초보가 디자인 감각 없이도 "시네마 레터박스"·"Y2K 크롬"·"멤피스 팝" 같은 룩을 즉시 입힌다.
기존 AI 영상툴이 "요리사가 다 해 주는 레스토랑"이라면(재료를 주방=클라우드에 넘기고 완성품을 받음), Pireel은 "AI 조수가 딸린 내 집 주방"이다. 재료(영상)는 내 냉장고(브라우저)를 안 떠나고, 조수(외부 AI 에이전트)에게 "여기 자막 넣고 저기 잘라"라고 시키면 내 눈앞에서 직접 손질한다. 조수의 두뇌는 내가 이미 구독 중인 것(Claude·Codex)을 쓰니, 식당에 두 번 돈 낼 필요가 없다.
이 repo는 사설 모노레포의 공개 미러라, MCP의 프로토콜 코어·툴 정의·브리지 클래스는 있지만 실제 HTTP 엔드포인트(/api/studio/mcp)·Cloudflare Worker 배선·인증은 호스티드 배포 전용이라 빠져 있다. README도 "self-hostable MCP entry는 로드맵"이라고 명시한다. 즉 OSS 셸에서 지금 당장 되는 것은 임포트·편집·프리뷰·타임라인·자막·WebCodecs 익스포트이고, 생성 기능은 provider를 직접 주입하거나 호스티드를 써야 한다. 이 문서는 그 경계를 정확히 표시한다.
Pireel은 pnpm 워크스페이스 모노레포(루트 name pireel-studio v0.1.0, "type":"module")다. 서버가 없으므로 스택의 무게중심이 전부 프론트엔드 + 브라우저 API에 쏠려 있다는 게 특징이다. 계층별로 훑어보자.
| 도구 | 버전 | 역할 |
|---|---|---|
| Vite | ^8.0.16 | 번들러/개발서버. @vitejs/plugin-react + @tailwindcss/vite + vite-tsconfig-paths 조합. |
| TypeScript | ^5.5.4 | target ES2022, moduleResolution Bundler, strict, noEmit. 워크스페이스 패키지가 .ts 소스를 그대로 export → 번들러가 직접 소비. |
| Tailwind CSS | ^4.0.0 | 유틸리티 CSS. v4의 Vite 플러그인 방식. |
| Vitest | ^4.1.5 | 테스트 러너(테스트 파일 23개). |
| pnpm | 9.12.0 | 모노레포 패키지 매니저(packages/*, apps/*). |
| 구분 | 기술 | 비고 |
|---|---|---|
| 프레임워크 | React ^19.0.0 + react-dom 19 | 최신 React 19. 에디터 전체가 React SPA. |
| UI 프리미티브 | radix-ui ^1.4.3 · @base-ui/react ^1.4.0 · cmdk · lucide-react | shadcn 계열 접근성 컴포넌트 + 커맨드 팔레트 + 아이콘. |
| 마크다운/코드 | streamdown ^2.5.0 · shiki ^4.0.2 | 챗의 스트리밍 마크다운(cjk/code/math/mermaid) + 코드 하이라이팅. |
| 국제화/폰트 | use-intl ^4.13.0 · Geist · JetBrains Mono | i18n + 가변 폰트. |
Vercel AI SDK(ai ^6.0.168 + @ai-sdk/react ^3.0.176)가 내부 챗의 useChat + streamText 툴콜링을 담당한다. 그리고 이 챗의 툴 실행면이 외부 MCP 에이전트와 정확히 같은 실행면을 공유한다는 게 핵심 설계다(4장 참고).
| 기술 | 버전 | 무엇을 하나 |
|---|---|---|
| mediabunny | ^1.45.4 | WebCodecs 위의 mux/demux/encode 래퍼. 익스포트·씬 감지·썸네일이 전부 이걸로. Input/Output/CanvasSource/AudioSampleSource/Mp4OutputFormat 등. |
| @mediapipe/tasks-vision | ^0.10.35 | FaceDetector + ImageSegmenter(셀피 세그멘테이션). 화자 얼굴·세이프존 검출로 그래픽 자동 배치. |
| onnxruntime-web | ^1.27.0 | MODNet 인물 매팅을 WebGPU로 실행. 배경 제거/합성 품질을 끌어올림. |
| gsap | (vendored) | 블록 타임라인 애니메이션. 런타임에 public/vendor/gsap.min.js를 same-origin으로 로드. |
fast-json-patch ^3.1.1 + jsondiffpatch ^0.7.6로 undo/버전 diff를, nanoid ^5.1.11로 ID를 만든다. 컴포지션(편집 상태)은 JSON 패치로 관리돼 실행 취소가 가볍다.
OSS 셸의 유일한 "서버"는 Vite 개발 플러그인 apps/studio-oss/local-assets-plugin.ts다. node:fs/node:crypto로 디스크 기반 업로드 라우트(content-addressed sha256)를 흉내 낸다 — 호스티드의 R2+CDN을 로컬로 대체하는 개발 편의 장치일 뿐이다. 실제 프로덕션 백엔드(Cloudflare Workers + Durable Objects + R2)는 이 repo에 없다. 검증 결과 "백엔드리스"는 사실이다 — 편집/프리뷰/타임라인/자막/익스포트가 전부 클라이언트다.
Pireel의 아키텍처는 세 개의 렌즈로 보면 전부 이해된다. ① 패키지가 어떻게 층을 이루는가, ② 편집 상태(컴포지션)를 어떻게 표현해 프리뷰와 익스포트가 픽셀 단위로 같아지는가, ③ 외부 AI 에이전트가 어떻게 열린 브라우저 탭을 원격 조종하는가. 순서대로 파고든다.
핵심은 studio-engine이 React도 DB도 provider도 모르는 순수 로직이라는 점이다. 컴포지션 모델·자막 렌더·MCP 프로토콜·비디오 렌더가 전부 여기 있고, UI(studio-ui)는 이 엔진을 React로 감쌀 뿐이다. 이 분리 덕분에 같은 엔진 코드가 브라우저에서도, (호스티드의) 서버 오프라인 툴에서도 재사용된다.
composition-core.ts의 구조는 연속 비디오(track 0) + 멀티트랙 오버레이 블록이다. 여기서 영리한 결정 하나: 블록은 HTML을 저장하지 않고 데이터만 저장한다.
// 블록은 "무엇을" 담을지만 선언한다 (어떻게 그릴지는 나중에)
{
templateId: "metric-card", // 어떤 템플릿?
slots: { title: "MRR", value: "$42k" }, // 슬롯 채우기
startSec: 3.2, durationSec: 2.5, // 언제부터 얼마나
trackIndex: 1, box: {...} // 어느 트랙, 어디에
}
실제 HTML/애니메이션은 조립(assemble) 시점에 템플릿 레지스트리 + 테마(CSS 변수)로 동적 렌더된다. 그래서 테마를 바꿔도 블록 데이터는 그대로고, CSS 변수만 교체하면 룩이 바뀐다. 또 에이전트는 자유형 HTML을 통째로 생성할 필요 없이, templateId와 slots만 선언적으로 채우면 된다(자유형이 필요하면 custom 템플릿 사용).
비디오 트랙은 VideoShot[](각 {srcStart, srcEnd, treatment, src?})로, 편집 타임라인 = 각 클립의 소스 구간을 이어붙인 것이다. treatment는 full / punch-in / corner-br / split-l 같은 프레이밍이고, src가 설정되면 외부에서 삽입한 B-roll 클립이 된다.
대부분의 에디터에서 "미리보기"와 "실제 출력"이 미묘하게 다른 건, 둘을 다른 코드로 그리기 때문이다. Pireel은 이걸 원천 봉쇄한다 — 프리뷰와 익스포트가 글자 그대로 같은 iframe을 구동한다.
익스포트의 마법은 "브라우저 자신을 래스터라이저로 쓰는" 트릭이다. HTML 오버레이(#root)를 SVG foreignObject로 감싸 <img>로 만들면, 브라우저(Blink)가 그걸 이미지로 그려 준다. 그 이미지를 캔버스에 얹고, 그 캔버스를 WebCodecs로 인코딩한다. viewBox는 레이아웃 좌표, width/height는 출력 해상도로 잡아 4K에서도 벡터가 재래스터화되어 선명하다.
caption-fx.ts의 키네틱 자막은 어떤 프레임이든 오직 시각 t에만 의존한다(requestAnimationFrame도, 난수도 없다). 왜? WebCodecs 익스포트는 프레임을 시간 순서대로가 아니라 특정 t로 "시크"해서 뽑는데, 애니가 이전 상태에 의존하면 익스포트가 깨진다. t만 넣으면 결정론적으로 같은 그림이 나오도록 설계해, 프리뷰와 익스포트가 완벽히 정렬된다. 5개 이펙트(highlight / pill-karaoke / kinetic-slam / editorial-emphasis / word-pop) 전부 이 원칙을 지킨다.
가장 독창적인 부분이다. 툴 실행이 브라우저에 깊게 묶여 있어(React state 클로저, MediaPipe, 프리뷰 iframe) 서버에서 실행할 수 없다. 그래서 Pireel은 서버가 실행하지 않고 "열린 탭으로 포워딩"한다. 아래 그림에서 ✅는 이 repo에 있는 것, ❌는 호스티드 전용이라 빠진 것이다.
왜 Durable Object(DO)인가? 툴 실행이 특정 브라우저 탭에 묶이므로, 유저 하나당 DO 하나(idFromName(userId))를 두고 소켓을 단일 활성으로 유지한다(새 탭이 열리면 옛 탭을 code 4000으로 kick). 외부 에이전트의 툴콜은 이 DO를 거쳐 열린 탭의 runStudioTool로 전달되고, 그 결과가 다시 소켓으로 돌아온다.
// use-agent-bridge.ts — 툴콜을 열린 탭에서 "직렬로" 실행
queue = queue.then(async () => {
let out;
try {
out = tool === 'get_state'
? { ok: true, state: getState() }
: await runTool(tool, input ?? {}); // 내부 챗과 동일
} catch (e) {
out = { ok: false, error: String(e) };
}
sock.send(JSON.stringify({ id, ...out })); // 결과를 소켓으로 반환
});
여기서 로컬로 도는 것(analyze_visual = MediaPipe, 익스포트 = WebCodecs)과 유료 LLM이 필요한 것(analyze_narration, add_graphics)이 명확히 나뉜다. 후자를 "brief 계약"으로 외부 에이전트에 넘기는 게 BYO-brain의 핵심이다.
구조가 말해 주는 것: 관심사가 칼같이 분리돼 있다. studio-engine은 순수 로직(React 无관)이라 테스트·재사용이 쉽고, studio-ui는 그걸 React로 감싼 표현 계층이다. 눈여겨볼 파일 크기의 불균형 — hyperframes-workbench.tsx가 무려 7,116줄이다. 워크벤치 하나가 state·프리뷰·타임라인·runStudioTool을 전부 쥐고 있는 "God component"에 가까운데, 이건 편집 상태가 서로 촘촘히 얽혀 있어 쪼개기 어려운 에디터의 숙명이기도 하다.
content/<id>/frame.md 파일 하나로 정의되는데, frontmatter에 팔레트·디자인 토큰이 들어가고, 본문(playbook)은 LLM 시스템 프롬프트에 주입되는 영문 디자인 지침이다. 즉 테마가 "색과 규칙"만이 아니라 "AI가 이 스타일로 블록을 만들 때 따를 매뉴얼"까지 담는다. cinema-frame(레터박스), y2k-chrome, memphis-pop, neon-runner 등 25종.mediabunny가 VideoEncoder/muxer를 추상화한다. Canvas2D 합성 + DOM→SVG foreignObject 래스터화(브라우저 자체가 래스터라이저)로 HTML 오버레이를 캔버스에 굽는 기법은, 리포트·차트·명함 이미지 생성 등 "웹에서 이미지/영상을 뽑는" 어떤 작업에도 응용된다.
툴 정의를 내부 챗과 외부 MCP가 한 테이블로 공유(STUDIO_TOOLS)하고, 실행은 브라우저 브리지로 포워딩한다. "stateless MCP + WebSocket DO 브리지"는 브라우저에 묶인 앱을 에이전트에게 열어 주는 정석 구조다. SaaS를 에이전트 시대에 대응시키려는 사람이 통째로 베낄 만한 청사진.
서버가 LLM을 안 부르고 {system, prompt} 계약만 조립 → 생성은 호출자 모델 → 결과는 lint 통과해야 반영. "토큰 비용을 호출자에게 넘기는" 이 설계는 오픈코어 비즈니스 모델과 직결된다. AI 기능을 넣되 비용을 통제하려는 모든 제품이 참고할 패턴.
영상 바이트는 OPFS에(fileSig = name:size:mtime로 식별), 컴포지션은 localStorage에 저장. 새로고침 후 자동 재연결. 서버 없이도 "작업이 사라지지 않는" 앱을 만드는 법 — 오프라인 우선 웹앱 전반에 적용된다.
애니를 t에만 의존하는 순수 함수로 짜면, 실시간 프리뷰와 프레임 시크 익스포트가 같은 로직을 공유해 절대 어긋나지 않는다. 게임 리플레이·시뮬레이션·차트 애니 등 "결정론이 필요한" 모든 곳의 핵심 원리.
MODNet(onnxruntime-web, WebGPU)이 실패하면 MediaPipe 셀피 세그로 폴백한다. 모델을 전부 self-host(public/models)해 외부 의존을 끊는다. "브라우저에서 ML을 돌리되, 안 되는 기기에선 우아하게 물러서는" 패턴.
블록이 HTML이 아니라 {templateId, slots} 데이터를 담으니, 테마 교체가 블록을 안 건드린다. 에이전트도 자유형 HTML 대신 슬롯만 채우면 돼 생성 실패율이 낮다. 선언적 UI/문서 생성의 좋은 본보기.
studio-engine은 React·DB·provider를 모른다. 그래서 같은 코드가 브라우저와 서버 오프라인 툴에서 재사용된다. "로직과 프레임워크를 섞지 않는다"는 오래된 교훈의 최신 실천 사례.
가장 먼저 packages/studio-engine/src/caption-fx.ts를 열어 drawCaptionFx(ctx, o, t, ...)가 어떻게 시각 t 하나로 자막 프레임을 그리는지 따라가 보라. 그다음 client-export.ts의 헤더 주석(오버레이를 SVG foreignObject로 굽는 과정)을 읽으면, "프리뷰와 익스포트가 왜 같은가"가 손에 잡힌다. 이 두 파일만으로도 Pireel의 렌더링 철학이 완성된다.
| 항목 | 요구사항 / 수치 |
|---|---|
| 브라우저 (익스포트) | Chromium 계열 필수. 코드가 canClientExport() = 'VideoEncoder' in window로 체크한다(use-export.ts). WebCodecs 미지원 브라우저에선 익스포트 불가. README도 "Client export … on WebCodecs (Chromium)" 명시. |
| 인물 매팅 (고품질) | WebGPU 필요('gpu' in navigator). 없으면 MediaPipe wasm으로 폴백(품질↓). MODNet 모델 public/models/modnet_portrait.onnx(HuggingFace Xenova/modnet)가 있어야 하고, 없으면 그 기능만 우아하게 실패. |
| 로컬 영상 라이브러리 | OPFS(navigator.storage.getDirectory) 지원 브라우저. 없으면 null 폴백(라이브러리 기능만 비활성). |
| 정적 자산 계약 | /vendor/gsap.min.js가 same-origin으로 있어야 프리뷰 런타임이 뜬다(필수). 선택: /models/modnet_portrait.onnx, /mediapipe/*(wasm+tflite). |
| 개발 환경 | pnpm 9.12.0, Node(Vite 8이 요구하는 버전). pnpm install 후 apps/studio-oss에서 dev 서버 실행. |
| 익스포트 성능 | 대략 실시간(3분 영상 ≈ 3분). WebCodecs 하드웨어 디코드가 긴 프로젝트 성능에 유리(코드가 디코더 은퇴 로직으로 하드웨어 쿼터 관리). |
| GPU | 필수 아님. 있으면 WebGPU 매팅 품질↑ + 하드웨어 인코드/디코드로 익스포트 속도↑. |
WebCodecs VideoEncoder는 Chromium 계열에서 가장 안정적이다. Safari/Firefox는 지원 범위가 좁아 익스포트가 막힐 수 있으니 개발/사용은 Chrome·Edge 권장. 또 OSS 셸은 생성 provider를 unavailableProviders()로 비워 두므로, ASR·나레이션 분석·이미지 생성 같은 AI 생성 기능은 기본 상태에서 비활성이다 — 직접 provider를 주입하거나 외부 에이전트(BYO)를 붙여야 켜진다. "받아서 바로 AI가 다 해 줄" 것으로 기대하면 어긋난다.
pnpm install 후 apps/studio-oss의 dev 서버를 Chrome에서 연다. 짧은 토킹헤드 클립을 임포트해 자막을 켜고, WebCodecs 익스포트로 mp4를 뽑아 보라. 브라우저 탭 하나가 서버 없이 영상을 인코딩하는 걸 체감하는 게 목표. (익스포트 버튼이 회색이면 WebCodecs 미지원 → Chromium으로.)
caption-fx.ts의 drawCaptionFx switch에 6번째 이펙트(예: "typewriter")를 추가하라. 반드시 시각 t에만 의존하는 순수 함수로 짜야 익스포트에서도 정상 동작한다. 프리뷰에서 잘 보이는데 익스포트에서 깨진다면, 어딘가 t 외의 상태(rAF·난수)에 의존한 것 — 그걸 찾아 고치는 게 학습의 핵심.
composition-core.ts의 타입을 보고, {templateId, slots, startSec, durationSec, trackIndex} 블록을 직접 JSON으로 작성해 컴포지션에 추가하라. 템플릿 레지스트리에서 templateId가 어떻게 실제 HTML로 렌더되는지, 테마(CSS 변수)를 바꾸면 같은 블록이 어떻게 달라지는지 확인한다.
person-matte.ts가 onnxruntime-web으로 MODNet을 WebGPU에서 돌리는 과정을 읽고, WebGPU 미지원 시 MediaPipe로 폴백하는 분기를 찾아라. 모델 파일이 없을 때 어떻게 우아하게 실패하는지도 확인한다. 브라우저 ML 추론의 실전 에러 핸들링을 배우는 과제.
prompts/agent-tools.ts의 STUDIO_TOOLS 테이블에 새 툴(예: set_background_blur)을 스키마·설명과 함께 추가하고, runStudioTool에서 그 동작을 구현하라. 내부 챗에서 호출해 동작을 확인한 뒤, use-agent-bridge.ts의 실행 경로를 따라가며 "외부 에이전트가 부르면 어떻게 같은 코드에 도달하는지"를 이해한다.
| 주차 | 주제 | 무엇을 · 왜 |
|---|---|---|
| 1주차 | WebCodecs + Canvas | 브라우저 VideoEncoder/VideoDecoder, Canvas2D 합성, mediabunny 추상화. 서버 없이 영상을 인코딩하는 파이프라인의 기초를 잡는다. |
| 2주차 | SVG foreignObject 래스터화 | HTML을 SVG로 감싸 이미지로 뽑는 트릭, 폰트/이미지 인라이닝(data:) 제약. client-export.ts를 읽으며 "브라우저를 래스터라이저로 쓰는" 법을 익힌다. |
| 3주차 | 브라우저 ML (WebGPU/ONNX/MediaPipe) | onnxruntime-web으로 MODNet 매팅, MediaPipe 얼굴/세그, WebGPU EP와 wasm 폴백. person-matte.ts+geometry.ts를 짝지어 분석. |
| 4주차 | MCP + 에이전트 브리지 | MCP 프로토콜(JSON-RPC, streamable-HTTP), Durable Object, WebSocket 포워딩. mcp.ts+bridge-do.ts+use-agent-bridge.ts로 "앱을 에이전트에 여는" 구조를 완성한다. |
| 5주차 | 로컬 우선 지속성 + 데이터 모델 | OPFS·localStorage, JSON 패치 undo(fast-json-patch), 데이터-드리븐 컴포지션. local-media.ts+composition-core.ts로 "서버 없이 상태를 안전하게 관리"하는 법을 마무리한다. |
fileSig=name:size:mtime로 식별).{templateId, slots} 데이터만 저장한다.seekTimelines(t)로 구동해 미리보기와 출력이 픽셀 단위로 동일. HTML 오버레이는 SVG foreignObject로 캔버스에 굽는다.t에만 의존하는 순수 함수로 그리는 애니 자막(5개 이펙트). rAF·난수 없이 결정론적이라 WebCodecs 프레임 시크와 정렬된다.runStudioTool. 브라우저에 묶인 툴을 에이전트가 원격 실행하게 하는 3계층 구조.{system, prompt} 계약만 만들고 생성은 사용자의 Claude/Codex 구독으로 → 결과를 검증 후 반영.content/<id>/frame.md 한 파일로 정의되는 디자인 테마. 팔레트·토큰 + LLM에 주입되는 영문 "playbook" 지침을 함께 담는다(25종 내장).idFromName(userId))를 두어 브리지 소켓을 단일 활성으로 관리(호스티드 전용).