TuBrief
구독 채널
비디오
커뮤니티

外部Claudeデザインスキルをプロダクションコードに隔離するビルドパイプライン

TuBrief 편집팀
2026년 9월 11일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

日本語한국어EnglishEspañol中文العربيةहिन्दीDeutschFrançaisPortuguêsРусскийBahasa Indonesia

관련 영상

美しいサイトを実際に構築するために必要なClaudeのとんでもないデザインスキル12:55

美しいサイトを実際に構築するために必要なClaudeのとんでもないデザインスキル

AI LABS

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

外部Claudeデザインスキルをプロダクションコードに隔離するビルドパイプライン

デザイナーなしで一人でフルスタックサービスを作る開発者にとって、外部のデザインスキルドキュメントは魅力的です。TwitterやGitHubで見つけた5,000行のデザインシステムプロンプトをコピーしてClaudeのシステム指示に入れれば、画面が綺麗になりそうな気がします。

実際には正反対の結果になります。最初のメッセージを送信した瞬間にトークン制限の警告が表示され、API応答の待ち時間は果てしなく長くなります。モデルが吐き出したコードは、コンポーネントに正体不明のインラインスタイルを埋め込んだり、プロジェクトにインストールされたこともないFramer Motionモジュールを読み込んだりします。結局、壊れたUIを深夜まで手動で直した挙げ句、既存のshadcn/uiのデフォルトテンプレートに戻ることになります。

問題はモデルの美的感覚ではありません。非構造化されたテキストマークダムをシステムプロンプトにそのまま流し込む方式自体が原因です。自然言語によるスタイルの記述を機械が読めるトークン規格に変更し、モデルが生成したコードがプロダクションファイルに届く前に隔離テストを行うパイプラインを構築する必要があります。

感情的なマークダウンを捨ててW3C DTCG JSON規格に置き換える

デザインシステムのマークダウンをプロンプトに丸ごと貼り付けると、トークンの無駄遣いが深刻になります。「ユーザーに信頼感を与える深いネービー」といった美学的修飾語は、レイアウト生成には何の意味もありません。こうした文章は、言語モデルの内部で本来守るべきTypeScriptインターフェースや検証ロジックに向かうべき演算リソースを削ってしまいます。

自然言語の記述を削ぎ落とし、W3C Design Tokens Community Group(DTCG)規格のJSONオブジェクトでシステムプロンプトを再構成します。色、余白、角の曲率だけを残します。

`json
{
"color": {
"background": {
"surface": { "value":"oklch(0.980.005250)","value": "oklch(0.98 0.005 250)", "value":"oklch(0.980.005250)","type": "color" },
"canvas": { "value":"oklch(1.000)","value": "oklch(1.0 0 0)", "value":"oklch(1.000)","type": "color" }
},
"action": {
"primary": { "value":"oklch(0.550.20250)","value": "oklch(0.55 0.20 250)", "value":"oklch(0.550.20250)","type": "color" },
"primary-hover": { "value":"oklch(0.480.22250)","value": "oklch(0.48 0.22 250)", "value":"oklch(0.480.22250)","type": "color" }
}
},
"spacing": {
"compact": { "value":"0.5rem","value": "0.5rem", "value":"0.5rem","type": "dimension" },
"comfortable": { "value":"1rem","value": "1rem", "value":"1rem","type": "dimension" }
},
"radius": {
"md": { "value":"0.5rem","value": "0.5rem", "value":"0.5rem","type": "dimension" }
}
}

`

定型化したトークン仕様をAnthropicのプロンプトキャッシング(Prompt Caching)と組み合わせます。基本トークンとシステム役割を固定ブロックにし、cache_control: { type: "ephemeral" }を設定します。

`typescript
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

export async function requestComponentGeneration(componentBrief: string) {
return await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 4096,
system: [
{
type: "text",
text: Next.js 및 Tailwind CSS 컴포넌트 생성기. 반드시 아래 DTCG 토큰 명세에 정의된 클래스만 사용하고 인라인 스타일(style 속성)은 출력하지 마십시오. [DTCG_TOKENS_JSON_DATA],
cache_control: { type: "ephemeral" },
},
],
messages: [
{
role: "user",
content: componentBrief,
},
],
});
}

`

