일관성 패턴
AI 생성 UI의 시각적·구조적 일관성을 보장하는 시스템 설계
AI가 생성하는 UI의 일관성은 디자인 시스템의 명확한 제약에 달려 있습니다. 레이아웃 그리드, 스페이싱 시스템, 반응형 전략을 체계화하여 AI가 항상 일관된 결과물을 만들도록 합니다.
일관성 보장 계층
레이아웃 그리드 시스템
그리드 정의
// tokens/grid.ts
export const grid = {
columns: 12,
gutter: {
sm: '16px', // spacing.4
md: '24px', // spacing.6
lg: '32px', // spacing.8
},
margin: {
sm: '16px',
md: '24px',
lg: '32px',
xl: '48px',
},
maxWidth: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
} as constContainer 컴포넌트
// components/layout/container.tsx
interface ContainerProps {
children: React.ReactNode
/** 최대 너비 @default "lg" */
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
/** 좌우 패딩 포함 @default true */
padded?: boolean
/** 중앙 정렬 @default true */
centered?: boolean
}
/**
* 페이지 콘텐츠 컨테이너
* @ai-hint 페이지 최상위 래퍼로 사용. 대부분의 경우 size="lg" 유지
*/
export function Container({
children,
size = 'lg',
padded = true,
centered = true,
}: ContainerProps) {
return (
<div
className={cn(
'w-full',
centered && 'mx-auto',
padded && 'px-4 md:px-6 lg:px-8',
{
'max-w-screen-sm': size === 'sm',
'max-w-screen-md': size === 'md',
'max-w-screen-lg': size === 'lg',
'max-w-screen-xl': size === 'xl',
'max-w-screen-2xl': size === '2xl',
}
)}
>
{children}
</div>
)
}그리드 레이아웃 패턴
// components/layout/grid.tsx
interface GridProps {
children: React.ReactNode
/** 컬럼 수 @default 12 */
cols?: 1 | 2 | 3 | 4 | 6 | 12
/** 반응형 컬럼 설정 */
responsive?: {
sm?: 1 | 2 | 3 | 4 | 6 | 12
md?: 1 | 2 | 3 | 4 | 6 | 12
lg?: 1 | 2 | 3 | 4 | 6 | 12
}
/** 간격 @default "4" */
gap?: '0' | '2' | '4' | '6' | '8'
}
/**
* @ai-hint 그리드 레이아웃 컨테이너
* - 카드 그리드: cols={3} gap="6"
* - 폼 레이아웃: cols={2} gap="4"
* - 대시보드: responsive={{ sm: 1, md: 2, lg: 4 }}
*/
export function Grid({ children, cols = 12, responsive, gap = '4' }: GridProps) {
// 구현
}스페이싱 시스템
스페이싱 스케일
용도별 스페이싱
// tokens/spacing.ts
export const spacing = {
// Primitive
scale: {
0: '0',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
8: '32px',
10: '40px',
12: '48px',
16: '64px',
20: '80px',
},
// Semantic - Component
component: {
/** 인라인 요소 간격 (아이콘-텍스트) */
inline: '{spacing.2}', // 8px
/** 관련 요소 그룹 간격 */
tight: '{spacing.3}', // 12px
/** 기본 내부 패딩 */
padding: '{spacing.4}', // 16px
/** 섹션 내 요소 간격 */
gap: '{spacing.4}', // 16px
/** 카드/모달 내부 패딩 */
inset: '{spacing.6}', // 24px
},
// Semantic - Layout
layout: {
/** 페이지 상하 여백 */
pageY: '{spacing.8}', // 32px
/** 섹션 간 간격 */
section: '{spacing.12}', // 48px
/** 주요 블록 간 간격 */
block: '{spacing.8}', // 32px
},
} as const스페이싱 규칙
## Spacing Rules (CLAUDE.md용)
### Component 내부
- 아이콘-텍스트 간격: `gap-2` (8px)
- 버튼 내부 패딩: `px-4 py-2`
- 카드 내부 패딩: `p-6` (24px)
- 인풋 내부 패딩: `px-3 py-2`
### Component 그룹
- 같은 유형 요소: `gap-2` (8px)
- 폼 필드 간격: `space-y-4` (16px)
- 버튼 그룹: `gap-2` (8px)
### 섹션/레이아웃
- 페이지 상단 여백: `pt-8` (32px)
- 섹션 간 간격: `space-y-12` (48px)
- 카드 그리드 간격: `gap-6` (24px)
### 금지 사항
- 임의의 값 사용 금지: `p-[13px]` ❌
- 홀수 스케일 사용 금지: `gap-7` ❌
- 인라인 스타일 간격 금지반응형 전략
브레이크포인트 시스템
반응형 패턴
// 반응형 그리드 패턴
const responsivePatterns = {
// 카드 그리드: 모바일 1열 → 태블릿 2열 → 데스크톱 3열
cardGrid: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6',
// 사이드바 레이아웃
sidebarLayout: 'flex flex-col lg:flex-row',
sidebar: 'w-full lg:w-64 shrink-0',
main: 'flex-1 min-w-0',
// 폼 레이아웃: 모바일 스택 → 데스크톱 2열
formGrid: 'grid grid-cols-1 md:grid-cols-2 gap-4',
// 히어로 섹션
hero: 'py-12 md:py-20 lg:py-32',
heroTitle: 'text-3xl md:text-4xl lg:text-5xl',
// 숨김/표시
mobileOnly: 'block lg:hidden',
desktopOnly: 'hidden lg:block',
}컴포넌트별 반응형 규칙
## Responsive Rules (CLAUDE.md용)
### Navigation
- 모바일: 햄버거 메뉴 + 드로어
- 데스크톱 (lg+): 수평 메뉴
### Card Grid
- < sm: 1열
- sm ~ lg: 2열
- lg+: 3열 또는 4열
### Form
- 모바일: 전체 너비, 스택 레이아웃
- md+: 2열 그리드 가능
### Dialog/Modal
- 모바일: 전체 화면 (드로어 스타일)
- md+: 중앙 모달
### Table
- 모바일: 카드 리스트로 변환 또는 수평 스크롤
- md+: 표준 테이블
### Typography
- 모바일: 기본 크기
- lg+: 히어로/제목은 스케일업일관성 검증
Layout Linting
// scripts/lint-layout.ts
interface LayoutRule {
name: string
test: (node: TSNode) => boolean
message: string
}
const layoutRules: LayoutRule[] = [
{
name: 'no-arbitrary-spacing',
test: (node) => /\b(p|m|gap|space)-\[\d+px\]/.test(node.text),
message: '임의의 간격 값 대신 토큰을 사용하세요.',
},
{
name: 'container-required',
test: (node) => isPageComponent(node) && !hasContainer(node),
message: '페이지 컴포넌트는 Container로 감싸야 합니다.',
},
{
name: 'consistent-card-padding',
test: (node) => isCard(node) && !hasStandardPadding(node),
message: 'Card 내부 패딩은 p-6을 사용하세요.',
},
]Visual Consistency Score
// scripts/consistency-score.ts
interface ConsistencyMetrics {
spacingVariance: number // 0-100: 낮을수록 좋음
alignmentScore: number // 0-100: 높을수록 좋음
gridCompliance: number // 0-100: 그리드 준수율
typographyScore: number // 0-100: 타입 스케일 준수율
}
function calculateConsistencyScore(metrics: ConsistencyMetrics): number {
const weights = {
spacingVariance: 0.3,
alignmentScore: 0.25,
gridCompliance: 0.25,
typographyScore: 0.2,
}
return (
(100 - metrics.spacingVariance) * weights.spacingVariance +
metrics.alignmentScore * weights.alignmentScore +
metrics.gridCompliance * weights.gridCompliance +
metrics.typographyScore * weights.typographyScore
)
}