TuBrief
Subscribed Channels
Videos
Community

Claude Code가 만든 화면 코드를 기존 프로젝트에 안전하게 합치는 방법

TuBrief Editorial
September 12, 2026
0
컴퓨터/소프트웨어

Written with AI assistance from the source video. The video is the authority.

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

Related Video

Claude Code, 올해 역대급 디자인 업그레이드 출시 (완벽 마스터 가이드)10:58

Claude Code, 올해 역대급 디자인 업그레이드 출시 (완벽 마스터 가이드)

Chase AI

More from the community

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

September 13, 2026

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

September 13, 2026

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

September 13, 2026

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

September 13, 2026

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

September 12, 2026

Apple Won the AI Race

September 12, 2026

Comments (0)

Log in to leave a comment

No posts yet

© 2026 . All rights reserved.

TuBrief
Subscribed Channels
Videos
Community
Log in

Claude Code가 만든 화면 코드를 기존 프로젝트에 안전하게 합치는 방법

터미널에서 Claude Code를 켜고 /design을 치면 화면 초안이 몇 초 만에 튀어나온다. 1인 개발자 입장에선 손이 덜 가니 편하다. 문제는 그 코드를 열어보는 순간 시작된다.

프로젝트에 이미 세팅해 둔 shadcn/ui 컴포넌트는 거들떠보지도 않고 날것의 <button> 태그를 새로 짠다. 팔레트에 등록된 시맨틱 색상 대신 bg-[#1e293b] 같은 임의의 헥스 코드를 파일마다 흩뿌려 놓는다. 화면 하나 만들 때마다 엉망이 된 임포트 경로를 바로잡고 인라인 스타일을 지우느라 40분씩 버린다.

기분 탓이 아니다. 2억 1,100만 라인의 커밋을 분석한 GitClear의 2024년 연구를 보면, AI 도구 도입 후 2주 안에 완전히 버려지거나 뜯어고쳐지는 코드 비율이 3.1%에서 5.7%로 두 배 가까이 뛰었다. 리팩토링 비율은 25%에서 10% 밑으로 곤두박질쳤다. 코드가 늘어나는 속도만큼 부채가 쌓인다. 모델이 알아서 기존 디자인 시스템을 지켜줄 거라는 기대를 접고, 시스템 레벨에서 손발을 묶어둬야 한다.


프로젝트 규칙 파일로 컴포넌트 경로 고정하기

Claude Code가 기존 코드를 무시하는 이유는 단순하다. 컨텍스트 윈도우를 아끼려고 필요한 파일만 좁게 훑기 때문이다. 아무런 제약을 주지 않으면 모델은 가장 원시적인 HTML 태그를 조합해 화면을 그린다.

세션 루트의 설정 파일은 프롬프트 캐싱 덕에 기본 입력 비용의 10% 수준으로 유지된다. 대화를 초기화하거나 압축해도 사라지지 않는다. 여기에 컴포넌트 재사용 규칙을 박아두면 모델이 제멋대로 날것의 태그를 만드는 일을 막을 수 있다.

프로젝트 루트의 CLAUDE.md 파일에 공통 컴포넌트 경로와 스타일 규칙을 적는다.

# Design System Guidelines

1. Component Reuse (STRICT)
- DO NOT use raw DOM tags (<button>, <input>, <dialog>).
- MUST import from @/components/ui:
  - Button: import { Button } from "@/components/ui/button"
  - Input: import { Input } from "@/components/ui/input"
  - Card: import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
  - Dialog: import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
- If a component does not exist in @/components/ui, ask to run: "npx shadcn@latest add <component>".

2. Token Boundaries
- NEVER use arbitrary hex codes or pixel widths: NO bg-[#...], NO w-[...px].
- Use Semantic CSS variables:
  - Surfaces: bg-background, bg-card, bg-muted
  - Text: text-foreground, text-muted-foreground, text-primary
  - Borders: border-border, border-input

수천 줄짜리 전역 CSS 파일을 매번 프롬프트에 통째로 넘길 필요는 없다. Tailwind 설정에서 토큰 이름만 골라 JSON으로 뽑아두면 된다.

// scripts/extract-tokens.mjs
import fs from 'fs';
import resolveConfig from 'tailwindcss/resolveConfig.js';
import tailwindConfig from '../tailwind.config.js';

const fullConfig = resolveConfig(tailwindConfig);
const semanticTokens = {
  colors: Object.keys(fullConfig.theme.colors || {}).filter(
    (name) => !['inherit', 'current', 'transparent'].includes(name)
  ),
  spacing: Object.keys(fullConfig.theme.spacing || {}),
  borderRadius: Object.keys(fullConfig.theme.borderRadius || {}),
};

if (!fs.existsSync('.claude')) {
  fs.mkdirSync('.claude');
}

fs.writeFileSync(
  '.claude/design-tokens.json',
  JSON.stringify(semanticTokens, null, 2)
);

이 스크립트를 package.json의 postinstall과 predev에 걸어둔다.

{
  "scripts": {
    "postinstall": "node scripts/extract-tokens.mjs",
    "predev": "node scripts/extract-tokens.mjs"
  }
}

빌드할 때마다 사용 가능한 클래스 목록이 갱신된다. 오타 난 스타일 클래스를 잡느라 쏟던 수작업 시간이 사라진다.


훅 스크립트로 임의 스타일 자동 치환하기

프롬프트에 아무리 주의사항을 적어도 모델은 가끔 엉뚱한 값을 뱉는다. 스크린샷 한 장 던져주고 화면을 만들어달라고 하면 이미지 비율에 맞춰 w-[380px] 같은 고정 폭을 박아버린다. 모바일 화면에서 가로 스크롤이 터지는 주범이다.

WCAG 2.1 AA 기준을 맞추려면 해상도별 기준을 명시하고, 파일이 생성되는 순간 린터로 강제 검사해야 한다.

검증 영역 대상 기준 필수 Tailwind 클래스 차단 조건
모바일 390px (base) flex-col, w-full, grid-cols-1 w-[...px] 고정 폭 사용으로 인한 수평 스크롤
태블릿 768px (md:) md:flex-row, md:grid-cols-2, md:p-6 모바일 1열 구조가 넓은 화면에서 유지될 때
데스크톱 1440px (xl:) xl:max-w-7xl, mx-auto, xl:grid-cols-4 고해상도에서 레이아웃 컨테이너가 무제한 늘어날 때
다크 모드 .dark 셀렉터 bg-background, text-foreground bg-white, text-black 같은 기본 클래스 단독 방치
접근성 WCAG 2.1 AA aria-label, <main>, focus-visible:ring-2 아이콘 버튼에 스크린리더용 대체 텍스트 누락

말 안 듣는 모델을 통제할 때는 생명주기 훅을 쓰는 편이 확실하다. Claude Code의 PostToolUse 훅을 이용하면 파일을 디스크에 쓰는 즉시 스크립트가 실행된다.

.claude/settings.json에 훅 명령을 등록한다.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/ast-lint-guard.mjs",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

이제 검사 스크립트를 작성한다. 정규식으로 흔한 헥스 코드를 잡아 프로젝트 토큰으로 바꾸고, Tailwind ESLint 플러그인으로 규칙을 강제한다.

// .claude/hooks/ast-lint-guard.mjs
import fs from 'fs';
import readline from 'readline';
import { execSync } from 'child_process';

const rl = readline.createInterface({ input: process.stdin });
let inputBuffer = '';

rl.on('line', (line) => { inputBuffer += line; });
rl.on('close', () => {
  try {
    const payload = JSON.parse(inputBuffer);
    const filePath = payload.tool_input?.file_path || payload.tool_input?.path;

    if (!filePath || !/\.(tsx|jsx)$/.test(filePath) || !fs.existsSync(filePath)) {
      process.exit(0);
    }

    let code = fs.readFileSync(filePath, 'utf-8');
    let changed = false;

    const replacementMap = {
      '#ffffff': 'bg-background',
      '#000000': 'text-foreground',
      '#020817': 'bg-background',
      '#0f172a': 'bg-card',
      '#1e293b': 'bg-muted',
      '#64748b': 'text-muted-foreground',
      '#2563eb': 'bg-primary',
    };

    for (const [hex, token] of Object.entries(replacementMap)) {
      const regex = new RegExp(`(bg|text|border)-\\[${hex}\\]`, 'gi');
      if (regex.test(code)) {
        code = code.replace(regex, token);
        changed = true;
      }
    }

    if (changed) {
      fs.writeFileSync(filePath, code, 'utf-8');
    }

    execSync(`npx eslint "${filePath}" --rule "tailwindcss/no-arbitrary-value: error"`, {
      stdio: 'pipe',
    });

    process.exit(0);
  } catch (error) {
    const failureLog = error.stdout?.toString() || error.stderr?.toString() || error.message;
    console.error(`[Lint Pipeline Block] 스타일 규칙 위반:\n${failureLog}`);
    process.exit(1);
  }
});

프로젝트에 린터 플러그인을 설치한다.

npm install -D eslint-plugin-tailwindcss

스크립트가 종료 코드 1을 뱉으면 Claude Code는 에러 로그를 읽고 다음 턴에서 시맨틱 클래스로 코드를 고쳐 쓴다. QA 단계에서 디자인 깨진 화면을 붙잡고 씨름할 일이 줄어든다.


뷰와 비즈니스 로직 물리적으로 떼어놓기

Claude Code한테 화면을 짜라고 시키면 한 파일 안에 fetch 함수, 거대한 가짜 데이터 객체, JSX를 한데 버무려 놓기 일쑤다. 나중에 실제 API를 붙이려면 렌더링 코드까지 다 들어내야 한다.

화면 코드는 상태 변경 방식을 알 필요가 없다. 하나의 기능 디렉터리를 4개 파일로 쪼개고 데이터 스키마를 먼저 확정 짓는 편이 안전하다.

먼저 데이터 규격부터 정의한다.

// src/components/features/dashboard-card/schema.ts
import { z } from "zod";

export const MetricItemSchema = z.object({
  id: z.string(),
  label: z.string(),
  value: z.string(),
  changePercentage: z.number(),
  trend: z.enum(["up", "down", "neutral"]),
});

export const DashboardCardSchema = z.object({
  title: z.string().min(1),
  metrics: z.array(MetricItemSchema),
});

export type DashboardCardData = z.infer<typeof DashboardCardSchema>;

export interface DashboardCardViewProps {
  data: DashboardCardData;
  isLoading?: boolean;
  onActionClick?: (metricId: string) => void;
}

그다음 터미널에서 Claude Code를 호출할 때 내부 상태를 일절 쓰지 말라고 못 박는다.

claude "src/components/features/dashboard-card/schema.ts의 DashboardCardViewProps를 구현하는 순수 UI 컴포넌트 src/components/features/dashboard-card/dashboard-card-view.tsx를 만들어줘. 내부에서 useState, useEffect, fetch는 절대 쓰지 말고 오직 넘겨받은 Props와 @/components/ui 요소만 사용해서 반응형으로 짜줘."

데이터를 붙일 때는 훅으로 감싼다. 목 데이터와 실제 API 호출 함수를 같은 구조로 만들어둔다.

// src/components/features/dashboard-card/use-dashboard-card.ts
import { useQuery } from "@tanstack/react-query";
import { DashboardCardData } from "./schema";

const MOCK_DATA: DashboardCardData = {
  title: "월간 활성 지표",
  metrics: [
    { id: "m-1", label: "신규 유입", value: "1,240명", changePercentage: 12.5, trend: "up" },
    { id: "m-2", label: "이탈률", value: "2.1%", changePercentage: -0.4, trend: "down" },
  ],
};

export const useDashboardCard = (cardId: string, useMock = false) => {
  return useQuery<DashboardCardData>({
    queryKey: ["dashboard-card", cardId],
    queryFn: async () => {
      if (useMock) {
        return MOCK_DATA;
      }
      const res = await fetch(`/api/dashboard/${cardId}`);
      if (!res.ok) throw new Error("데이터 조회 실패");
      return res.json();
    },
  });
};

컨테이너 컴포넌트에서 둘을 조립한다.

// src/components/features/dashboard-card/index.tsx
"use client";

import React from "react";
import { DashboardCardView } from "./dashboard-card-view";
import { useDashboardCard } from "./use-dashboard-card";

export function DashboardCardContainer({ cardId, useMock = false }: { cardId: string; useMock?: boolean }) {
  const { data, isLoading } = useDashboardCard(cardId, useMock);

  if (!data) return null;

  return (
    <DashboardCardView
      data={data}
      isLoading={isLoading}
      onActionClick={(id) => console.log(id)}
    />
  );
}

백엔드가 나오기 전에는 useMock={true}로 화면을 다듬는다. API가 완성되면 플래그만 지운다. 뷰 코드는 단 한 줄도 건드릴 이유가 없다.


작업 트리 격리와 변경 사항 골라 담기

터미널에서 모델과 길게 대화하며 UI를 만지작거리다 보면, 멀쩡하던 전역 설정 파일이 수정되어 있거나 쓰지 않는 임시 파일이 디렉터리 곳곳에 생긴다. 내 작업 공간을 그대로 둔 채 실험 전용 디렉터리를 파서 작업하는 편이 속 편하다.

Git worktree를 쓰면 완전히 분리된 폴더에서 Claude Code를 굴릴 수 있다.

git worktree add ../saas-ui-sandbox -b experiment/ai-dashboard-ui
cd ../saas-ui-sandbox
claude

실험이 망가지면 고민할 필요 없이 폴더째 날린다.

cd ../saas-platform
git worktree remove ../saas-ui-sandbox --force
git branch -D experiment/ai-dashboard-ui

원하는 모양이 나왔다면 브랜치를 통째로 머지하지 말고, 대화형 모드로 뷰 파일의 코드 조각만 골라 가져온다.

git checkout main
git checkout -p experiment/ai-dashboard-ui -- src/components/features/dashboard-card/dashboard-card-view.tsx

터미널에 뜨는 코드 덩어리를 보면서 마음에 드는 부분만 y를 누르고, 이상한 수정은 n으로 쳐낸다.

세션이 5턴을 넘어가면 컨텍스트가 흐려지면서 모델이 헛소리를 하기 시작한다. 그때마다 세션을 정리해 줘야 한다.

  1. 타입 점검: 단일 컴포넌트를 만들고 나면 즉시 npx tsc --noEmit을 돌린다. 타입 에러가 0개일 때만 다음 프롬프트로 넘어간다.
  2. 컨텍스트 압축: 대화가 길어지면 주저 없이 /compact를 쳐서 토큰 낭비를 줄인다.
  3. 세션 비우기: 화면 작업이 끝나면 커밋을 남기고 /clear로 메모리를 완전히 비운다. 꼬였을 때는 /rewind로 이전 체크포인트로 돌아간다.

모델의 생성 능력이 뛰어난 것과 그 코드가 프로덕션에 들어갈 수 있는지는 전혀 다른 문제다. CLAUDE.md로 입력 통로를 좁히고, 생명주기 훅으로 출력 코드를 검증하며, worktree로 작업 공간을 분리해 두면 AI가 뱉어낸 코드를 수습하느라 밤새는 일은 피할 수 있다.