テキストドキュメントを削ることで、プロンプトのサイズが10,000トークン前後から1,200トークンレベルに減少します。Anthropicのドキュメントに基づくプロンプトキャッシュの読み込みは、基本入力トークン料金の10%しか請求されないため、繰り返し呼び出す際のAPIコストが70%以上削減されます。

任意のインラインスタイルとクラスの衝突を防ぐリント規則

外部スキルを注入すると、Claudeはしばしば style={{ marginTop: '13px' }} のような任意の値をこっそり仕込みます。大括弧の任意クラス(w-[342px])も乱雑に生成します。このようなコードは、グローバルなスタイルシステムを少しずつ崩壊させます。

eslint.config.mjsのFlat Configに eslint-plugin-react と eslint-plugin-tailwindcss を設定し、ビルド段階で弾き出します。

`javascript
import reactPlugin from "eslint-plugin-react";
import tailwindPlugin from "eslint-plugin-tailwindcss";

export default [
{
files: ["**/.{ts,tsx}"],
plugins: {
react: reactPlugin,
tailwindcss: tailwindPlugin,
},
settings: {
tailwindcss: {
callees: ["cn", "cva"],
config: "./tailwind.config.ts",
},
},
rules: {
"react/forbid-dom-props": [
"error",
{
forbid: [
{
propName: "style",
message: "インラインスタイルは許可されていません。指定されたTailwindユーティリティを使用してください。",
},
],
},
],
"tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-custom-classname": [
"error",
{
whitelist: ["animate-.
"],
},
],
},
},
];

`

抽出したトークンは tailwind.config.ts の theme.extend にマッピングします。デフォルトパレットを上書きすると、shadcn/uiの内部プリミティブが参照する色が壊れてしまいます。

`typescript
import type { Config } from "tailwindcss";
import themeTokens from "./build/tailwind/theme.json";

const config: Config = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
surface: themeTokens.color.background.surface,
canvas: themeTokens.color.background.canvas,
action: {
primary: themeTokens.color.action.primary,
"primary-hover": themeTokens.color.action["primary-hover"],
},
},
borderRadius: {
token: themeTokens.radius.md,
},
},
},
plugins: [require("tailwindcss-animate")],
};

export default config;

`

インタラクションは Class Variance Authority (CVA) のバリアント(Variant)で隔離します。Radix UIのプリミティブを直接触ると、アクセシビリティ(ARIA)ツリーの属性が消えてしまいます。

`typescript
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
"inline-flex items-center justify-center rounded-token text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-action-primary text-white hover:bg-action-hover active:scale-[0.98]",
secondary: "bg-surface text-foreground hover:bg-surface/80",
ghost: "hover:bg-surface text-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-12 px-8 text-base",
},
motion: {
subtle: "transition-all duration-150 ease-out",
expressive: "transition-all duration-300 cubic-bezier(0.16, 1, 0.3, 1)",
},
},
defaultVariants: {
variant: "default",
size: "default",
motion: "subtle",
},
}
);

export interface ActionButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}

export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
({ className, variant, size, motion, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, motion, className }))}
ref={ref}
{...props}
/>
);
}
);
ActionButton.displayName = "ActionButton";

`

バンドルサイズを守るアニメーションの制限

Claudeは華やかなモーションを求められると、習慣的にFramer Motionを読み込みます。Bundlephobiaの測定基準によると、Framer MotionはGzip圧縮後でも約60KBを占めます。ボタンをいくつか動かすために60KBを追加した瞬間、初期読み込み指標であるLCPが揺らぎます。

システムプロンプトでサードパーティライブラリのインポートを禁止し、ブラウザコンポジタースレッド(Compositor Thread)専用のプロパティのみを使用するように制限します。

`typescript
// メインスレッドでレイアウトのリフローを引き起こす非推奨の形式
//

// GPUコンポジタースレッドで処理する推奨の形式

`

top、left、width、height などの幾何学的プロパティは、フレームごとにレイアウトツリーを再計算します。一方、transform-gpu ユーティリティと opacity はメインスレッドを経由せず、GPU独立レイヤーで処理されるため、60 FPSをスムーズに維持します。

