AI 코딩 시대의 컴포넌트 관리: UI/UX 가이드 시스템
같은 버튼을 세 번 만들었다
프로젝트가 커지면 이런 일이 생깁니다.
"이 페이지에 로딩 스피너가 필요한데..." 하고 만듭니다. 다른 페이지를 작업하다가 또 필요합니다. 전에 만든 게 있는지 찾아보지만, shared/ui/에 있는지 features/board/components/에 있는지 features/lesson/components/common/에 있는지 기억이 안 납니다. 그냥 하나 더 만듭니다.
사람이 코딩할 때도 이러는데, AI가 코딩하면 더 심합니다. Claude에게 "게시판에 투표 버튼 달아줘"라고 하면, 이미 VoteButtons라는 컴포넌트가 있는지 모릅니다. 새로 만듭니다. Props 구조도 다르고, 스타일도 다릅니다. 코드베이스에 비슷한 컴포넌트가 2개, 3개 쌓입니다.
FullStackFamily는 AI(Claude Code)가 프론트엔드 코드의 상당 부분을 작성합니다. 그래서 이 문제가 사람만 작업하는 프로젝트보다 훨씬 빨리 드러났습니다.
프로젝트에 어떤 컴포넌트가 있는지 한눈에 보고, AI에게도 그 정보를 전달할 수 있는 시스템이 필요했습니다.
화면 구성
3패널 레이아웃입니다.
┌────────────────┬──────────────────────────┬────────────────────┐ │ │ │ │ │ 사이드바 │ Live Preview │ Options Panel │ │ │ (컴포넌트 실시간 렌더) │ (Props 조절) │ │ · 카테고리 분류 │ │ │ │ · 검색 │ ├────────────────────┤ │ · 컴포넌트 목록 │ Variations │ Vibe Prompt / │ │ │ (미리 정의된 변형 그리드) │ Code │ │ │ │ (프롬프트/코드 │ │ │ │ 복사 패널) │ └────────────────┴──────────────────────────┴────────────────────┘
왼쪽에서 컴포넌트를 고르면 가운데에 실제 렌더링 결과가 나옵니다. 오른쪽 상단에서 Props를 바꾸면 미리보기가 즉시 반영됩니다. 오른쪽 하단에서는 현재 설정에 맞는 프롬프트와 코드를 복사할 수 있습니다.
컴포넌트 등록 구조
각 컴포넌트는 ComponentDefinition이라는 타입으로 정의됩니다. 핵심 필드만 보면 이렇습니다.
{ id: 'fsf.button', name: 'Button', category: 'Form Controls', source: 'custom', // custom | html5 | shadcn importPath: '@/shared/ui/Button', render: (props) => <Button {...props} />, options: [ ... ], // Props 타입/기본값 정의 variations: [ ... ], // 미리 정의된 변형들 vibePromptTemplate: '...', // AI용 프롬프트 템플릿 codeTemplate: '...', // 코드 스니펫 템플릿 }
현재 7개 카테고리로 나뉩니다.
┌──────────────────┬──────────────────────────────────────┐ │ 카테고리 │ 주요 컴포넌트 │ ├──────────────────┼──────────────────────────────────────┤ │ Form Controls │ Button, SearchInput, TagInput, ... │ │ Data Display │ Badge, Tag │ │ Feedback │ LoadingSpinner, Modal, PageLoading │ │ Navigation │ Pagination, Tabs │ │ Layout │ Container │ │ Editor/Viewer │ MarkdownEditor, MarkdownViewer │ │ Board Components │ VoteButtons, BookmarkButton, ... │ └──────────────────┴──────────────────────────────────────┘
컴포넌트를 추가할 때는 해당 카테고리의 데이터 파일(features/uiux-guide/data/{category}.tsx)에 ComponentDefinition 하나를 추가하면 됩니다. 사이드바, 프리뷰, 프롬프트 생성이 전부 자동으로 따라옵니다.
이 시스템의 진짜 목적: AI에게 컴포넌트 정보를 전달하기
사이드바나 라이브 프리뷰도 쓸모 있지만, 진짜 만든 이유는 AI 때문입니다. AI가 코드를 쓸 때 기존 컴포넌트를 정확하게 가져다 쓰게 만들고 싶었습니다.
Claude에게 "이 페이지에 버튼 넣어줘"라고 하면 보통 이렇게 답합니다.
<button className="px-4 py-2 bg-blue-500 text-white rounded"> 저장 </button>
프로젝트에 Button 컴포넌트가 있고, variant, size, isLoading 같은 Props를 지원하는데, AI는 그걸 모릅니다. Tailwind로 직접 스타일링하거나, 전혀 다른 Props 구조의 버튼을 새로 만듭니다.
이걸 해결하기 위해 Vibe Prompt를 만들었습니다.
Vibe Prompt: Mustache 템플릿 기반 한글 프롬프트
각 컴포넌트에 vibePromptTemplate이 있습니다. Mustache 문법으로 현재 Props를 한글 프롬프트로 변환합니다.
// 템플릿 FullStackFamily의 Button 컴포넌트를 사용해주세요. {{#variant}}variant는 "{{variant}}"로 설정해주세요.{{/variant}} {{#size}}size는 "{{size}}"로 설정해주세요.{{/size}} {{#isLoading}}로딩 상태로 표시해주세요.{{/isLoading}} {{#children}}텍스트는 "{{children}}"입니다.{{/children}}
Options 패널에서 variant를 "primary", size를 "lg"로 설정하면 이런 프롬프트가 생성됩니다.
FullStackFamily의 Button 컴포넌트를 사용해주세요. variant는 "primary"로 설정해주세요. size는 "lg"로 설정해주세요. 텍스트는 "큰 버튼"입니다.
이걸 복사해서 Claude에게 붙여넣으면, AI가 <button className="...">을 직접 만드는 대신 기존 Button 컴포넌트를 정확한 Props로 사용합니다. import 경로까지 맞게.
Code 탭에서는 같은 설정으로 실제 React 코드도 생성됩니다.
import { Button } from '@/shared/ui/Button' <Button variant="primary" size="lg"> 큰 버튼 </Button>
Props를 바꿀 때마다 프롬프트와 코드가 동시에 갱신됩니다. 화면에서 원하는 모습으로 조절한 뒤 복사해서 AI에게 넘기면 끝입니다.
코드 생성의 세부 처리
코드를 생성할 때 소스 타입에 따라 처리가 다릅니다.
┌────────┬─────────────────────────────────────────┐ │ source │ 처리 │ ├────────┼─────────────────────────────────────────┤ │ custom │ import 문 + JSX (import 경로 포함) │ │ shadcn │ import 문 + JSX (import 경로 포함) │ │ html5 │ JSX만 (import 없음, 네이티브 요소) │ └────────┴─────────────────────────────────────────┘
Props 필터링도 있습니다. 기본값과 같은 Props는 코드에서 생략합니다. disabled={false}를 출력하면 오히려 혼란스러우니까요. boolean이 true일 때는 JSX 관례대로 isLoading={true} 대신 isLoading만 출력합니다.
래퍼 컴포넌트 패턴: useState가 필요한 컴포넌트 처리
문제가 하나 있었습니다.
SearchInput 같은 컴포넌트는 입력값을 useState로 관리해야 합니다. 그런데 UI/UX 가이드의 render 함수는 Props만 받아서 렌더링합니다. Hook을 직접 호출할 수 없습니다. 호출하면 React Error #310이 발생합니다.
해결책은 래퍼 컴포넌트입니다.
┌─────────────────────────────────────────────────────────┐ │ ComponentDefinition │ │ │ │ render: (props) => <SearchInputDemo {...props} /> │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────┐ │ │ │ function SearchInputDemo({ placeholder }) │ │ │ │ const [value, setValue] = useState('') │ ← Hook OK │ │ │ return <SearchInput │ │ │ │ value={value} │ │ │ │ onChange={setValue} │ │ │ │ placeholder={placeholder} │ │ │ │ /> │ │ │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
SearchInputDemo는 일반 React 컴포넌트이므로 useState를 자유롭게 쓸 수 있습니다. ComponentDefinition의 render 함수는 이 래퍼 컴포넌트를 호출할 뿐입니다.
TagInput, VoteButtons, MarkdownEditor 같이 상태가 필요한 컴포넌트는 모두 이 패턴을 씁니다.
Variations: 미리 보여주는 변형
각 컴포넌트에 variations 배열이 있습니다.
variations: [ { name: 'Primary', props: { variant: 'primary' } }, { name: 'Loading', props: { isLoading: true, children: '처리 중...' } }, { name: 'Danger', props: { variant: 'danger', children: '삭제' } }, { name: 'FullWidth', props: { fullWidth: true } }, ]
Preview 영역 아래에 그리드로 표시됩니다. 클릭하면 해당 Props가 Options 패널에 적용되고, 프리뷰와 프롬프트가 동시에 바뀝니다.
"이 컴포넌트 어떤 모습이 가능하지?"를 코드를 읽지 않고 눈으로 확인할 수 있습니다.
실제 개발 워크플로우에서 어떻게 쓰이나
CLAUDE.md에 이런 규칙을 넣었습니다.
프론트엔드 개발 시 공유 UI 컴포넌트는 반드시 아래 절차를 따릅니다. 1단계: /uiux에서 기존 컴포넌트 확인 2단계: 유사 컴포넌트 있으면 하위호환 유지하며 업그레이드, 없으면 새로 구현 3단계: 컴포넌트 생성/수정/삭제 시 /uiux 갱신 필수
Claude Code가 프론트엔드 작업을 시작하면 이 규칙을 읽습니다. "게시판에 북마크 버튼 달아줘"라는 요청이 들어오면:
1. /uiux 페이지에서 BookmarkButton 검색 2. 이미 있음 → import 경로와 Props 확인 3. 기존 컴포넌트를 그대로 사용하거나, 부족한 기능이 있으면 하위호환을 유지하면서 업그레이드 4. 새 컴포넌트를 만들었으면 /uiux 데이터 파일에 등록
"새로 만들지 말고 있는 걸 찾아 써라"를 규칙으로 강제한 겁니다.
사용자가 둘이다
이 시스템에서 좀 재밌는 점이 있습니다. 사용자가 사람과 AI, 둘입니다.
사람은 이 페이지에서 컴포넌트를 눈으로 확인하고, Props를 바꿔보고, 코드를 복사합니다. 시각적 카탈로그로 쓰는 거죠. AI는 같은 페이지를 CLAUDE.md 규칙에 의해 "먼저 여기서 찾아봐라"라는 강제를 받으면서 접근합니다. Vibe Prompt가 컴포넌트명, import 경로, Props를 정확히 알려주니까 중복 구현 없이 기존 걸 가져다 씁니다.
같은 페이지인데 쓰는 방식이 다릅니다.
컴포넌트를 추가하는 과정
새 컴포넌트를 등록하는 과정을 Button 예시로 보면 이렇습니다.
1. 컴포넌트 구현 → shared/ui/Button.tsx 2. ComponentDefinition 작성 → features/uiux-guide/data/form-controls.tsx 에 추가 → id, name, source, importPath, render, options, variations, vibePromptTemplate, codeTemplate 정의 3. 카테고리 배열에 등록 → formControlComponents 배열에 push 4. 끝. 사이드바, 프리뷰, 프롬프트, 코드 탭 전부 자동 생성
카테고리 자체를 추가하려면 categories.ts에 한 줄 추가하면 됩니다. 사이드바 UI가 카테고리 배열을 순회하면서 렌더링하기 때문에 별도 UI 작업이 필요 없습니다.
다크 모드 프리뷰
Preview 상단에 Light/Dark 토글이 있습니다. 배경만 바뀌는 게 아니라 Tailwind의 dark: 클래스가 실제로 적용돼서, 다크 모드에서 깨지는 컴포넌트를 미리 잡을 수 있습니다.
마무리
결국 하고 싶은 말은 하나입니다. "이미 있는 걸 또 만들지 마라."
사람은 기억력에 의존하고, AI는 규칙에 의존합니다. 이 페이지는 둘 다 커버합니다. 컴포넌트가 50개로 늘어나도 "버튼이 몇 종류야?"에 페이지 하나 열면 답이 나옵니다.
AI와 코드를 같이 쓰다 보니, 코드 자체보다 코드에 대한 메타데이터를 관리하는 데 신경을 더 쓰게 됐습니다. 처음엔 좀 이상하다고 느꼈는데, 지금은 이게 맞는 것 같습니다.

댓글
댓글을 작성하려면 이 필요합니다.