Build Pipeline untuk Mengisolasi Desain Eksternal Claude ke dalam Kode Produksi
TuBrief 편집팀
2026년 9월 11일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Bagi seorang developer yang membuat layanan full-stack sendirian tanpa seorang desainer, dokumen keterampilan desain eksternal tampak sangat menarik. Menyalin prompt sistem desain 5.000 baris yang ditemukan di Twitter atau GitHub dan memasukkannya ke dalam instruksi sistem Claude sepertinya akan membuat tampilan layar menjadi rapi.
Kenyataannya justru memberikan hasil yang sebaliknya. Begitu pesan pertama dikirim, peringatan batas token langsung muncul, dan waktu tunggu respons API bertambah tanpa henti. Kode yang dikeluarkan oleh model memasukkan gaya inline misterius ke dalam komponen atau memanggil modul Framer Motion yang bahkan belum pernah diinstal di dalam proyek. Pada akhirnya, Anda akan memperbaiki UI yang rusak secara manual hingga subuh dan kembali menggunakan templat bawaan shadcn/ui yang lama.
Masalahnya bukanlah pada selera estetika model. Penyebab utamanya adalah metode mencemplungkan markdown teks tak berstruktur begitu saja ke dalam prompt sistem. Kita harus mengubah deskripsi gaya berbahasa natural menjadi spesifikasi token yang dapat dibaca oleh mesin, serta membangun pipeline pengujian isolasi sebelum kode yang dihasilkan oleh model menyentuh file produksi.
Jika Anda menempelkan markdown sistem desain secara utuh ke dalam prompt, pemborosan token akan menjadi sangat parah. Pengubah estetika seperti "navy tua yang memberikan rasa percaya kepada pengguna" sama sekali tidak berguna dalam pembuatan tata letak. Kalimat semacam itu menggerogoti sumber daya komputasi di dalam model bahasa yang seharusnya dialokasikan untuk antarmuka TypeScript atau logika validasi yang benar-benar harus dijaga.
Kita memangkas deskripsi bahasa natural dan menyusun ulang prompt sistem dengan objek JSON berstandar W3C Design Tokens Community Group (DTCG). Hanya menyisakan warna, jarak (spacing), dan kelengkungan sudut (radius).
`json
{
"color": {
"background": {
"surface": { "type": "color" },
"canvas": { "type": "color" }
},
"action": {
"primary": { "type": "color" },
"primary-hover": { "type": "color" }
}
},
"spacing": {
"compact": { "type": "dimension" },
"comfortable": { "type": "dimension" }
},
"radius": {
"md": { "type": "dimension" }
}
}
`
Kita mengombinasikan spesifikasi token yang telah distandarisasi ini dengan Anthropic Prompt Caching. Token dasar dan peran sistem dijadikan blok tetap lalu diterapkan 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: Generator komponen Next.js dan Tailwind CSS. Wajib hanya menggunakan kelas yang ditentukan dalam spesifikasi token DTCG di bawah ini dan jangan mengeluarkan gaya inline (atribut style). [DTCG_TOKENS_JSON_DATA],
cache_control: { type: "ephemeral" },
},
],
messages: [
{
role: "user",
content: componentBrief,
},
],
});
}
`
Dengan memangkas dokumen teks biasa, ukuran prompt yang tadinya sekitar 10.000 token berkurang menjadi sekitar 1.200 token. Berdasarkan dokumentasi Anthropic, pembacaan cache prompt hanya dikenakan biaya 10% dari tarif token input dasar, sehingga biaya API berkurang lebih dari 70% saat dipanggil berulang kali.
Saat keterampilan eksternal disuntikkan, Claude sering kali diam-diam menyisipkan nilai sembarangan seperti style={{ marginTop: '13px' }}. Kelas arbitrase kurung siku (w-[342px]) juga dicetak secara serampangan. Kode semacam ini perlahan-lahan merusak sistem gaya global.
Kita menerapkan eslint-plugin-react dan eslint-plugin-tailwindcss pada Flat Config di eslint.config.mjs untuk menyaringnya pada tahap build.
`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: "Gaya inline tidak diperbolehkan. Gunakan utilitas Tailwind yang telah ditentukan.",
},
],
},
],
"tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-custom-classname": [
"error",
{
whitelist: ["animate-."],
},
],
},
},
];
`
Token yang diekstrak dipetakan ke dalam theme.extend di tailwind.config.ts. Menimpa palet dasar akan merusak warna yang dirujuk oleh primitif internal 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;
`
Interaksi diisolasi menggunakan varian Class Variance Authority (CVA). Menyentuh primitif Radix UI secara langsung akan menghapus atribut pohon aksesibilitas (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";
`
Ketika diminta untuk membuat gerakan yang memukau, Claude biasanya akan memanggil Framer Motion karena sudah menjadi kebiasaannya. Berdasarkan pengukuran Bundlephobia, Framer Motion memakan ruang sekitar 60KB bahkan setelah kompresi Gzip. Begitu Anda menambahkan 60KB hanya untuk menggerakkan beberapa tombol, metrik pemuatan awal LCP akan langsung terganggu.
Kita melarang impor pustaka pihak ketiga di dalam prompt sistem dan memastikan hanya menggunakan properti yang khusus untuk thread kompresor peramban (Compositor Thread).
`typescript
// Bentuk yang tidak disarankan karena memicu reflow tata letak di thread utama
//
// Bentuk yang disarankan karena diproses di thread GPU compositor
`
Properti geometris seperti top, left, width, dan height menghitung ulang pohon tata letak pada setiap frame. Di sisi lain, utilitas transform-gpu dan opacity diproses di lapisan independen GPU tanpa melewati thread utama, sehingga mampu mempertahankan 60 FPS secara mulus.
Efek buram latar belakang (backdrop-filter: blur()) juga dihilangkan pada area gulir (scroll). Pada peramban seluler berspesifikasi rendah, filter buram di dalam area pandang gulir menghitung ulang tekstur setiap frame dan menurunkan laju penyegaran layar di bawah 20 FPS. Menggunakan saluran alfa semi-transparan warna solid (bg-background/80 dengan border 1px) alih-alih efek blur dapat mencegah masalah penggunaan memori video.
Jika kode yang dihasilkan Claude langsung dimasukkan ke dalam direktori Next.js app/, tata letak akan rusak dan terjadi kesalahan hidrasi. Kita merendernya terlebih dahulu di lingkungan independen Storybook dan hanya menggabungkan komponen yang lolos pengujian ke dalam produksi.
Kita menyematkan axe-playwright ke dalam .storybook/test-runner.js untuk memeriksa aksesibilitas berdasarkan standar 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"],
},
},
});
},
};
`
Jika pengujian gagal, log kesalahan tersebut dikirimkan kembali ke Claude secara langsung.
`text
[AI Feedback Loop: Prompt Koreksi Presisi Komponen]
Kode yang dihasilkan gagal dalam pengujian aksesibilitas axe-core.
Analisis log tersebut dan keluarkan kode yang hanya memperbaiki cacat sambil mempertahankan struktur yang ada.
[Detail Kegagalan]
Item Pelanggaran: color-contrast
Node yang Gagal:
Alasan: Kontras pencahayaan adalah 2.8:1, gagal memenuhi standar WCAG AA yaitu 4.5:1.
Panduan Solusi: Ganti utilitas teks menjadi text-foreground atau text-action-primary-hover.
`
Kita mengambil cuplikan snapshot regresi visual menggunakan Playwright dan mengonfigurasi skrip shell yang hanya membuat cabang (branch) baru ketika semua pemeriksaan berhasil dilewati.
`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(Verifikasi Snapshot: ${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 "Kesalahan: Harap masukkan nama komponen yang akan diverifikasi."
exit 1
fi
echo "1. Pemeriksaan Lint Guardrail"
pnpm eslint "src/components/ui/${COMPONENT_NAME}.tsx" --max-warnings=0
echo "2. Build Statis Storybook"
pnpm build-storybook --quiet
echo "3. Uji CLI Aksesibilitas Storybook"
pnpm test-storybook
echo "4. Perbandingan Snapshot Regresi Visual Playwright"
pnpm playwright test tests/visual/component-regression.spec.ts
echo "5. Berhasil: Membuat branch dan mengirimkan PR"
git checkout -b "feature/ui-{COMPONENT_NAME}.tsx"
git commit -m "feat(ui): verifikasi isolasi {COMPONENT_NAME}"
gh pr create --title "feat(ui): tambah ${COMPONENT_NAME}" --body "Komponen yang telah lulus uji Storybook A11y dan regresi Playwright."
`
Anda tidak perlu lagi menyalakan alat pengembang peramban dan mengubah-ubah margin beberapa piksel setiap kali tampilan layar menjadi kacau. Dengan menyerahkan aturan verifikasi ke skrip terminal, Anda dapat merancang UI secara aman seorang diri.