背景のぼかし効果(backdrop-filter: blur())もスクロール領域では除外します。低スペックのモバイルブラウザでは、スクロールビューポート内のブラーフィルターが毎フレームテクスチャを再計算し、画面のリフレッシュレートを20 FPS以下に低下させます。ブラーの代わりに単色の半透明アルファチャネル(bg-background/80 と1pxのボーダー)を使用することで、ビデオメモリの占有問題を防ぐことができます。

StorybookとPlaywrightを通る自動検証パイプライン

Claudeが生成したコードを Next.js の app/ ディレクトリに直接プッシュすると、レイアウトが崩れてハイドレーションエラーが発生します。Storybookの独立環境でまずレンダリングし、テストを通過したコンポーネントのみをプロダクションにマージします。

.storybook/test-runner.js に axe-playwright を組み込み、WCAG 2.1 AA基準のアクセシビリティを確認します。

`javascript
const { injectAxe, checkA11y } = require("axe-playwright");

module.exports = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await page.waitForSelector("#storybook-root", { state: "attached" });

await checkA11y(page, "#storybook-root", {
  detailedReport: true,
  axeOptions: {
    runOnly: {
      type: "tag",
      values: ["wcag2a", "wcag2aa"],
    },
  },
});

},
};

`

テストが失敗した場合、エラーログをそのままClaudeに投げ返します。

`text
[AI フィードバックループ:コンポーネント精密校正プロンプト]
生成されたコードが axe-core アクセシビリティテストに失敗しました。
ログを分析し、既存の構造を維持したまま欠陥のみを修正したコードを出力してください。

[失敗の詳細]
違反項目: color-contrast
失敗ノード:
理由: 明度対比が 2.8:1 で、WCAG AA基準の 4.5:1 に満たない。
解決指針: テキストユーティリティを text-foreground または text-action-primary-hover に置き換えてください。

`

Playwrightでビジュアルリグレッション(Visual Regression)のスナップショットを撮影し、すべての検査を通過した場合のみブランチを切るシェルスクリプトを構成します。

`typescript
import { test, expect } from "@playwright/test";
import storybookManifest from "../../storybook-static/index.json";

const stories = Object.values(storybookManifest.entries).filter(
(entry) => entry.type === "story"
);

for (const story of stories) {
test(スナップショット検証: ${story.title} - ${story.name}, async ({ page }) => {
await page.goto(/iframe.html?id=${story.id}&viewMode=story);
await page.waitForSelector("#storybook-root");
await page.waitForLoadState("networkidle");

await expect(page).toHaveScreenshot(`${story.id}.png`, {
  animations: "disabled",
  maxDiffPixelRatio: 0.01,
  threshold: 0.2,
});

});
}

`

`bash
#!/usr/bin/env bash
set -e

COMPONENT_NAME=$1

if [ -z "$COMPONENT_NAME" ]; then
echo "エラー: 検証するコンポーネント名を入力してください。"
exit 1
fi

echo "1. リントガードレイル検査"
pnpm eslint "src/components/ui/${COMPONENT_NAME}.tsx" --max-warnings=0

echo "2. Storybook静的ビルド"
pnpm build-storybook --quiet

echo "3. StorybookアクセシビリティCLIテスト"
pnpm test-storybook

echo "4. Playwright視覚的リグレッションスナップショット比較"
pnpm playwright test tests/visual/component-regression.spec.ts

echo "5. 合格: ブランチ作成およびPR送信"
git checkout -b "feature/ui-COMPONENTNAME"gitadd"src/components/ui/{COMPONENT_NAME}" git add "src/components/ui/COMPONENTN​AME"gitadd"src/components/ui/{COMPONENT_NAME}.tsx"
git commit -m "feat(ui): COMPONENTNAME隔離検証完了"gitpushorigin"feature/ui−{COMPONENT_NAME} 隔離検証完了" git push origin "feature/ui-COMPONENTN​AME隔離検証完了"gitpushorigin"feature/ui−{COMPONENT_NAME}"
gh pr create --title "feat(ui): ${COMPONENT_NAME} 追加" --body "Storybook A11y 및 Playwright 회귀 검사를 통과한 컴포넌트입니다."

`

画面が崩れるたびにブラウザの開発者ツールを開いて余白を数ピクセルずついじる必要はありません。検証ルールをターミナルスクリプトに任せておけば、一人でも安全にUIを生成し続けることができます。