프론트엔드 디자이너 (Frontend Designer)
프론트엔드 디자이너와 개발자가 최신 모범 사례로 아름답고 접근 가능하며 성능이 뛰어난 사용자 인터페이스를 구축할 수 있는 종합적인 스킬입니다.
이 스킬이 하는 일
프론트엔드 디자이너/개발자가 다음을 수행하도록 돕습니다:
- 컴포넌트 디자인 및 개발 - 재사용 가능하고 접근성 있는 컴포넌트 구축
- 디자인 시스템 - 토큰, 패턴, 문서화 구현
- 반응형 디자인 - 모바일 우선, 유동적인 레이아웃
- 접근성 (WCAG 2.1) - 포괄적인 디자인 패턴
- 최신 CSS - Flexbox, Grid, 사용자 정의 속성
- 성능 최적화 - 빠르고 효율적인 프론트엔드
- 프레임워크 패턴 - React, Vue, Svelte 모범 사례
- 디자인 투 코드 - Figma에서 프로덕션까지의 워크플로우
이 스킬이 중요한 이유
체계적인 접근 방식 없이:
- 일관성 없는 컴포넌트 구현
- 접근성 문제
- 부실한 반응형 동작
- 중복된 코드와 스타일
- 유지보수 어려움
- 성능 문제
이 스킬을 사용하면:
- 일관되고 재사용 가능한 컴포넌트
- 기본적으로 WCAG AA 준수
- 모바일 우선 반응형 디자인
- 디자인 시스템 정렬
- 유지보수 가능한 코드베이스
- 빠르고 최적화된 전달
핵심 원칙
1. 접근성 우선
- WCAG 2.1 AA 최소
- 시맨틱 HTML
- 키보드 탐색
- 화면 판독기 지원
- 포커스 관리
- 색상 대비
2. 모바일 우선 반응형
- 모바일(320px)부터 시작
- 점진적 향상
- 유동적인 타이포그래피
- 유연한 레이아웃
- 터치 친화적 대상
3. 기본 성능
- 최소한의 CSS/JS
- 지연 로딩
- 최적화된 이미지
- 중요 CSS
- 트리 쉐이킹
4. 컴포넌트 기반
- 아토믹 디자인 방법론
- 재사용 가능한 컴포넌트
- Props 기반 사용자 정의
- 상속보다는 구성
5. 디자인 시스템 정렬
- 디자인 토큰
- 일관된 간격
- 타이포그래피 스케일
- 색상 팔레트
- 컴포넌트 라이브러리
컴포넌트 패턴
버튼 컴포넌트
접근성 있고 유연한 버튼 패턴:
// React 예제
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
children,
onClick,
...props
}) => {
return (
<button
className={`btn btn--${variant} btn--${size}`}
disabled={disabled || loading}
onClick={onClick}
aria-busy={loading}
{...props}
>
{loading ? <Spinner /> : children}
</button>
);
};
CSS (디자인 토큰 사용):
.btn {
/* 기본 스타일 */
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-sans);
font-weight: 600;
text-decoration: none;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
/* 접근성 */
min-height: 44px; /* WCAG 터치 대상 */
&:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* 변형 */
.btn--primary {
background: var(--color-primary);
color: var(--color-on-primary);
&:hover:not(:disabled) {
background: var(--color-primary-hover);
}
}
.btn--secondary {
background: var(--color-secondary);
color: var(--color-on-secondary);
}
.btn--ghost {
background: transparent;
color: var(--color-primary);
border: 1px solid currentColor;
}
/* 크기 */
.btn--sm {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
}
.btn--md {
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
}
.btn--lg {
padding: var(--space-4) var(--space-6);
font-size: var(--text-lg);
}
카드 컴포넌트
유연하고 접근성 있는 카드:
interface CardProps {
variant?: 'elevated' | 'outlined' | 'filled';
padding?: 'none' | 'sm' | 'md' | 'lg';
interactive?: boolean;
children: React.ReactNode;
}
export const Card: React.FC<CardProps> = ({
variant = 'elevated',
padding = 'md',
interactive = false,
children,
}) => {
const Component = interactive ? 'button' : 'div';
return (
<Component
className={`,card card--${variant} card--padding-${padding} ${interactive ? 'card--interactive' : ''}`}
{...(interactive && { role: 'button', tabIndex: 0 })}
>
{children}
</Component>
);
};
CSS:
.card {
border-radius: var(--radius-lg);
background: var(--color-surface);
}
.card--elevated {
box-shadow: var(--shadow-md);
}
.card--outlined {
border: 1px solid var(--color-border);
}
.card--filled {
background: var(--color-surface-variant);
}
.card--interactive {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
&:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
}
.card--padding-sm { padding: var(--space-3); }
.card--padding-md { padding: var(--space-4); }
.card--padding-lg { padding: var(--space-6); }
폼 입력 컴포넌트
접근성 있는 폼 입력:
interface InputProps {
label: string;
error?: string;
hint?: string;
required?: boolean;
type?: 'text' | 'email' | 'password' | 'number';
}
export const Input: React.FC<InputProps> = ({
label,
error,
hint,
required = false,
type = 'text',
...props
}) => {
const id = useId();
const hintId = `${id}-hint`;
const errorId = `${id}-error`;
return (
<div className="input-wrapper">
<label htmlFor={id} className="input-label">
{label}
{required && <span aria-label="required">*</span>}
</label>
{hint && (
<p id={hintId} className="input-hint">
{hint}
</p>
)}
<input
id={id}
type={type}
className={`input ${error ? 'input--error' : ''}`}
aria-required={required}
aria-invalid={!!error}
aria-describedby={error ? errorId : hint ? hintId : undefined}
{...props}
/>
{error && (
<p id={errorId} className="input-error" role="alert">
{error}
</p>
)}
</div>
);
};
디자인 토큰
디자인 시스템을 위한 CSS 사용자 정의 속성:
:root {
/* 색상 - 기본 */
--color-primary: #0066FF;
--color-primary-hover: #0052CC;
--color-on-primary: #FFFFFF;
/* 색상 - 표면 */
--color-surface: #FFFFFF;
--color-surface-variant: #F5F5F5;
--color-on-surface: #1A1A1A;
/* 색상 - 테두리 */
--color-border: #E0E0E0;
--color-border-hover: #BDBDBD;
/* 색상 - 의미 */
--color-error: #D32F2F;
--color-success: #388E3C;
--color-warning: #F57C00;
--color-info: #1976D2;
/* 간격 스케일 (8px 기준) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.5rem; /* 24px */
--space-6: 2rem; /* 32px */
--space-8: 3rem; /* 48px */
--space-10: 4rem; /* 64px */
/* 타이포그래피 스케일 */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
/* 글꼴 패밀리 */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "SF Mono", Monaco, "Cascadia Code", monospace;
/* 줄 높이 */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* 테두리 반경 */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 1rem; /* 16px */
--radius-full: 9999px;
/* 그림자 */
--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);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
/* 포커스 링 */
--color-focus: #0066FF;
/* 전환 */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
}
/* 다크 모드 */
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #1A1A1A;
--color-surface-variant: #2A2A2A;
--color-on-surface: #FFFFFF;
--color-border: #3A3A3A;
}
}
반응형 디자인 패턴
모바일 우선 중단점
/* 모바일 우선 접근 방식 */
.container {
padding: var(--space-4);
/* 태블릿: 768px 이상 */
@media (min-width: 48rem) {
padding: var(--space-6);
}
/* 데스크탑: 1024px 이상 */
@media (min-width: 64rem) {
padding: var(--space-8);
max-width: 1200px;
margin: 0 auto;
}
}
유동적인 타이포그래피
/* 반응형 타이포그래피 */
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: var(--leading-tight);
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
p {
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: var(--leading-normal);
}
그리드 레이아웃
/* 반응형 그리드 */
.grid {
display: grid;
gap: var(--space-4);
/* 자동 맞춤 열 (최소 280px) */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* 12열 그리드 시스템 */
.grid-12 {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--space-4);
}
.col-span-4 {
grid-column: span 4;
}
/* 모바일에서 쌓기 */
@media (max-width: 48rem) {
.col-span-4 {
grid-column: span 12;
}
}
접근성 패턴
스킵 링크
export const SkipLink = () => (
<a href="#main-content" className="skip-link">
주 콘텐츠로 건너뛰기
</a>
);
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: var(--color-on-primary);
padding: var(--space-2) var(--space-4);
text-decoration: none;
z-index: 100;
&:focus {
top: 0;
}
}
포커스 관리
// 포커스 트랩이 있는 모달
export const Modal = ({ isOpen, onClose, children }) => {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
// 현재 포커스된 요소 저장
const previouslyFocused = document.activeElement;
// 모달 내 첫 번째 포커스 가능 요소에 포커스
const firstFocusable = modalRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// 닫을 때 포커스 복원
return () => previouslyFocused?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
className="modal-overlay"
>
<div className="modal">
{children}
</div>
</div>
);
};
ARIA 라벨
// 접근성 라벨이 있는 아이콘 버튼
export const IconButton = ({ icon, label, ...props }) => (
<button
aria-label={label}
className="icon-button"
{...props}
>
<span aria-hidden="true">{icon}</span>
</button>
);
// 로딩 상태
export const LoadingButton = ({ loading, children, ...props }) => (
<button
aria-busy={loading}
aria-live="polite"
disabled={loading}
{...props}
>
{loading ? '로딩 중...' : children}
</button>
);
성능 최적화
중요 CSS
<!-- 중요 CSS 인라인 -->
<style>
/* 접힌 부분 위 스타일 */
body { margin: 0; font-family: var(--font-sans); }
.header { /* 중요 헤더 스타일 */ }
.hero { /* 중요 히어로 스타일 */ }
</style>
<!-- 전체 스타일시트를 비동기로 로드 -->
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
이미지 지연 로딩
export const LazyImage = ({ src, alt, ...props }) => (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
{...props}
/>
);
코드 분할
// React 지연 로딩
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
최신 CSS 기술
컨테이너 쿼리
.card {
container-type: inline-size;
}
.card-content {
display: flex;
flex-direction: column;
/* 컨테이너가 400px보다 클 때 행 레이아웃으로 전환 */
@container (min-width: 400px) {
flex-direction: row;
}
}
CSS 그리드 자동 맞춤
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-4);
}
테마 지정을 위한 사용자 정의 속성
/* 밝은 테마 (기본) */
:root {
--bg: #ffffff;
--text: #000000;
}
/* 어두운 테마 */
[data-theme="dark"] {
--bg: #000000;
--text: #ffffff;
}
body {
background: var(--bg);
color: var(--text);
}
컴포넌트 라이브러리 구조
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.css
│ │ ├── Button.test.tsx
│ │ └── Button.stories.tsx
│ ├── Card/
│ ├── Input/
│ └── index.ts
├── tokens/
│ ├── colors.css
│ ├── spacing.css
│ ├── typography.css
│ └── index.css
├── utils/
│ ├── a11y.ts
│ └── responsive.ts
└── index.ts
이 스킬 사용하기
컴포넌트 생성
./scripts/generate_component.sh Button
다음을 포함한 컴포넌트 생성:
- TypeScript/JSX 파일
- CSS 모듈
- 테스트 파일
- Storybook 스토리
- 접근성 검사
디자인 시스템 설정
./scripts/setup_design_system.sh
생성:
- 디자인 토큰 (CSS 사용자 정의 속성)
- 기본 스타일
- 컴포넌트 템플릿
- 문서 구조
접근성 감사
./scripts/audit_accessibility.sh
검사:
- 색상 대비 비율
- 키보드 탐색
- ARIA 속성
- 시맨틱 HTML
- 포커스 관리
모범 사례
컴포넌트 디자인
✅ 권장:
- 시맨틱 HTML 사용
- 컴포넌트를 키보드로 접근 가능하게 만들기
- ARIA 라벨 제공
- 밝은 모드와 어두운 모드 모두 지원
...
[truncated for translation]