소프트웨어 엔지니어를 위한 디자인 원칙
소프트웨어 엔지니어를 위한 디자인 기본기를 알아보세요. 타이포그래피, 색상, 간격, 애니메이션, AI 인터페이스 패턴, 디자인 토큰과 최신 제품 사례 연구 48개를 다룹니다.
2026년 5월 18일 업데이트됨
2026년 5월 18일 업데이트: Design Studies 색인을 현재 48개 연구 자료에 맞춰 새로 고치고, AI Interface Patterns에 agentic workflow 근거를 보강했어요. 여기에는 체크포인트, 승인 상태, 출처 신뢰도, 눈에 보이는 검증이 포함돼요.
2026년 5월 업데이트: WCAG 3.0의 2026년 3월 working draft와 WCAG 2.2 및 3.0이 함께 유지된다는 W3C의 입장에 대한 메모를 추가했어요. 현재 접근성 작업에서는 여전히 WCAG 2.2를 기준으로 삼아야 해요.
2026년 2월 업데이트: 새로운 섹션 2개를 추가했어요. Interaction Patterns에는 Framer, Flighty, Halide, Warp, Bear, Craft, Superhuman을 연구해 정리한 8가지 패러다임을 담았고, AI Interface Patterns에는 citation-forward design, 스트리밍 단계, Perplexity에서 확인한 오류 투명성을 담았어요. Web Patterns는 anchor positioning, scroll-driven animations, @starting-style을 반영해 2026년 기준으로 업데이트했어요. 접근성 내용도 WCAG 2.2의 ISO 표준화를 반영하도록 업데이트했어요. 뛰어난 제품 48개의 심층 분석은 Design Studies를 참고하세요.
저는 소프트웨어를 만들며 수년 동안 디자인을 공부해 왔고, Dieter Rams 같은 전설적인 인물의 원칙을 익히고 Linear, Stripe, Raycast 같은 제품의 인터페이스를 분석해 왔어요. 이 가이드는 제가 소프트웨어의 모습과 느낌에 관심을 갖기 시작했을 때 있었으면 했던 종합 참고 자료로, 그동안의 이해를 정리한 것입니다.
디자인은 장식이 아니에요. 커뮤니케이션이에요. 모든 픽셀은 기능, 위계, 의미를 전달해요. 아마추어처럼 느껴지는 소프트웨어와 전문적으로 느껴지는 소프트웨어의 차이는 이런 원칙을 이해하고 일관되게 적용하는 데서 나옵니다.
이 가이드는 이미 코드를 작성할 수 있다고 가정해요. 그리고 보는 법을 알려줍니다. 왜 어떤 인터페이스는 자연스럽고 어떤 인터페이스는 혼란스럽게 느껴지는지, 더 중요하게는 그런 자연스러운 인터페이스를 어떻게 만들 수 있는지를 이해하도록 돕습니다.
목차
Part 1: 기초
Part 2: 인터랙션 & AI
Part 3: 디자인 철학
Part 4: 구현
Part 5: 참고자료
Gestalt 심리학
“전체는 부분의 합과는 다른 무언가이다.” — Kurt Koffka
Gestalt 심리학은 1920년대 독일에서 발전한 이론으로, 인간이 시각 정보를 어떻게 인식하는지 설명해요. 뇌는 개별 픽셀을 보지 않고, 요소들을 의미 있는 패턴으로 조직해요. 이 원칙들을 익히면 사용자가 인터페이스를 어떻게 인식할지 제어할 수 있어요.
근접성
가까이 있는 요소들은 하나의 그룹으로 인식돼요.
UI 디자인에서 가장 강력한 Gestalt 원칙이에요. 여백은 다른 어떤 시각적 속성보다 관계를 더 효과적으로 전달해요.
잘못된 예 (동일한 간격 = 그룹핑 없음):
┌─────────────────┐
│ Label │
│ │
│ Input Field │
│ │
│ Label │
│ │
│ Input Field │
└─────────────────┘
올바른 예 (불균등한 간격 = 명확한 그룹):
┌─────────────────┐
│ Label │
│ Input Field │ ← 좁은 간격(4px) - 관련 요소
│ │
│ │ ← 넓은 간격(24px) - 그룹 분리
│ Label │
│ Input Field │ ← 좁은 간격(4px) - 관련 요소
└─────────────────┘
CSS 구현:
.form-group {
margin-bottom: 24px; /* Between groups: wide */
}
.form-group label {
margin-bottom: 4px; /* Label to input: tight */
display: block;
}
SwiftUI 구현:
VStack(alignment: .leading, spacing: 4) { // Tight within group
Text("Email")
.font(.caption)
.foregroundStyle(.secondary)
TextField("you@example.com", text: $email)
.textFieldStyle(.roundedBorder)
}
.padding(.bottom, 24) // Wide between groups
유사성
시각적 특성을 공유하는 요소들은 관련된 것으로 보여요.
요소가 같은 모습이면 사용자는 같은 기능을 한다고 추정해요. 디자인 시스템에서 일관된 버튼 스타일, 카드 처리, 타이포그래피를 사용하는 이유가 바로 이것이에요.
네비게이션 예시:
┌───────────────────────────────────┐
│ [Dashboard] [Projects] [Settings] │ ← 같은 스타일 = 같은 기능
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Card │ │Card │ │Card │ │ ← 같은 스타일 = 같은 콘텐츠 유형
│ └─────┘ └─────┘ └─────┘ │
│ │
│ [+ New Project] │ ← 다른 스타일 = 다른 기능
└───────────────────────────────────┘
전경-배경 관계
콘텐츠는 배경과 명확히 구분되어야 해요.
뇌는 “전경”(집중할 대상)과 “배경”(뒷면)을 구별해야 해요. 전경-배경 관계가 불분명하면 시각적 혼란이 생겨요.
기법: - 대비 (밝은 전경 위에 어두운 배경, 또는 그 반대) - 그림자 (전경을 배경 위로 들어올림) - 테두리 (전경의 경계를 구분) - 블러 (배경을 흐리게, 전경을 선명하게)
/* Strong figure-ground relationship */
.card {
background: var(--color-surface); /* Figure */
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* Elevation */
}
.modal-overlay {
background: rgba(0, 0, 0, 0.5); /* Dim ground */
backdrop-filter: blur(4px); /* Blur ground */
}
공통 영역
경계 안에 있는 요소들은 그룹으로 인식돼요.
시각적 컨테이너(카드, 박스, 테두리가 있는 영역) 안에 요소를 넣으면 서로 관련이 있다는 신호를 줘요.
연속성
눈은 경로, 선, 곡선을 따라가요.
정렬과 시각적 흐름을 활용하면 인터페이스 전체에 걸쳐 시선을 유도할 수 있어요.
정렬의 연속성:
┌────────────────────────────────┐
│ Logo [Nav] [Nav] [Nav] │ ← 수평축으로 정렬
├────────────────────────────────┤
│ │
│ Headline │
│ ───────────────────────────── │ ← 시선이 왼쪽 끝을 따라감
│ Paragraph text continues │
│ along the same left edge │
│ │
│ [Primary Action] │ ← 여전히 왼쪽 끝에 위치
└────────────────────────────────┘
폐합
뇌는 불완전한 형태를 완성해요.
사용자는 모든 픽셀이 그려져 있을 필요가 없어요—익숙한 형태는 머릿속에서 자동으로 완성해요. 이를 활용하면 더 미니멀하고 우아한 디자인이 가능해요.
/* Horizontal scroll with partial card (closure) */
.card-carousel {
display: flex;
gap: 16px;
overflow-x: auto;
padding-right: 48px; /* Show partial card = scroll hint */
}
.card-carousel .card {
flex: 0 0 280px; /* Fixed width, partial visible */
}
Gestalt 빠른 참조
| 원칙 | 규칙 | 주요 활용 |
|---|---|---|
| 근접성 | 관련 = 가깝게, 비관련 = 멀게 | 폼 필드, 콘텐츠 섹션 |
| 유사성 | 같은 외관 = 같은 기능 | 버튼, 카드, 네비게이션 |
| 전경-배경 | 레이어의 명확한 분리 | 카드, 모달, 오버레이 |
| 공통 영역 | 경계가 콘텐츠를 묶음 | 설정 섹션, 사용자 카드 |
| 연속성 | 선과 정렬을 따라감 | 타임라인, 읽기 흐름 |
| 폐합 | 뇌가 형태를 완성함 | 아이콘, 스크롤 힌트, 스켈레톤 |
타이포그래피
“타이포그래피는 인간의 언어에 영속적인 시각적 형태를 부여하는 기술이다.” — Robert Bringhurst
타이포그래피는 인터페이스 디자인의 기반이에요. 텍스트는 기능, 위계, 브랜드를 전달해요. 부실한 타이포그래피는 인터페이스를 사용하기 어렵게 만들고, 훌륭한 타이포그래피는 눈에 띄지 않아요—그저 자연스럽게 작동할 뿐이에요.
타입 스케일
일관된 스케일은 시각적 조화를 만들어요. 수학적 비율을 사용하세요.
1.25 스케일 (UI에 권장):
:root {
/* Base: 16px (1rem) */
--text-xs: 0.64rem; /* 10.24px - use sparingly */
--text-sm: 0.8rem; /* 12.8px - captions, labels */
--text-base: 1rem; /* 16px - body text */
--text-lg: 1.25rem; /* 20px - lead text */
--text-xl: 1.563rem; /* 25px - h4 */
--text-2xl: 1.953rem; /* 31.25px - h3 */
--text-3xl: 2.441rem; /* 39px - h2 */
--text-4xl: 3.052rem; /* 48.8px - h1 */
}
행간 (Leading)
행간은 가독성에 큰 영향을 미쳐요. 콘텐츠 유형에 따라 다른 행간이 필요해요.
| 콘텐츠 유형 | 행간 | 이유 |
|---|---|---|
| 헤드라인 | 1.1 - 1.2 | 짧고 굵은 텍스트 |
| UI 텍스트 | 1.3 - 1.4 | 라벨, 버튼 |
| 본문 | 1.5 - 1.7 | 읽기 쉬운 단락 |
| 장문 콘텐츠 | 1.7 - 2.0 | 아티클, 문서 |
줄 길이 (Measure)
적절한 줄 길이는 눈의 피로를 줄이고 읽기 이해도를 높여요.
- 최적: 한 줄당 45-75자
- 목표: 50-65자
- 절대 최대: 85자
p {
max-width: 65ch; /* ch unit = width of '0' character */
}
.article-body {
max-width: 70ch;
margin: 0 auto;
}
글꼴 선택
시스템 글꼴 우선. 즉시 로드되고, 플랫폼과 어울리며, 화면에 최적화되어 있어요.
:root {
--font-sans: system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: ui-monospace, 'SF Mono', 'Cascadia Code',
'JetBrains Mono', Consolas, monospace;
}
커스텀 글꼴을 사용할 때: - 브랜드 차별화 (마케팅 사이트) - 에디토리얼/출판 느낌 - 시스템 글꼴로 달성할 수 없는 특정 디자인 의도
글꼴 굵기로 위계 표현
크기뿐 아니라 굵기를 활용해 위계를 잡으세요.
h1 { font-weight: 700; } /* Bold */
h2 { font-weight: 600; } /* Semibold */
h3 { font-weight: 600; } /* Semibold */
.lead { font-weight: 500; } /* Medium */
p { font-weight: 400; } /* Regular */
.meta { font-weight: 400; color: var(--text-muted); }
타이포그래피 빠른 참조
| 속성 | 본문 | 헤드라인 | UI 라벨 |
|---|---|---|---|
| 크기 | 16-18px | 24-48px | 12-14px |
| 굵기 | 400 | 600-700 | 500 |
| 행간 | 1.5-1.7 | 1.1-1.2 | 1.3-1.4 |
| 줄 길이 | 45-75ch | N/A | N/A |
| 정렬 | 왼쪽 | 가운데 가능 | 왼쪽 |
색채 이론
“색상은 영혼에 직접 영향을 미치는 힘입니다.” — Wassily Kandinsky
색상은 말보다 빠르게 메시지를 전달합니다. 분위기를 만들고, 주의를 이끌며, 의미를 알리고, 브랜드 인지도를 쌓습니다.
60-30-10 규칙
균형 잡힌 인터페이스를 위한 가장 신뢰할 수 있는 색상 배분 방식입니다.
┌──────────────────────────────────────────┐
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ 60% - Dominant (Background)
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░│ 30% - Secondary (Cards, sections)
│░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░│
│░░░░░▓▓▓▓▓▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓▓░░░░░░░░│ 10% - Accent (Buttons, links)
└──────────────────────────────────────────┘
색상 팔레트 만들기
모든 인터페이스에는 다음 의미 기반 색상이 필요합니다:
:root {
/* Brand */
--color-primary: hsl(220, 80%, 50%);
--color-primary-hover: hsl(220, 80%, 45%);
/* Semantic */
--color-success: hsl(142, 76%, 36%); /* Green - positive */
--color-warning: hsl(38, 92%, 50%); /* Amber - caution */
--color-error: hsl(0, 84%, 60%); /* Red - danger */
/* Neutrals */
--color-background: hsl(0, 0%, 100%);
--color-surface: hsl(220, 14%, 96%);
--color-border: hsl(220, 13%, 91%);
/* Text */
--color-text: hsl(220, 13%, 13%);
--color-text-secondary: hsl(220, 9%, 46%);
--color-text-muted: hsl(220, 9%, 64%);
}
색채 심리
| 색상 | 심리 | UI 활용 |
|---|---|---|
| 파란색 | 신뢰, 안정감, 차분함 | 금융, 기술, 기업용 |
| 초록색 | 성장, 자연, 성공 | 헬스케어, 친환경, 긍정 상태 |
| 빨간색 | 에너지, 긴급성, 위험 | 알림, 세일, 오류 |
| 주황색 | 따뜻함, 열정 | CTA, 경쾌한 브랜드 |
| 노란색 | 낙관, 주의 | 경고, 강조 |
| 보라색 | 고급스러움, 창의성 | 프리미엄 제품 |
| ### 다크 모드 우선 디자인 (Vercel) | ||
| Vercel은 먼저 다크 모드를 기준으로 디자인한 다음 라이트 모드를 도출해요. 이렇게 하면 다크 모드가 나중에 덧붙이는 요소가 아니라 핵심 고려 사항이 되기 때문에 더 나은 다크 인터페이스를 만들 수 있어요. |
/* Design dark first, derive light */
:root {
/* Dark mode defaults */
--color-background: hsl(0, 0%, 0%);
--color-surface: hsl(0, 0%, 7%);
--color-border: hsl(0, 0%, 15%);
--color-text: hsl(0, 0%, 93%);
--color-text-secondary: hsl(0, 0%, 63%);
}
@media (prefers-color-scheme: light) {
:root {
--color-background: hsl(0, 0%, 100%);
--color-surface: hsl(0, 0%, 97%);
--color-border: hsl(0, 0%, 89%);
--color-text: hsl(0, 0%, 9%);
--color-text-secondary: hsl(0, 0%, 40%);
}
}
사용 시점: 개발자 도구, 미디어 앱, 대시보드처럼 사용자가 오래 머물고 다크 모드가 눈의 피로를 줄여 주는 맥락에서 사용하세요.
접근성 대비
| 수준 | 일반 텍스트 | 큰 텍스트 | UI 컴포넌트 |
|---|---|---|---|
| AA | 4.5:1 | 3:1 | 3:1 |
| AAA | 7:1 | 4.5:1 | N/A |
| WCAG 2.2는 2025년 10월에 ISO 표준(ISO/IEC 40500:2025)이 되었고, focus visibility, redundant entry, accessible authentication 기준이 추가되었어요. 주요 추가 사항은 다음과 같아요. focus indicators는 다른 콘텐츠에 완전히 가려지면 안 되고(2.4.11), 인증은 cognitive function tests에만 의존하면 안 돼요(3.3.8). W3C는 2024년 12월에 업데이트된 WCAG 2.2를 공개했으며, 이는 2026년 말까지 ISO/IEC 40500:2026으로 배포될 것으로 예상돼요. |
WCAG 3.0은 아직 작업 초안이에요. 최신 개정판은 2026년 3월에 배포되었고, WCAG 2.2를 대체하지 않아요. W3C는 두 표준이 함께 유지될 것이라고 밝혔어요. Accessibility Guidelines Working Group은 2026년에 예상 WCAG 3 일정을 발표할 계획이지만, 현재 작업에서 목표로 삼아야 할 표준은 여전히 WCAG 2.2예요.
도구: WebAIM Contrast Checker, Chrome DevTools color picker
시각적 위계
“디자인은 브랜드의 조용한 대사입니다.” — Paul Rand
시각적 위계는 사용자가 무엇을 첫 번째, 두 번째, 세 번째로 보게 되는지를 제어해요. 명확한 위계가 없으면 사용자는 정보를 찾기 위해 애써야 해요. 위계가 있으면 인터페이스는 자연스럽고 쉽게 느껴져요.
위계의 6가지 도구
1. 크기 — 더 큰 요소가 먼저 시선을 끕니다
.hero-title { font-size: 3rem; } /* Dominant */
.section-title { font-size: 1.5rem; } /* Secondary */
.body-text { font-size: 1rem; } /* Baseline */
2. 굵기 — 굵은 요소는 앞으로 튀어나와 보이고, 가벼운 요소는 뒤로 물러나 보입니다
h1 { font-weight: 700; }
.lead { font-weight: 500; }
p { font-weight: 400; }
3. 색상과 대비 — 높은 대비 = 주목
.title { color: var(--color-text); } /* Near black */
.meta { color: var(--color-text-muted); } /* Gray */
4. 위치 — 핵심 위치가 중요합니다
F-PATTERN (content pages): Z-PATTERN (landing pages):
████████████████████████ 1 ──────────────────► 2
████████ ↘
████ ↘
██ ↘
3 ──────────────────► 4
5. 여백 — 고립된 요소는 중요해 보입니다
.hero { padding: 120px 48px; } /* Generous space */
.data-table { padding: 12px; } /* Dense content */
6. 깊이와 높이 — 앞으로 튀어나와 보이는 요소는 시선을 요구합니다
:root {
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
}
.card { box-shadow: var(--shadow-sm); }
.card:hover { box-shadow: var(--shadow-md); }
.modal { box-shadow: var(--shadow-lg); }
적용 패턴
협업 중 존재감(Figma): 이름 라벨이 붙은 여러 사용자 커서, 선택 강조 표시, component 윤곽선이 살아 있는 문서를 만들어 줍니다. 각 협업자의 색은 서로 구분되지만 비중은 동일해서, 어떤 커서도 다른 커서보다 더 두드러지지 않습니다.
은은한 상태 표시기(Vercel): 배포 상태는 방해가 되는 알림 대신 미묘하고 지속적인 표시기를 사용합니다. 상단의 얇은 색상 막대가 workflow를 끊지 않으면서 상태(빌드 중, 배포됨, 실패)를 전달합니다.
현실 세계 디자인 비유(Flighty): 비행 진행 시각화는 물리적인 항공 계기판을 반영합니다. 고도 곡선, 속도 표시기, 게이트 지도는 추상적인 진행 막대 대신 익숙한 시각적 은유를 사용합니다.
눈을 가늘게 뜨고 보는 테스트
디자인을 눈을 가늘게 뜨고 보세요. 그래도 hierarchy가 보이나요? 그렇다면 강한 디자인입니다.
간격과 리듬
“여백은 공기와 같습니다. 디자인이 숨 쉬려면 꼭 필요합니다.” — Wojciech Zieliński
간격은 디자인의 보이지 않는 구조입니다. 일관된 간격은 시각적 리듬을 만들어 요소들이 하나의 coherent system 안에 함께 속해 있다는 느낌을 줍니다.
8px 그리드
8px 그리드는 다음 이유로 업계 표준입니다: - 고르게 나누어집니다 (8, 16, 24, 32, 40, 48…) - 일반적인 화면 밀도와 잘 맞습니다 (1x, 1.5x, 2x, 3x) - 계산 없이 일관된 리듬을 만듭니다
:root {
--space-1: 4px; /* Tight: icon gaps */
--space-2: 8px; /* Compact: inline elements */
--space-3: 12px; /* Snug: form fields */
--space-4: 16px; /* Default: most gaps */
--space-6: 24px; /* Spacious: card padding */
--space-8: 32px; /* Section gaps */
--space-12: 48px; /* Major sections */
--space-16: 64px; /* Page sections */
--space-20: 80px; /* Hero spacing */
}
내부 간격과 외부 간격
내부 (padding): 요소 안쪽의 공간 외부 (margin): 요소 사이의 공간
원칙: 관련된 그룹 안에서는 보통 내부 간격이 외부 간격보다 커야 합니다.
.card {
padding: 24px; /* Internal: spacious */
margin-bottom: 16px; /* External: less than padding */
}
컴포넌트 간격 패턴
카드:
.card { padding: 24px; border-radius: 12px; }
.card-header { margin-bottom: 16px; }
.card-title { margin-bottom: 4px; } /* Tight to subtitle */
버튼:
.btn { padding: 12px 24px; border-radius: 8px; }
.btn--sm { padding: 8px 16px; }
.btn--lg { padding: 16px 32px; }
.btn-group { display: flex; gap: 12px; }
폼:
.form-row { margin-bottom: 24px; }
.form-label { margin-bottom: 4px; }
.form-help { margin-top: 4px; }
.form-actions { margin-top: 32px; display: flex; gap: 12px; }
간격 빠른 참조
| 맥락 | 권장 간격 |
|---|---|
| 아이콘과 텍스트 사이 | 4-8px |
| 레이블과 입력 필드 사이 | 4px |
| 폼 그룹 사이 | 24px |
| 카드 padding | 20-24px |
| 카드 gap | 16-24px |
| 섹션 padding (모바일) | 48-64px |
| 섹션 padding (데스크톱) | 80-96px |
| 버튼 padding (가로/세로) | 24px / 12px |
| — |
애니메이션 원칙
“애니메이션은 움직이는 그림의 예술이 아니라, 그려진 움직임의 예술이다.” — Norman McLaren
애니메이션은 인터페이스에 생명을 불어넣어요. 잘 활용하면 시선을 유도하고, 상태를 전달하며, 감정적 연결을 만들어 줘요. 잘못 사용하면 사용자를 짜증나게 하고 주의를 분산시켜요.
핵심 원칙
애니메이션은 장식이 아니라 필연적으로 느껴져야 해요.
좋은 애니메이션: 1. 정적인 디자인으로는 전달할 수 없는 정보를 전달해요 2. 관계를 시각적으로 보여줌으로써 인지 부하를 줄여요 3. 자연스럽고 예상 가능하게 느껴져요 4. 의식하지 못할 정도로 자연스럽게 녹아들어요
나쁜 애니메이션: 1. 단지 “멋져 보이기 때문에” 존재해요 2. 사용자를 느리게 만들어요 3. 그 자체로 시선을 끌어요 4. 불안감이나 초조함을 유발해요
UI를 위한 핵심 원칙
1. 예비 동작(Anticipation) — 사용자에게 다음에 일어날 일을 준비시켜요.
.button {
transition: transform 0.1s ease-out;
}
.button:active {
transform: scale(0.97); /* Slight press before action */
}
2. 관성(Follow-Through) — 모션이 스프링처럼 자연스럽게 안착하도록 해요.
.panel {
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
isOpen = true
}
3. Ease-In, Ease-Out — 자연계에서 일정한 속도로 움직이는 것은 없어요.
| 곡선 | 사용 시점 | 특성 |
|---|---|---|
ease-out |
요소 진입 시 | 빠르게 시작, 부드럽게 정지 |
ease-in |
요소 퇴장 시 | 부드럽게 시작, 빠르게 퇴장 |
ease-in-out |
상태 전환 시 | 전체적으로 부드러움 |
linear |
로딩 인디케이터 | 연속적이고 기계적 |
4. 연출(Staging) — 중요한 것에 시선을 유도하세요. 그룹으로 안무된 경우가 아니라면 한 번에 하나만 움직여야 해요.
5. 시차 배치(Staggering) — 요소들은 한꺼번에가 아니라 순차적으로 등장해야 해요.
.list-item {
animation: fadeSlideIn 0.3s ease-out both;
}
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
.list-item:nth-child(4) { animation-delay: 150ms; }
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
타이밍 가이드라인
| 지속 시간 | 사용 사례 | 느낌 |
|---|---|---|
| 50-100ms | 마이크로 인터랙션 (호버, 누르기) | 즉각적인 피드백 |
| 150-200ms | 간단한 상태 변화 (토글, 선택) | 경쾌함 |
| 250-350ms | 중간 전환 (패널 슬라이드, 카드 플립) | 부드러움 |
| 400-500ms | 큰 움직임 (페이지 전환, 모달) | 의도적 |
성능: 황금 규칙
transform과 opacity만 애니메이션하세요 — 이 속성들은 GPU 가속을 활용하며 레이아웃을 트리거하지 않아요.
/* BAD: Animating layout */
.panel { transition: left 0.3s, width 0.3s; }
/* GOOD: Using transform */
.panel { transition: transform 0.3s; }
애니메이션을 적용하지 말아야 할 때
-
사용자가
prefers-reduced-motion을 활성화한 경우css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } -
애니메이션이 정보를 추가하지 않는 경우 — 불필요한 스피너, 튀는 요소들
- 사용자가 급한 경우 — 에러 상태, 폼 유효성 검사, 검색 결과
- 반복 작업을 느리게 만드는 경우 — 키보드 단축키는 애니메이션을 건너뛰어야 해요
- 데이터가 이미 로드된 경우 — Bear는 콘텐츠를 미리 로드하여 로딩 상태를 전혀 사용하지 않아 앱이 즉각적으로 느껴져요. 미리 로드할 수 있다면 스켈레톤이나 스피너를 아예 건너뛰세요.
// Bear's approach: preload so no loading state is needed
struct NoteListView: View {
@Query var notes: [Note] // SwiftData loads from disk instantly
// No loading state, no skeleton, no spinner — data is always there
var body: some View {
List(notes) { note in
NoteRow(note: note)
}
}
}
애니메이션 빠른 참조
:root {
/* Durations */
--duration-instant: 0.1s;
--duration-fast: 0.15s;
--duration-normal: 0.25s;
--duration-slow: 0.4s;
/* Easings */
--ease-out: cubic-bezier(0.0, 0.0, 0.58, 1.0);
--ease-in: cubic-bezier(0.42, 0.0, 1.0, 1.0);
--ease-in-out: cubic-bezier(0.42, 0.0, 0.58, 1.0);
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
}
인터랙션 패턴
“최고의 인터페이스는 인터페이스가 없는 것이다.” — Golden Krishna
인터랙션 패턴은 사용자가 제품을 조작하고, 탐색하고, 이해하는 방식을 정의해요. 이 패턴들은 뛰어난 인터랙션을 제공하는 제품들을 연구하여 추출한 것이에요.
직접 조작 (Framer)
추상적인 개념을 실체화하세요. Framer는 추상적인 숫자에 불과한 CSS 브레이크포인트를 드래그 가능한 핸들로 변환해요. 사용자는 레이아웃이 실시간으로 적응하는 모습을 직접 확인할 수 있어요.
/* Breakpoint handle styling */
.breakpoint-handle {
position: absolute;
top: 0;
bottom: 0;
width: 4px;
background: var(--accent);
cursor: col-resize;
opacity: 0.6;
transition: opacity 0.15s ease;
}
.breakpoint-handle:hover,
.breakpoint-handle:active {
opacity: 1;
width: 6px;
}
사용 시점: 결과가 시각적인 모든 설정 — 드래그로 크기 조절, 색상 선택기, 타임라인 스크러빙 등.
맥락 인식 인터페이스 (Flighty, Figma)
현재 순간에 관련된 것만 보여주세요. Flighty는 항공편 추적에 15가지 고유 상태를 사용해요. Figma의 속성 패널은 선택한 대상에 따라 완전히 달라져요.
| 단계 (Flighty) | 사용자에게 표시되는 내용 |
|---|---|
| 24시간 전 | 확인 코드, 터미널 정보 |
| 공항 도착 시 | 게이트 번호, 탑승 시간 |
| 비행 중 | 남은 시간, 진행 상황, 도착 예정 시간 |
| 착륙 시 | 환승 게이트, 이동 경로 |
enum ContextState: CaseIterable {
case farOut, dayBefore, headToAirport, atAirport
case atGate, boarding, inFlight, landed, connection
static func current(for flight: Flight, context: UserContext) -> ContextState {
// Factor in: time, location, flight status
// Return the single most relevant state
}
}
안티패턴: 모든 컨트롤을 표시하고 관련 없는 것들을 회색 처리하는 것. 이는 시각적 노이즈를 만들어요.
지능형 활성화 (Halide)
도구는 맥락을 감지하고 스스로 활성화되어야 해요. Halide의 초점 루페는 초점 드래그 시 나타나고 손을 떼면 사라져요. 토글 버튼이 필요 없어요.
struct IntelligentlyActivated<Content: View>: ViewModifier {
let isInteracting: Bool
@State private var isVisible = false
func body(content: Content) -> some View {
content
.opacity(isVisible ? 1 : 0)
.scaleEffect(isVisible ? 1 : 0.95)
.animation(.easeInOut(duration: 0.2), value: isVisible)
.onChange(of: isInteracting) { _, newValue in
if newValue {
withAnimation { isVisible = true }
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if !isInteracting { isVisible = false }
}
}
}
}
}
듀얼 모드 디자인 (Halide, Warp)
모드 전환은 단순히 요소를 토글하는 게 아니라 UI를 근본적으로 변환해야 해요. Halide의 자동 모드와 수동 모드는 완전히 다른 인터페이스예요. Warp는 네 가지 입력 방식(타이핑, 팔레트, AI, 마우스)을 통해 CLI와 GUI를 연결하면서도 사용자를 하나의 패러다임에 강제하지 않아요.
구조화된 콘텐츠 (Warp, Bear, Craft)
전통적으로 비구조화된 콘텐츠에 구조를 부여하세요. Warp는 터미널 출력을 복사, 공유, 재실행이 가능한 개별 블록으로 만들어요. Bear는 글을 쓰면서 인라인으로 노트를 정리할 수 있어요 (#tag/subtag). Craft는 모든 블록이 페이지가 될 수 있어서, 미리 정의된 계층 구조가 아닌 사용 과정에서 구조가 자연스럽게 형성돼요.
점진적 학습 (Superhuman)
반복적 노출을 통해 사용자에게 빠른 방법을 가르치세요. Superhuman의 Cmd+K 팔레트는 결과 옆에 항상 키보드 단축키를 표시해요. 매번 사용할 때마다 짧은 학습이 이루어져요.
/* Always show shortcut alongside command name */
.command-result {
display: flex;
justify-content: space-between;
padding: 8px 12px;
}
.command-shortcut {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
background: var(--bg-subtle);
padding: 2px 6px;
border-radius: 4px;
}
안티패턴: 기능을 설명하는 튜토리얼 모달. 설명은 잊히지만, 직접 해본 경험은 기억에 남아요.
AI 인터페이스 패턴
“최고의 AI 인터페이스는 기계가 처리하는 과정을 보여 주고, 기계가 내놓은 결과를 검증할 수 있게 해요.”
AI 인터페이스에는 고유한 어려움이 있어요. 사용자는 결과를 예측할 수 없고, 눈으로 살펴보는 것만으로 정확성을 검증할 수 없으며, 시스템이 작동 중인지 고장 났는지조차 판단하기 어려울 때가 많아요.
핵심 문제
| 기존 소프트웨어 | AI 소프트웨어 |
|---|---|
| 결과를 예측할 수 있어요 | 결과가 달라져요 |
| 오류가 명확해요 | 오류도 그럴듯해 보여요 |
| 사용자가 테스트로 검증해요 | 사용자가 출처를 확인해 검증해요 |
| 로딩 = 기다리는 중 | 로딩 = 작업 중(과정을 보여 주세요) |
| 기본적으로 신뢰해요 | 신뢰를 얻어야 해요 |
인용을 우선하는 디자인(Perplexity)
사실에 관한 모든 주장은 출처로 연결되어야 해요. Perplexity는 모든 주장에 인라인 인용 [1]을 삽입하고, 마우스를 올리면 미리 보기를 제공하며 출처 패널을 계속 표시해요.
.citation-marker {
position: relative;
color: var(--accent);
cursor: pointer;
font-size: 0.8em;
vertical-align: super;
}
.citation-preview {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
width: 280px;
padding: 12px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
피해야 할 패턴: 추적 가능한 출처 없이 주장을 생성하는 AI 인터페이스예요. 모델이 출처를 인용할 수 없다면 인터페이스에서 이를 표시해야 해요.
스트리밍 단계 표시기(Perplexity)
AI가 단순히 작동 중이라는 사실만 보여 주지 말고, 무엇을 하고 있는지 사용자에게 알려 주세요. 일반적인 로딩 표시기 대신 “검색 중…” → “출처 4개를 읽는 중…” → “답변 작성 중…”과 같은 단계 표시기를 사용하세요.
.phase-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: color-mix(in srgb, var(--phase-color) 10%, transparent);
border-radius: 16px;
font-size: 13px;
color: var(--phase-color);
transition: all 0.3s ease;
}
.loading-dots span {
width: 4px;
height: 4px;
background: currentColor;
border-radius: 50%;
animation: pulse 1.4s ease-in-out infinite;
}
오류의 투명성
AI가 실패하거나 확신하지 못할 때는 이를 명확히 보여 주세요. 확신에 찬 듯한 문장으로 감추지 마세요.
| 상황 | 나쁜 패턴 | 좋은 패턴 |
|---|---|---|
| 낮은 확신도 | 확신하는 것처럼 말해요 | “확실하지 않지만…”이라고 밝히고 시각적 강조를 낮춰요 |
| 출처를 찾지 못함 | 내용을 지어내요 | “이 주장을 뒷받침할 출처를 찾지 못했어요”라고 알려요 |
| 출처끼리 모순됨 | 하나를 말없이 선택해요 | 양쪽 모두 보여 주고 충돌하는 부분을 강조해요 |
| 오래된 정보 | 최신 정보처럼 제시해요 | “[날짜] 기준…”이라고 밝히고 최신성 표시기를 제공해요 |
핵심 통찰: 사용자는 불확실성을 솔직히 밝히는 AI는 이해해 줘요. 하지만 확신에 차서 틀린 답을 내놓는 AI는 용납하지 않아요.
에이전트형 작업 흐름의 증거
AI 에이전트 인터페이스는 노력했다고 설명하는 데 그치지 않고, 실제로 작업했음을 증명하는 상태를 제공해야 해요. 좋은 에이전트형 작업 흐름은 계획 승인, 출처 수집, 편집 적용, 테스트 실행, 검토 대기, 출시 차단 또는 준비 완료와 같은 점검 지점을 통해 진행 상황을 명확히 보여 줘요.
| 에이전트 상태 | 인터페이스 패턴 | 중요한 이유 |
|---|---|---|
| 계획 | 명확한 목표와 승인 기준 | 에이전트가 결과 대신 활동량을 최적화하지 않도록 막아 줘요 |
| 탐색 | 확신도와 최신성을 포함한 출처 목록 | 사용자가 검증된 사실과 작업 중인 가정을 구분할 수 있어요 |
| 실행 | 점검 지점과 연결된 변경 파일 요약 | 전체 기록을 읽지 않아도 편집 내용을 검토할 수 있어요 |
| 검증 | 테스트, 경로, 스크린샷 또는 실행 증거 | 실제 증거와 근거 없이 만들어 낸 확신을 구분해 줘요 |
| 출시 | 승인, 배포 또는 네이티브 검토 상태 | 로컬 검사를 통과했다는 표시 뒤에 미완료 작업을 숨긴 채 게시하지 못하게 해요 |
피해야 할 패턴: 여러 단계로 이루어진 에이전트 작업에 하나의 “완료” 상태만 사용하는 방식이에요. 복잡한 작업에는 무엇이 변경되었고, 무엇으로 검증했으며, 무엇을 거부했고, 어떤 부분에 여전히 사람의 판단이 필요한지 보여 주는 명확한 관리 이력이 필요해요.
Dieter Rams: 10가지 원칙
“더 적게, 하지만 더 좋게.” — Dieter Rams
Dieter Rams는 20세기에 가장 큰 영향을 끼친 산업 디자이너예요. 1961년부터 1995년까지 Braun의 디자인 책임자로 일하며 수십 년이 지난 지금도 시대를 타지 않는 제품을 만들었어요. 그의 작업은 Apple의 디자인 언어에 직접적인 영감을 주었어요.
좋은 디자인의 10가지 원칙
1. 좋은 디자인은 혁신적이에요 모방하지 마세요. 발전하는 기술에 혁신적인 디자인을 결합하세요.
2. 좋은 디자인은 제품을 유용하게 만들어요 모든 요소에는 목적이 있어야 해요. 형태는 기능을 따라야 해요.
3. 좋은 디자인은 아름다워요 아름다움은 피상적인 요소가 아니라 꼭 필요한 요소예요. 우리가 매일 사용하는 제품은 삶의 질에 영향을 줘요.
4. 좋은 디자인은 제품을 이해하기 쉽게 만들어요 사용자에게 설명서가 필요하지 않아야 해요. 인터페이스 자체가 사용법을 알려 줘야 해요.
5. 좋은 디자인은 눈에 거슬리지 않아요 디자인은 압도하지 않고 뒷받침해야 해요. 중심은 UI가 아니라 사용자의 콘텐츠예요.
/* Obtrusive: UI competes with content */
.editor {
background: linear-gradient(135deg, purple, blue);
border: 3px dashed gold;
}
/* Unobtrusive: UI recedes, content shines */
.editor {
background: var(--color-background);
border: 1px solid var(--color-border);
}
6. 좋은 디자인은 정직해요 다크 패턴을 사용하지 마세요. 과장해서 약속하지 마세요. 한계를 투명하게 밝히세요.
7. 좋은 디자인은 오래 지속돼요 금방 낡아 보일 유행은 피하세요. 유행보다 고전을 선택하세요.
TRENDY (will date): TIMELESS:
- Extreme glassmorphism - Clean typography
- Neon colors, glitch effects - Subtle elevation
- Aggressive gradients - Neutral palette with considered accent
8. 좋은 디자인은 마지막 세부 사항까지 철저해요 어느 것도 자의적으로 정하면 안 돼요. 로딩 상태, 빈 상태, 오류 상태까지 모두 디자인해야 해요.
9. 좋은 디자인은 환경을 생각해요 성능은 환경과 연결되어 있어요. 사용자의 주의를 존중하세요. 효율적인 코드를 작성하세요.
10. 좋은 디자인은 가능한 한 적게 디자인해요 필요하지 않은 것은 모두 제거하세요. 최고의 디자인은 보이지 않아요.
2026년 웹 패턴
이제 점점 더 많은 레이아웃과 상호작용 문제에서 네이티브 CSS가 JavaScript를 대체하고 있어요. 2025년부터 2026년까지 앵커 배치, 스크롤 기반 애니메이션, @starting-style이 정식 브라우저에 도입됐고, 2026년 중반에는 폼 컨트롤 자동 크기 조절, 간격 장식, 텍스트 자동 맞춤 기능이 추가됐어요.
여기서는 새로움보다 지원 현황이 더 중요해요. 한 엔진에만 출시된 기능은 점진적 향상으로 사용해야 하지만, Baseline Newly available에 도달한 기능은 안심하고 기반으로 삼을 수 있어요. 아래 표는 2026년 8월을 기준으로 두 범주를 구분해요.
| 기능 | Chrome | Firefox | Safari | 상태 |
|---|---|---|---|---|
field-sizing |
123 | 152 | 26.2 | Baseline Newly available |
shape-outside: rect() / xywh() |
149 | 149 | 17.2 | Baseline Newly available |
background-clip: border-area |
150 | — | 18.2 | 2개 엔진 |
간격 장식(grid/flex의 column-rule) |
149 | — | — | Chrome 전용 |
text-fit |
150 | — | — | Chrome 전용 |
focusgroup |
150 | — | — | Chrome 전용 |
출처: web.dev의 6월 웹 플랫폼 신규 기능과 7월.
컨테이너 쿼리
뷰포트가 아니라 컨테이너를 기준으로 컴포넌트 크기를 조절하세요.
.card-grid {
container-type: inline-size;
container-name: card-grid;
}
.card {
display: grid;
gap: 16px;
padding: 20px;
}
@container card-grid (min-width: 400px) {
.card {
grid-template-columns: auto 1fr;
}
}
@container card-grid (min-width: 600px) {
.card {
padding: 32px;
gap: 24px;
}
}
:has() 선택자
하위 요소를 기준으로 상위 요소를 선택해요. 이전에는 JavaScript 없이는 불가능했던 기능이에요.
/* Card with image gets different padding */
.card:has(img) {
padding: 0;
}
.card:has(img) .card-content {
padding: 20px;
}
/* Form group with error */
.form-group:has(.input:invalid) .form-label {
color: var(--color-error);
}
/* Highlight navigation when on that page */
.nav-item:has(a[aria-current="page"]) {
background: var(--color-surface);
}
CSS 중첩
전처리기 없이 네이티브 중첩을 사용할 수 있어요.
.card {
background: var(--color-surface);
border-radius: 12px;
padding: 24px;
& .card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 8px;
}
& .card-body {
color: var(--color-text-secondary);
line-height: 1.6;
}
&:hover {
box-shadow: var(--shadow-md);
}
@media (min-width: 768px) {
padding: 32px;
}
}
간격 장식
Grid와 flex의 간격 자체에 선을 넣을 수 있어 구분선을 만들 때 더 이상 가상 요소나 모든 하위 요소의 테두리, 열마다 별도의 래퍼가 필요하지 않아요. Chrome 149 이상에서만 지원되므로 기반 기능이 아닌 점진적 향상으로 사용해야 해요.
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
/* Vertical rules between columns, horizontal between rows */
column-rule: 1px solid var(--color-border);
row-rule: 1px solid var(--color-border);
}
기존 방식은 첫 번째 항목을 제외한 모든 항목에 border-left를 적용했기 때문에 Grid가 두 번째 행으로 줄 바꿈되는 순간 레이아웃이 깨졌어요. 간격 장식은 간격 자체에 선을 그리므로 줄 바꿈 후에도 올바르게 표시돼요.
한 엔진에서만 지원되므로 이 규칙은 장식으로만 사용하고 의미를 구분하는 유일한 수단으로 삼지 마세요. 장식이 없을 때 모호해지는 레이아웃은 Firefox와 Safari에서 제대로 작동하지 않는 레이아웃이에요.
콘텐츠에 맞춰 크기가 조절되는 폼 컨트롤
field-sizing: content를 사용하면 입력란, 텍스트 영역, 선택 메뉴가 사용자의 입력 내용에 따라 커지거나 작아져요. 코드베이스마다 반복해서 구현하던 스크롤 높이 계산용 JavaScript를 대체할 수 있어요.
.comment-box {
field-sizing: content;
min-height: 3lh; /* floor: three lines */
max-height: 12lh; /* ceiling: then scroll */
}
.tag-input {
field-sizing: content;
min-width: 8ch;
}
항상 min-/max- 범위와 함께 사용하세요. 최솟값이 없으면 빈 필드가 거의 보이지 않을 정도로 축소되고, 최댓값이 없으면 긴 내용을 붙여 넣었을 때 제출 버튼이 화면 밖으로 밀려나요.
이 기능은 2026년 6월에 Baseline Newly available에 도달했으므로 점진적 향상이 아닌 주요 구현 방식으로 사용해도 안전해요.
상자에 맞춰지는 텍스트
text-fit은 컨테이너 너비를 채우도록 글꼴 크기를 조절해요. 이전에는 측정 스크립트가 필요했던 디스플레이용 헤드라인이나 대형 숫자 대시보드에 사용할 수 있어요.
.stat-value {
text-fit: consistent; /* one size for the whole box */
}
Chrome 150에서만 지원해요. 다른 엔진에서도 기본값이 아닌 의도한 크기로 표시되도록 먼저 일반 font-size를 지정하세요.
.stat-value {
font-size: clamp(2rem, 8vw, 6rem); /* the real answer everywhere */
text-fit: consistent; /* refinement where supported */
}
대부분의 경우에는 이미 모든 브라우저가 지원하는 clamp()로 충분해요. 콘텐츠 길이의 변동 폭이 너무 커서 하나의 clamp로 해결할 수 없을 때 text-fit을 사용하세요.
편법 없는 그라데이션 테두리
background-clip: border-area는 테두리 상자 영역까지 배경을 그려 주므로 중첩 요소나 mask-composite 편법 없이 2줄의 규칙만으로 그라데이션 테두리를 만들 수 있어요.
.featured-card {
border: 2px solid transparent;
border-radius: 12px;
background:
linear-gradient(var(--color-surface), var(--color-surface)) padding-box,
linear-gradient(135deg, #6366f1, #ec4899) border-area;
}
Chrome 150과 Safari 18.2에서 지원하며 Firefox에는 아직 도입되지 않았어요. 대체 표시가 투명 테두리가 아닌 실제 테두리가 되도록 먼저 단색 border-color를 선언하세요.
Keydown 핸들러 없는 화살표 키 탐색
focusgroup 속성은 브라우저에 컨트롤 집합이 하나의 키보드 단위라는 사실을 알려 줘요. Tab 키를 누르면 그룹에 한 번만 진입하고, 그룹 안에서는 화살표 키로 이동하며, Tab 키를 다시 누르면 그룹을 벗어나요. 툴바, 탭 목록, 분할 컨트롤, 라디오 버튼 형태의 버튼 그룹에는 모두 이런 동작이 필요하며, 직접 구현한 컴포넌트에서 가장 자주 잘못 처리되는 부분이기도 해요.
<div role="toolbar" focusgroup="wrap">
<button>Bold</button>
<button>Italic</button>
<button>Underline</button>
</div>
wrap을 사용하면 마지막 항목에서 화살표 키를 눌렀을 때 첫 번째 항목으로 돌아가요. 기본 동작은 양 끝에서 멈춰요.
Chrome 150에서만 지원하므로 지원 범위가 넓어질 때까지 기존 keydown 핸들러를 유지하세요. 이 속성은 접근성 동작을 정확히 표현한다는 점에서 지금 알아 둘 가치가 있어요. 툴바는 5개의 Tab 정지 지점이 아니라 하나의 정지 지점이어야 하며, 이런 기대 동작은 해당 속성이 등장하기 수십 년 전부터 존재했어요. ARIA 작성 지침에서는 구현 방식과 관계없이 이 패턴을 설명해요.
HTMX 통합
무거운 JavaScript 프레임워크 없이 서버 주도형 상호작용을 구현해요.
<!-- Load content on click -->
<button hx-get="/api/more-items"
hx-target="#item-list"
hx-swap="beforeend"
hx-indicator="#loading">
Load More
</button>
<!-- Form with inline validation -->
<form hx-post="/api/contact"
hx-target="#form-response"
hx-swap="outerHTML">
<input type="email" name="email"
hx-post="/api/validate-email"
hx-trigger="blur"
hx-target="next .error" />
<span class="error"></span>
</form>
앵커 배치
JavaScript 없이 한 요소를 다른 요소를 기준으로 배치하는 네이티브 CSS 기능이에요. 트리거를 따라다니는 툴팁, 팝오버, 드롭다운 메뉴에 사용할 수 있어요.
/* Anchor an element to another */
.trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
}
스크롤 기반 애니메이션
애니메이션 진행 상태를 스크롤 위치에 연결해요. JavaScript 없이 읽기 진행률 표시기, 패럴랙스 효과, 순차 표시 효과를 구현할 수 있어요.
/* Reading progress bar */
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--color-primary);
transform-origin: left;
animation: progress linear;
animation-timeline: scroll();
}
@keyframes progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@starting-style
DOM에 진입하는 요소의 초기 스타일을 정의해 JavaScript 없이 CSS만으로 진입 애니메이션을 구현할 수 있어요.
.card {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(10px);
}
}
Design Tokens 시스템
애플리케이션 전체의 일관성을 유지하는 완전한 tokens 시스템이에요.
:root {
/* Colors */
--color-text: #1a1a1a;
--color-text-secondary: #666666;
--color-text-muted: #999999;
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-surface-elevated: #ffffff;
--color-border: #e5e7eb;
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* Typography */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: "SF Mono", Consolas, monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 2rem;
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Spacing (8px base) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
/* Borders */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--duration-fast: 100ms;
--duration-normal: 200ms;
}
다크 모드 제대로 만들기
단순히 색상을 반전하지 말고 어두운 환경에 맞게 다시 디자인하세요.
@media (prefers-color-scheme: dark) {
:root {
/* Neutrals */
--color-background: hsl(220, 13%, 10%);
--color-surface: hsl(220, 13%, 15%);
--color-surface-elevated: hsl(220, 13%, 18%);
--color-border: hsl(220, 13%, 23%);
/* Text (inverted) */
--color-text: hsl(220, 9%, 93%);
--color-text-secondary: hsl(220, 9%, 70%);
--color-text-muted: hsl(220, 9%, 55%);
/* Adjust saturation for dark mode */
--color-primary: hsl(220, 80%, 60%);
--color-success: hsl(142, 70%, 45%);
--color-error: hsl(0, 80%, 65%);
/* Shadows in dark mode need adjustment */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
}
}
다크 모드 원칙: - 넓은 영역에서는 채도를 낮추세요 - 강조 색상의 명도를 높이세요 - 그림자는 대비가 더 필요하므로 선명하게 표현하세요 - 다크 모드를 나중에 덧붙이지 말고 처음부터 의도적으로 디자인하세요
Figma 추출 작업 흐름
디자인 파일을 실제 코드로 변환하려면 디자인 언어를 구성하는 색상, 타이포그래피, 간격, 효과 등의 디자인 tokens를 체계적으로 추출해야 해요.
Figma 변수 내보내기
Figma의 기본 변수 기능을 사용하면 가장 깔끔하게 추출할 수 있어요.
내보내기 단계:
1. Figma 파일 열기 → 로컬 변수 패널
2. 컬렉션 메뉴 클릭 → “JSON(으)로 내보내기”
3. figma-variables.json으로 저장
JSON 토큰 구조:
{
"colors": {
"primitive": {
"blue-500": { "value": "#3b82f6", "type": "color" },
"blue-600": { "value": "#2563eb", "type": "color" }
},
"semantic": {
"primary": { "value": "{colors.primitive.blue-500}", "type": "color" },
"primary-hover": { "value": "{colors.primitive.blue-600}", "type": "color" }
}
},
"spacing": {
"1": { "value": "4px", "type": "spacing" },
"2": { "value": "8px", "type": "spacing" },
"4": { "value": "16px", "type": "spacing" }
}
}
토큰을 CSS(으)로 변환하기
CSS 사용자 지정 속성:
:root {
/* Primitive colors (direct values) */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-900: #1e3a8a;
/* Semantic colors (reference primitives) */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-background: var(--color-white);
--color-surface: var(--color-gray-50);
/* Spacing (8px grid) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
/* Typography */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--line-height-tight: 1.25;
--line-height-normal: 1.5;
/* Effects */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
}
다크 모드 토큰:
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--color-gray-900);
--color-surface: var(--color-gray-800);
--color-text: var(--color-gray-100);
--color-text-secondary: var(--color-gray-400);
/* Adjusted shadows for dark mode */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
}
}
토큰을 SwiftUI로 변환하기
색상 확장:
import SwiftUI
extension Color {
// MARK: - Primitive Colors
static let blue50 = Color(hex: "eff6ff")
static let blue500 = Color(hex: "3b82f6")
static let blue600 = Color(hex: "2563eb")
// MARK: - Semantic Colors
static let brandPrimary = Color.blue500
static let brandPrimaryHover = Color.blue600
// MARK: - Surface Colors
static let surfaceBackground = Color(light: .white, dark: Color(hex: "0f172a"))
static let surfaceElevated = Color(light: Color(hex: "f8fafc"), dark: Color(hex: "1e293b"))
}
extension Color {
init(hex: String) {
// Standard hex parsing implementation
}
init(light: Color, dark: Color) {
self.init(UIColor { traits in
traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
})
}
}
간격 상수:
enum Spacing {
static let xs: CGFloat = 4 // --space-1
static let sm: CGFloat = 8 // --space-2
static let md: CGFloat = 16 // --space-4
static let lg: CGFloat = 24 // --space-6
static let xl: CGFloat = 32 // --space-8
}
// Usage
VStack(spacing: Spacing.md) {
// ...
}
.padding(Spacing.lg)
디자이너 전달 체크리스트
디자이너가 내보내야 할 항목:
| 에셋 유형 | 형식 | 참고 사항 |
|---|---|---|
| 색상 | 변수 JSON | 라이트 모드와 다크 모드 포함 |
| 타이포그래피 | 스타일 내보내기 | 글꼴, 크기, 굵기, 줄 높이 |
| 간격 | 변수 JSON | 기본 단위 명시 |
| 아이콘 | SVG | 윤곽선 형태, 단색 |
| 이미지 | PNG @2x/@3x 또는 WebP | 압축 적용 |
| 컴포넌트 | Figma 링크 | 구현할 때 참고 |
품질 통과 기준:
- [ ] 모든 색상이 변수로 정의되어 있음(하드코딩된 헥스값 없음)
- [ ] 타이포그래피에 정의된 텍스트 스타일을 사용함
- [ ] 간격이 그리드 체계(8px 기본 단위)를 따름
- [ ] 다크 모드 변형이 제공됨
- [ ] 상호작용 상태가 문서화되어 있음(마우스 올림, 활성, 비활성)
- [ ] 반응형 중단점이 표시되어 있음
- [ ] 접근성 요구 사항이 명시되어 있음(대비율)
개발자가 받는 항목:
- 토큰 파일(플랫폼에 따라 JSON/CSS/Swift)
- 측정값이 포함된 컴포넌트 명세
- 필요한 형식으로 내보낸 에셋
- 상호작용 문서(상태, 애니메이션)
- 접근성 주석
빠른 참조 표
Gestalt 원칙
| 원칙 | 규칙 | 활용 |
|---|---|---|
| 근접성 | 관련 있음 = 가깝게 배치 | 양식, 섹션 |
| 유사성 | 같은 모양 = 같은 기능 | 버튼, 카드 |
| 전경과 배경 | 레이어를 명확히 구분 | 모달, 카드 |
| 연속성 | 선의 흐름을 따름 | 타임라인, 정렬 |
| 폐쇄성 | 뇌가 형태를 완성함 | 아이콘, 스크롤 힌트 |
타이포그래피
| 요소 | 크기 | 굵기 | 줄 높이 |
|---|---|---|---|
| 본문 | 16px | 400 | 1.5-1.7 |
| 제목 | 24-48px | 600-700 | 1.1-1.2 |
| UI 레이블 | 12-14px | 500 | 1.3-1.4 |
| 캡션 | 12px | 400 | 1.4 |
색상 역할
| 역할 | 라이트 모드 | 다크 모드 |
|---|---|---|
| 배경 | #ffffff | #0f172a |
| 표면 | #f4f5f7 | #1e293b |
| 테두리 | #e4e6ea | #334155 |
| 텍스트 | #1a1a2e | #f1f5f9 |
| 흐린 텍스트 | #6b7280 | #94a3b8 |
| 기본 강조 | #3b82f6 | #60a5fa |
| 성공 | #22c55e | #4ade80 |
| 오류 | #ef4444 | #f87171 |
간격 척도
| 토큰 | 값 | 활용 |
|---|---|---|
| –space-1 | 4px | 아이콘 사이 간격 |
| –space-2 | 8px | 인라인 요소 |
| –space-4 | 16px | 기본 간격 |
| –space-6 | 24px | 카드 안쪽 여백 |
| –space-8 | 32px | 섹션 사이 간격 |
| –space-16 | 64px | 페이지 섹션 |
디자인 체크리스트
인터페이스를 출시하기 전에 다음 항목을 확인하세요.
Gestalt
- [ ] 관련된 요소가 관련 없는 요소보다 더 가깝게 배치되어 있음(근접성)
- [ ] 비슷한 기능에 비슷한 스타일이 적용되어 있음(유사성)
- [ ] 전경과 배경이 명확하게 구분됨(전경과 배경)
- [ ] 시선이 레이아웃을 따라 자연스럽게 흐름(연속성)
타이포그래피
- [ ] 기본 글꼴 크기가 16px 이상임
- [ ] 본문 텍스트의 줄 높이가 1.5 이상임
- [ ] 한 줄이 75자 미만임
- [ ] 위계가 명확함(3개 수준을 구분할 수 있음)
- [ ] 전체에서 일관된 척도를 사용함
색상
- [ ] 모든 텍스트가 4.5:1 대비 기준을 통과함(WCAG AA)
- [ ] 색상만으로 정보를 구분하지 않음(아이콘이나 레이블도 사용)
- [ ] 다크 모드를 의도적으로 디자인함
- [ ] 60-30-10 배분을 따름
시각적 위계
- [ ] 가장 중요한 요소를 바로 알아볼 수 있음
- [ ] 시선이 의도한 순서대로 흐름
- [ ] 섹션마다 명확한 CTA가 하나만 있음
- [ ] 글자 크기 척도가 일관됨
간격
- [ ] 모든 간격에 정의된 척도를 사용함(임의의 숫자 없음)
- [ ] 카드와 컴포넌트의 안쪽 여백이 일관됨
- [ ] 모바일 환경의 간격이 편안함
- [ ] 그리드 정렬이 일관됨(8px 기본 단위)
상호작용
- [ ] 사용자가 도구를 의식하지 않고 목표를 달성할 수 있나요?
- [ ] UI가 현재 상황에 맞게 조정되나요?
- [ ] 필요한 상황에서만 도구가 표시되나요?
- [ ] 반복해서 사용할수록 더 빠른 방법을 익힐 수 있나요?
AI 인터페이스
- [ ] 모든 사실에 출처를 추적할 수 있음
- [ ] 스트리밍에서 단순한 로딩 표시가 아니라 처리 단계를 보여 줌
- [ ] 오류 상태를 숨기지 않고 투명하게 보여 줌
- [ ] 신뢰도가 낮은 결과를 시각적으로 구분함
Dieter Rams 점검
- [ ] 제거할 수 있는 요소가 있나요?
- [ ] 모든 요소가 기능에 기여하나요?
- [ ] 5년 뒤에도 낡아 보이지 않을까요?
- [ ] 모든 상태를 디자인했나요?
자료
도서: - Sophie Lovell의 As Little Design as Possible (Dieter Rams) - Robert Bringhurst의 The Elements of Typographic Style
도구: - WebAIM 대비 검사기 - 글자 크기 척도 생성기 - Figma Tokens Studio — 디자인 토큰 관리
디자인 시스템: - Apple HIG - Material Design 3 - Radix UI - shadcn/ui
디자인 사례 연구
48개의 뛰어난 제품을 깊이 있게 살펴보며, 가져다 쓸 만한 패턴과 원칙을 정리합니다.
개발자 도구 및 AI
| 제품 | 핵심 기여 |
|---|---|
| Figma | 여러 사용자의 동시 작업 상태 표시, 맥락 인식 패널 |
| Warp | 블록 기반 터미널, CLI-GUI 연결 |
| Framer | 시각적 반응형 디자인, 속성 제어 |
| Vercel | 탁월한 다크 모드, 자연스럽게 드러나는 상태 |
| Linear | 낙관적 UI, 키보드 중심 작업 흐름 |
| Raycast | 확장 기능 시스템, 빠른 작업 |
| Stripe | 탁월한 문서화, API 디자인 |
| Perplexity | 출처를 앞세운 AI, 단계별 스트리밍 |
| Obsidian | 로컬 우선 조합성, 지식 그래프 인터페이스 |
Apple 네이티브 및 창작 도구
| 제품 | 핵심 기여 |
|---|---|
| Flighty | 15가지 지능형 상태, 실시간 현황, 데이터 시각화 |
| Halide | 지능형 기능 활성화, 제스처 제어 |
| Bear | 타이포그래피 중심 설계, 인라인 태그 |
| Craft | 네이티브 우선 크로스 플랫폼, 중첩 페이지 |
| Things | 연기 날짜, 빠른 입력 패턴 |
| Darkroom | 몰입감 있는 다크 UI, 사진 중심 편집 |
| Procreate | 제스처 중심 창작 도구 |
| Overcast | 인터페이스 디자인으로 구현한 보이지 않는 오디오 엔지니어링 |
| Camo | 전문가용 영상 제작 UI |
| Ivory | 정교한 타임라인, 유쾌한 정밀함 |
| Anybox | 플랫폼 네이티브 투명성 |
| Drafts | 텍스트 중심 기록의 명료함 |
| Notion Calendar | 정밀한 일정 관리, 작업 공간 통합 |
생산성, 지식 및 금융
| 제품 | 핵심 기여 |
|---|---|
| Superhuman | 100ms 규칙, 명령 팔레트 학습, 실습형 시작 안내 |
| Notion | 블록 시스템, 슬래시 명령 |
| Arc | 공간, 분할 보기, 명령 모음 |
| Todoist | 따뜻한 미니멀리즘, 극도의 절제 |
| Amie | 즐거운 생산성, 따뜻한 미니멀리즘 |
| Loom | 친근하면서도 전문적인 비동기 영상 |
| Pitch | 대담한 프레젠테이션 디자인 언어 |
| Readwise Reader | 깊이 읽기를 위한 도구, 우주적 브랜드 이미지 |
| Copilot Money | 영화 같은 금융 데이터 시각화 |
| 1Password | 마찰 없이 구현한 보안 |
| Mercury | 영화 같은 세련미를 갖춘 은행 서비스 |
소비자, 소셜 및 신뢰
| 제품 | 핵심 기여 |
|---|---|
| Spotify | 색상, 감정, 규모 |
| Duolingo | 디자인 언어로 활용한 게임화 |
| Signal | 단순함으로 구현한 보안 |
| Airbnb | 대규모 거래 시장에서의 신뢰 |
| Bluesky | 알고리즘 투명성 |
| Letterboxd | 소셜 매개체로서의 영화 |
| Strava | 소셜 자산으로 활용한 GPS 데이터 |
| Apple Music | 편집자의 목소리, 공간 음향 |
| Headspace | 인터페이스 전략으로 활용한 평온함 |
| Zomato | 개성을 중심으로 설계한 음식 UX |
물리적 제품, 게임 및 접근성
| 제품 | 핵심 기여 |
|---|---|
| Balatro | 피드백 순환 구조, 게임 감각 시스템 |
| Rivian | 모험 사진과 웅장한 타이포그래피 |
| Teenage Engineering | 미학적 정체성으로 활용한 제약 |
| OKO | 오디오 및 햅틱 접근성 |
| CARROT Weather | 개성으로 차별화한 유틸리티 앱 |
이 가이드는 실천을 통해 발전합니다. 디자인 원칙은 시대를 초월하지만, 이를 적용하는 방식은 기술과 이해의 발전에 따라 달라집니다.