`; document.body.appendChild(el); return el; } const bubble = ensureBubble(); const elH = bubble.querySelector("#tt-title"); const elB = bubble.querySelector("#tt-body"); const elClose = bubble.querySelector(".tt-close"); // ---------------- Parse [[term|heading|body]] anywhere ---------------- const TOKEN_RE = /\[\[([^|\]]+)\|([^|\]]+)\|([^\]]+)\]\]/g; const BLOCK_SKIP = new Set(["SCRIPT","STYLE","NOSCRIPT","TEXTAREA","INPUT","SELECT","CODE","PRE","TEMPLATE","IFRAME"]); function shouldSkipTextNode(n){ let el = n.parentElement; while (el){ if (BLOCK_SKIP.has(el.tagName) || el.isContentEditable) return true; el = el.parentElement; } return false; } const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const textNodes = []; while (walker.nextNode()){ const n = walker.currentNode; if (!n.nodeValue || shouldSkipTextNode(n)) continue; if (TOKEN_RE.test(n.nodeValue)) textNodes.push(n); TOKEN_RE.lastIndex = 0; } textNodes.forEach(node => { const frag = document.createDocumentFragment(); const insideLink = !!node.parentElement.closest("a"); let text = node.nodeValue, last = 0; TOKEN_RE.lastIndex = 0; let m; while ((m = TOKEN_RE.exec(text))){ if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index))); const term=m[1].trim(), heading=m[2].trim(), body=m[3].trim(); const t = insideLink ? document.createElement("span") : document.createElement("button"); if (insideLink){ t.setAttribute("role","button"); t.setAttribute("tabindex","0"); } else { t.type="button"; } t.className="tt-trigger"; t.textContent=term; t.setAttribute("data-tt-h", heading); t.setAttribute("data-tt-b", body); t.setAttribute("aria-haspopup","dialog"); t.setAttribute("aria-expanded","false"); frag.appendChild(t); last = TOKEN_RE.lastIndex; } if (last = 2) return el; el = el.parentElement; } // Fallback: nearest non-inline container el = trigger.parentElement || document.body; while (el && el !== document.body){ const d = getComputedStyle(el).display; if (d !== "inline" && d !== "contents") return el; el = el.parentElement; } return document.body; } // Utility: child of `ancestor` that contains `target` (direct child) function directChildContaining(ancestor, target){ for (const ch of ancestor.children){ if (ch === target || ch.contains(target)) return ch; } return null; } function getElementTarget(e) { // If target is already an Element, use it if (e.target instanceof Element) return e.target; // Otherwise, walk the composed/path for the first Element const path = (typeof e.composedPath === 'function') ? e.composedPath() : []; for (const n of path) if (n instanceof Element) return n; return null; } // ---------------- Dim everything except the trigger branch (sibling branches only) ---------------- function dimAllOtherBranches(container, trigger){ undim(); // clear previous const dimEls = []; const wrappedTexts = []; const pathEls = []; // Build ELEMENT-only path [container -> ... -> trigger] const path = []; for (let el = trigger; el && el !== container; el = el.parentElement) path.push(el); path.push(container); path.reverse(); // At each ancestor level, find the *direct* child that leads to the trigger for (let i = 0; i { if (node.nodeType !== 3) return; // text only if (!node.nodeValue || !node.nodeValue.trim()) return; // If this text node sits inside branchChild, skip if (branchChild && branchChild.contains && branchChild.contains(node)) return; const span = document.createElement("span"); span.style.transition = `opacity ${DIM_EASE_MS}ms ease`; span.style.opacity = String(DIM_OPACITY); span.textContent = node.nodeValue; node.parentNode.replaceChild(span, node); wrappedTexts.push(span); }); // Keep a reference to the path elements (so we can explicitly restore opacity if needed) if (anc && anc.nodeType === 1) pathEls.push(anc); } // Hard-guard: explicitly set opacity:1 on the entire path to neutralize any inherited fade pathEls.forEach(el => { el.style.opacity = "1"; }); dimCtx = { container, dimEls, wrappedTexts, pathEls }; } function undim(){ if (!dimCtx) return; const { dimEls, wrappedTexts, pathEls } = dimCtx; // Animate back dimEls.forEach(el => { el.style.transition = `opacity ${DIM_EASE_MS}ms ease`; el.style.opacity = "1"; // remove inline style after the animation so we don't override site CSS setTimeout(() => { if (el) el.style.opacity = ""; }, DIM_EASE_MS + 50); }); wrappedTexts.forEach(span => { span.style.transition = `opacity ${DIM_EASE_MS}ms ease`; span.style.opacity = "1"; span.addEventListener("transitionend", () => { if (!span.parentNode) return; span.parentNode.replaceChild(document.createTextNode(span.textContent || ""), span); }, { once:true }); }); // Clear hard-guard on path pathEls.forEach(el => { if (el) el.style.opacity = ""; }); dimCtx = null; } // ---------------- Positioning (centered, edge-aware, flip) ---------------- function clamp(v,min,max){ return Math.max(min,Math.min(max,v)); } function measureBubbleForPlacement(){ const wasOpen = bubble.classList.contains("is-open"); if (!wasOpen){ bubble.style.visibility="hidden"; bubble.classList.add("is-open"); } const rect = bubble.getBoundingClientRect(); if (!wasOpen){ bubble.classList.remove("is-open"); bubble.style.visibility=""; } return { w: rect.width, h: rect.height }; } function placeAnchored(trigger){ const vw=innerWidth, vh=innerHeight; const r = trigger.getBoundingClientRect(); const { w, h } = measureBubbleForPlacement(); let left = r.left + (r.width/2) - (w/2); left = clamp(left, EDGE_PADDING, Math.max(EDGE_PADDING, vw - EDGE_PADDING - w)); const topBelow = r.bottom + OFFSET_Y; const spaceBelow = vh - topBelow - EDGE_PADDING; const placeBelow = spaceBelow >= h; let top = placeBelow ? topBelow : (r.top - h - OFFSET_Y); top = clamp(top, EDGE_PADDING, Math.max(EDGE_PADDING, vh - EDGE_PADDING - h)); bubble.style.left = left + "px"; bubble.style.top = top + "px"; const br = bubble.getBoundingClientRect(); if (br.bottom > vh - EDGE_PADDING){ bubble.style.maxHeight = (vh - 2*EDGE_PADDING) + "px"; bubble.style.overflowY = "auto"; } else { bubble.style.maxHeight = "none"; bubble.style.overflowY = "visible"; } } // ---------------- Open / Close (place → fade/scale) ---------------- function animateIn(){ bubble.style.transition = "none"; bubble.style.opacity = "0"; bubble.style.transform = "scale(0.95)"; void bubble.offsetWidth; bubble.style.transition = "opacity .18s ease, transform .18s ease"; bubble.style.opacity = "1"; bubble.style.transform = "scale(1)"; } function animateOut(done){ bubble.style.transition = "opacity .16s ease, transform .16s ease"; bubble.style.opacity = "0"; bubble.style.transform = "scale(0.95)"; const end = () => { bubble.removeEventListener("transitionend", end); done && done(); }; bubble.addEventListener("transitionend", end); setTimeout(end, 260); } function openFromTrigger(trigger){ if (current && current !== trigger) forceClose(); current = trigger; trigger.setAttribute("aria-expanded","true"); elH.textContent = trigger.getAttribute("data-tt-h") || ""; elB.textContent = trigger.getAttribute("data-tt-b") || ""; bubble.classList.add("is-open"); bubble.setAttribute("aria-hidden","false"); placeAnchored(trigger); animateIn(); const container = findTextContainer(trigger); dimAllOtherBranches(container, trigger); hoverCount = 0; cancelCloseTimer(); } function forceClose(){ if (!current) return; bubble.classList.remove("is-open"); bubble.setAttribute("aria-hidden","true"); current.setAttribute("aria-expanded","false"); current = null; undim(); hoverCount = 0; cancelCloseTimer(); } function closeWithAnim(){ if (!current) return; const t = current; animateOut(() => { bubble.classList.remove("is-open"); bubble.setAttribute("aria-hidden","true"); t.setAttribute("aria-expanded","false"); current = null; undim(); }); } function scheduleClose(){ cancelCloseTimer(); closeTimer = setTimeout(() => { if (hoverCount { if (isCoarse()) return; const target = getElementTarget(e); if (!target) return; const t = target.closest(".tt-trigger"); if (!t) return; onZoneEnter(); if (!current || current !== t) openFromTrigger(t); }; const handleLeave = (e) => { if (isCoarse()) return; const target = getElementTarget(e); if (!target) return; const t = target.closest(".tt-trigger"); if (!t) return; onZoneLeave(); }; document.addEventListener("pointerenter", handleEnter, true); document.addEventListener("mouseenter", handleEnter, true); document.addEventListener("pointerleave", handleLeave, true); document.addEventListener("mouseleave", handleLeave, true); // ---------------- Keyboard ---------------- document.addEventListener("focusin", (e) => { if (!e.target) return; const t = e.target.closest(".tt-trigger"); if (t) openFromTrigger(t); }); document.addEventListener("focusout", (e) => { if (!e.target) return; const t = e.target.closest(".tt-trigger"); if (t && current === t) closeWithAnim(); }); // ---------------- Mobile / coarse ---------------- document.addEventListener("pointerdown", (e) => { if (!isCoarse()) return; const t = e.target.closest(".tt-trigger"); if (!t) return; e.preventDefault(); e.stopPropagation(); if (current === t && bubble.classList.contains("is-open")) { closeWithAnim(); return; } openFromTrigger(t); }, true); document.addEventListener("click", (e) => { if (!isCoarse()) return; if (!bubble.classList.contains("is-open")) return; const inBubble = !!e.target.closest(".tt-bubble"); const onTrigger = !!e.target.closest(".tt-trigger"); if (!inBubble && !onTrigger) closeWithAnim(); }, true); // Close button + ESC elClose.addEventListener("click", closeWithAnim); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeWithAnim(); }); // Reposition on resize/scroll while open const reposition = () => { if (!current) return; placeAnchored(current); }; addEventListener("resize", reposition, { passive: true }); addEventListener("scroll", reposition, { passive: true }); });

Bolt, Claude Agent SDK를 기반으로 자율 디자인 시스템 에이전트 구축

Claude 사용해 보기
도입 문의
업종:
소프트웨어
회사 규모:
Startup
제품
Claude Agent SDK
위치:
북아메리카
자율 에이전트 워크플로우 평균 약 53분 소요
Claude Opus 4.7 기반, 수천 개의 컴포넌트 전반에서 충돌 해결 및 토큰 매핑
Agent SDK에서 약 90% 캐시 효율성
트래픽이 높은 워크플로우에서 추론 비용 절감

Bolt는 데이터베이스, 결제, 배포를 위한 통합 기능이 내장되어 있어 사용자가 전체 애플리케이션을 자연어로 빌드할 수 있도록 지원하는 바이브 코딩 도구입니다. StackBlitz에서 구축하고 Claude Agent SDK로 구동하는 Bolt의 최신 기능은 PM과 디자이너가 코드를 작성하지 않고도 프로덕션 준비가 된, 브랜드에 맞는 프로토타입을 생성할 수 있게 해주는 디자인 시스템 에이전트입니다.

Claude를 통해 StackBlitz가 달성한 성과:

  • 10,000명 이상의 사용자가 Bolt에 자체 디자인 시스템 업로드
  • 디자인 시스템 생성에 약 52분, 브랜드에 맞는 무제한 프로토타입 완성에는 각각 약 5분을 소요하여, 출시하는 데 필요한 엔지니어 재작업 최소화
  • Storybook, GitHub 리포지토리, npm 패키지, Figma 토큰, 문서를 하나의 통합 시스템으로 묶는 자율 디자인 시스템 생성
  • Agent SDK에서 약 90% 캐시 효율성으로 트래픽이 높은 워크플로우 전반에서 추론 비용 절감

과제

클로드 에이전트 SDK로 에이전트 빌드하기

클로드 에이전트 SDK는 개발자가 클로드 코드를 기반으로 강력한 에이전트를 구축할 수 있도록 도와주는 도구 모음입니다.

자세히 보기
클로드 에이전트 SDK로 에이전트 빌드하기
다음

클로드 에이전트 SDK는 개발자가 클로드 코드를 기반으로 강력한 에이전트를 구축할 수 있도록 도와주는 도구 모음입니다.

다음
클로드 에이전트 SDK로 에이전트 빌드하기

클로드 에이전트 SDK는 개발자가 클로드 코드를 기반으로 강력한 에이전트를 구축할 수 있도록 도와주는 도구 모음입니다.

단편화된 디자인 시스템의 문제

대부분의 기업은 디자인 시스템을 한곳에 두지 않습니다. 타이포그래피는 Google Doc에 있고, 컴포넌트는 Figma 파일, Storybook 인스턴스, GitHub 리포지토리에 흩어져 있죠. 여백과 레이아웃 가이드라인은 위키에 있을 수도 있고, 엔지니어의 머릿속에만 있을 수도 있습니다.

이러한 격차는 기업이 AI 개발 도구를 도입하는데 주요 장벽이 되었습니다. PM과 디자이너는 Bolt와 같은 도구에서 아이디어를 프로토타입으로 만들 수 있지만, 회사의 실제 디자인 언어를 사용하지 못하면, 그 결과는 획일적으로 보일 수밖에 없습니다. StackBlitz의 창립 엔지니어 Dominic Elm은 말합니다. "Bolt만 사용하면 대개 회사의 디자인 시스템은 전혀 알지 못합니다. 디자인 시스템은 다소 복잡한 체계를 가지고 있는데 말이죠. 가이드라인, 타이포그래피, 여백, 레이아웃 등 모든 것이 포함되어 있으니까요."

도구를 쓰면 코드를 빠르게 생성할 수 있지만, 결과물은 회사의 내부 기준과 맞지 않습니다. 결국 프로토타입은 일회용으로 전락합니다. PM이 아이디어를 전달하려 무언가를 만들지만, 출시하기 전에 엔지니어가 처음부터 다시 작성해야 하기 때문입니다. 프로토타입과 프로덕션 코드 사이의 이러한 간극으로 인해 엔지니어가 아닌 사람은 개발 수명 주기에서 소외될 수밖에 없습니다.

해결 방법

다음

다음

StackBlitz가 Claude Agent SDK를 기반으로 구축한 이유

StackBlitz는 Bolt를 구동하고자 자체 내부 에이전트를 개발했습니다. 그러다 대대적인 개편을 계획하던 중, 마침 Claude Agent SDK가 출시된 것입니다. Elm은 이렇게 설명합니다. "저희는 v2를 앞두고 에이전트를 대폭 개선하려던 참이었습니다. 그러다 Agent SDK가 등장했고, 이미 많은 사람들이 사용하고 있는 것을 선택하는 게 합리적이라고 판단했습니다."

이 결정으로 팀은 에이전트 인프라를 구축하고 유지 관리하는 부담에서 벗어나, 제품 혁신에만 초점을 맞출 수 있게 되었습니다. StackBlitz의 AI 팀은 소규모입니다. "저희 팀은 몇 명 수준인데, Claude Code를 개발하는 인력 규모에는 한참 못 미치죠."라고 Elm은 덧붙였습니다. Claude Code가 확립한 사용자 기반도 매력적이었습니다. Agent SDK를 선택하면 StackBlitz로서는 막대한 투자를 하지 않아도 Agent SDK와 같은 기본 기능을 얻을 수 있었기 때문입니다. Agent SDK는 Claude Code를 구동하는 것과 동일한 프레임워크를 기반으로 하므로, StackBlitz는 그동일한 기능을 그에 상응하는 투자 없이도 이어받을 수 있었습니다.

Claude로 디자인 시스템 에이전트 구축

StackBlitz는 Agent SDK를 가장 먼저 전면 도입한 팀 중 하나였고, 이를 장기간 개발한 경험을 덕분에 Elm은 이 플랫폼이 얼마나 유연한지 알고 있었습니다. "현재 설계된 방식만으로도 거의 모든 워크플로우를 구축할 수 있습니다."라고 그는 말했습니다.

디자인 시스템 에이전트가 대표적인 사례입니다. Storybook 사이트, GitHub 리포지토리, npm 패키지, Figma 토큰, 문서 파일처럼 회사가 보유한 모든 자산을 가져오는 것부터 시작합니다. 사용자가 에이전트에 이러한 소스를 지정해 주면, 에이전트는 각 소스를 철저하게 검토하며 충돌을 해결하고, 디자인 토큰을 매핑하며 컴포넌트 변형을 파악합니다. 그리고 결과적으로 전체 디자인 시스템을 하나로 통합한 중간 표현형이 만들어집니다. 이 에이전트는 Claude Opus 4.7을 사용하며, Elm은 지속적인 추론이 필요한 리서치 단계에서 '전반적으로 최고의 결과를 보여주었다'고 말했습니다.

이 리서치 단계는 연결된 소스량에 따라 40분에서 1시간 30분 정도 걸릴 수 있으며, 평균적으로 약 53분이 소요됩니다. 이는 일반적인 5분짜리 진행되는 프로토타입 구축과는 차원이 다른 작업입니다. SDK의 후크 덕분에 가능해진 것입니다. stop 후크를 사용하면 긴 워크플로우에서 에이전트를 계속 실행해 두면서 피드백을 다시 전달할 수 있으며, 실행 중간에 에이전트의 방향성을 수정할 수 있습니다. 맞춤형 서브 에이전트는 메인 에이전트의 컨텍스트를 깔끔하게 유지하는 동시에 병렬 작업을 처리해 줍니다. Elm은 말합니다. "작업을 병렬로 처리할 수 있어 전반적인 속도에 도움이 됩니다. 또한 서브 에이전트가 수행하는 작업이 메인 컨텍스트 창을 오염시키지 않습니다."

그 결과물은 Bolt 내부에 회사의 모든 컴포넌트, 섹션, 페이지를 한 곳에서 찾아볼 수 있는 라이브러리 형태로 존재하게 됩니다. StackBlitz는 Bolt를 회사 디자인 시스템의 단일 기준 소스로 만들지 않도록 신중하게 접근했습니다. 대신, 팀이 자체적으로 디자인 시스템을 업데이트하면 Bolt로 돌아와 다시 인덱싱할 수 있으며, 에이전트는 증분 업데이트를 수행합니다. 디자인 시스템이 준비되면 사용자가 이를 선택하고, Bolt에게 차량 구성기나 비교 페이지 등을 만들어 달라고 프롬프트를 넣습니다. 그러면 출력물은 회사의 실제 컴포넌트, 타이포그래피, 여백을 그대로 따릅니다. 그런 다음 에이전트는 검토 루프에서 스스로 작업물을 확인합니다. "무언가를 만들고 나서, 의도한 모습과 일치하는지 다시 확인해 줘." Elm은 이 과정을 개발자가 결과물을 Figma 파일과 비교해 검증하는 방식에 비유했습니다.

"Agent SDK를 사용하면 거의 모든 워크플로우를 구축할 수 있습니다."
Dominic Elm, 창립 엔지니어
Stackblitz

다음

다음

결과

일회성 프로토타입에서 프로덕션 코드로

디자인 시스템 에이전트는 PM과 디자이너가 제공할 수 있는 결과물에 변화를 가져옵니다. 평균 53분간의 자율 실행으로 디자인 시스템이 일단 생성됩니다. 그다음부터는 팀원 누구나 Bolt에 프롬프트를 입력해 약 5분 안에 브랜드에 맞는 프로토타입을 만들어낼 수 있으며, 이때 생성되는 코드는 최소한의 재작업으로 프로덕션에 투입할 수 있는 수준입니다. "핵심은 Bolt가 그냥 일회성으로 버려지지 않는 프로토타입을 생성할 수 있는 것이죠."라고 Elm은 말했습니다. "엔지니어에게 이렇게 코드를 넘기면, 컴포넌트에 비즈니스 로직만 연결해 거의 수정 없이 프로덕션 환경에 배포할 수 있습니다."

StackBlitz는 Agent SDK를 통해 약 90%의 캐시 효율성을 달성하며, 장시간 실행되는 워크플로우에서도 추론 비용을 안정적으로 관리하고 있습니다. 이미 10,000명 이상의 사용자가 자신의 디자인 시스템을 Bolt에 업로드했습니다.

미래 전망

StackBlitz는 Bolt를 통해 프로덕션 코드를 배포하는 기업이 더 많아지도록 도입률을 넓히는 데 집중하고 있습니다. "목표는 PM과 디자이너를 개발 수명 주기에 실제로 참여시켜 프로덕션 코드에 기여할 수 있게 만드는 것입니다. 거의 모든 사람이 바라는 궁극적인 지향점이죠"라고 Elm은 덧붙였습니다.