색
Senco는 색 그룹을 7개 쓴다. primary, accent, info, neutral, 그리고 상태 색 3개다. 각 그룹은 장식이 아니라 정해진 UI 기능을 맡는다. 모든 조합은 라이트와 다크 모드 모두에서 WCAG 2.2 AA 대비 기준을 통과한다.
송금 앱에서 색이 맡는 역할은 마케팅 사이트보다 훨씬 무겁다. 상태 색 하나로 "돈이 도착했다"와 "돈이 묶였다"가 갈리고, 이 구분은 어두운 화면에서도, 색각 이상이 있는 사용자에게도, 고객 지원으로 보낸 스크린샷에서도 유지돼야 한다. 아래 규칙은 모두 이 셋 중 하나에서 실제로 문제가 생겨서 만든 것이다.
팔레트
기본 팔레트는 기능별로 묶은 원시 색 값이다. 그룹마다 가장 어두운 900부터 가장 밝은 50까지 10단계가 있고, 대비 수준과 시각적 위계를 나누는 데 쓴다.
- Primary — 제목, 텍스트, UI 골격에 쓰는 어두운 기준색
- Accent — 상호작용 요소, 링크, 포커스 상태에 쓰는 브랜드 파랑
- Info — 안내성 피드백에 쓰는 부드러운 파랑. 브랜드 accent와 구분한다
- Neutral — 텍스트 위계, 테두리, 표면에 쓰는 회색 스케일
- Green / Yellow / Red — 성공, 경고, 에러 상태에 쓰는 상태 색
info와 accent를 나눈 이유
둘 다 파란색이고, 둘을 나누려면 팔레트 그룹이 하나 더 필요하다. 그래도 나눈 이유가 있다. 송금 화면에서 accent 파랑은 누르면 뭔가 할 수 있다는 뜻이고, info 파랑은 시스템이 알려 주는 내용이라는 뜻이다. 두 색이 같은 색조였을 때 사용자들은 안내 배너를 누르면 뭔가 될 거라 생각하고 눌렀다. 그래서 info를 회청색(600 단계 #3B82B8) 쪽으로 옮겨, 한눈에 다른 역할로 보이게 했다.
상태 색은 일부러 차분하게 둔다
상태 색은 흔히 쓰는 값보다 한두 단계 부드럽다. green-600은 채도 높은 #16A34A 대신 #22915A, red-600은 #DC2626 대신 #BB4A4A다.
송금 앱은 늘 상태를 보여 준다. 대기 중인 거래, 환율 알림, 인증 단계마다 상태 색이 붙는다. 채도를 끝까지 올리면 대시보드 전체가 비상 상황처럼 보인다. 상태 색을 차분하게 두어야 실패한 송금이나 반려된 서류 같은 진짜 에러가 다른 색 칩 열한 개에 묻히지 않고 눈에 띈다.
톤을 낮춘 값은 모두 AA 기준으로 다시 검증했다. 색을 부드럽게 하느라 대비를 희생한 경우는 없다.
일반적인 값에서 벗어난 neutral 두 단계
| Token | Conventional | Senco | Why |
|---|---|---|---|
--ds-gray-500 | #6B7280 | #636971 | 흐린 텍스트가 흰 배경뿐 아니라 gray-100 표면에서도 4.5:1을 넘어야 했다 |
--ds-gray-400 | #9CA3AF | #727780 | placeholder도 텍스트다. #9CA3AF는 2.54:1로 기준 미달이다 |
placeholder 대비는 실제 출시된 디자인 시스템에서 가장 흔한 AA 실패다. 리뷰하는 사람은 필드 용도를 이미 알고 있어서 리뷰에서 잘 드러나지도 않는다. 팔레트 단계에서 고쳐 두면 어떤 컴포넌트도 이 문제를 다시 만들 수 없다.
Primary
Neutral / Gray
Accent / Blue
Green
Yellow
Red
Info
시맨틱 사용
컴포넌트는 원시 팔레트 값이 아니라 시맨틱 토큰을 참조한다. 이렇게 한 단계를 거치기 때문에 테마를 바꿀 수 있고, 시스템 전체에서 색의 의도가 일관되게 유지된다.
| Token | Maps to | Purpose |
|---|---|---|
color-primary | {accent-600} | 브랜드, 주요 액션, 링크 |
color-primary-hover | {accent-700} | Primary의 Hover 상태 |
color-primary-light | {accent-50} | Primary 색을 옅게 깐 배경 |
color-bg | {white} | 페이지 배경 |
color-surface | {gray-100} | 카드·섹션 채움 |
color-fg | {gray-700} | 본문 텍스트 |
color-heading | {primary-900} | 제목, 강한 텍스트 |
color-muted | {gray-500} | 보조 텍스트, 힌트, 라벨 |
color-border | {gray-200} | 구분선, 입력 테두리 |
color-link | {accent-600} | 문장 안 링크 |
color-focus | {accent-600} | 포커스 링 |
표면 층위
Senco는 표면을 최대 세 단계까지만 쌓는다. 다이얼로그 안 시트 안의 카드에 그때그때 아무 회색이나 쓰지 않도록, 여기서 한 번 정해 둔다.
| Level | Name | Light | Dark | Used for |
|---|---|---|---|---|
| 0 | Page | --ds-bg #FFFFFF | #0F172A | 페이지 배경 |
| 1 | Raised | --ds-white | #1E293B | 카드, 폼 카드, 다이얼로그, 시트 |
| 2 | Recessed | --ds-surface {gray-100} | #1E293B | 안으로 들어간 채움. 검색 바, 코드 블록, 테이블 헤더 |
규칙:
- 1단계는 라이트 모드에서는 그림자로, 다크 모드에서는 밝기로 띄운다. 라이트 모드는 흰색 위에
--ds-shadow-xs를 쓴다. 다크 모드에서는 거의 검은 바탕에 그림자가 보이지 않으므로,#0F172A위에 더 밝은#1E293B를 올려 단계를 구분한다. - 1단계 표면 두 개를 겹치지 않는다. 카드 안에 카드가 필요하다면, 안쪽은 2단계 인셋이나 구분선으로 바꾼다.
- 그림자로 안 되는 일은 테두리가 맡는다. 2단계와 다크 모드에서는
--ds-border로 표면을 나눈다. 다크 모드에서는 높이만으로 부족하고, 고대비 모드에서는 높이로 구분이 아예 안 된다. --ds-white는 색이 아니라 시맨틱 토큰이다. 다크 모드에서는#1E293B가 된다. 정말 흰색이어야 하는 곳에는#FFFFFF를 직접 쓴다. 실제로 그런 곳은--ds-on-accent뿐이다.
피드백 색
피드백 상태마다 배경, 테두리, 텍스트, 아이콘 토큰을 한 세트로 써서 신호를 일관되게 준다.
| State | Background | Border | Text | Icon |
|---|---|---|---|---|
| Info | {info-50} | {info-400} | {info-800} | {info-700} |
| Success | {green-50} | {green-400} | {green-800} | {green-700} |
| Warning | {yellow-50} | {yellow-400} | {yellow-900} | {yellow-700} |
| Error | {red-50} | {red-400} | {red-800} | {red-700} |
| Tip | {accent-50} | {accent-400} | {accent-800} | — |
Warning 텍스트만 다른 상태처럼 800이 아니라 900 단계를 쓴다. 대비 때문이 아니라 시각적 무게 때문이다. yellow-800(#854D0E)은 yellow-50 위에서 6.46:1로 AA를 넉넉히 통과한다. 하지만 알림 목록에 쌓아 보면 800의 warning 텍스트가 옆의 success, error 텍스트보다 가벼워 보여서 한 단계 더 진하게 했다.
이 문서의 이전 버전은 800이 4.3:1로 기준 미달이라서 900을 쓴다고 설명했다. 사실이 아니었다. 노랑이 AA를 맞추기 가장 어려운 색조인 건 맞기 때문에 그럴듯해 보였고, 그래서 아무도 확인하지 않고 넘어갔다. 문장만 슬쩍 바꾸지 않고, 정정했다는 사실을 여기 남긴다.
전체 토큰 구조(base → semantic → state → component → platform → bridge)는 Tokens에 있다.
| Semantic token | Base token | Resolved | |
|---|---|---|---|
| --ds-bg | --ds-white | ||
| --ds-fg | --ds-gray-700 | ||
| --ds-heading | --ds-primary-900 | ||
| --ds-muted | --ds-gray-500 | ||
| --ds-border | --ds-gray-200 | ||
| --ds-surface | --ds-gray-100 | ||
| --ds-primary | --ds-accent-600 | ||
| --ds-primary-hover | --ds-accent-700 | ||
| --ds-primary-light | --ds-accent-50 | ||
| --ds-success-bg | --ds-green-50 | ||
| --ds-success-border | --ds-green-400 | ||
| --ds-success-text | --ds-green-800 | ||
| --ds-warning-bg | --ds-yellow-50 | ||
| --ds-warning-border | --ds-yellow-400 | ||
| --ds-warning-text | --ds-yellow-900 | ||
| --ds-error-bg | --ds-red-50 | ||
| --ds-error-border | --ds-red-400 | ||
| --ds-error-text | --ds-red-800 | ||
| --ds-info-bg | --ds-info-50 | ||
| --ds-info-border | --ds-info-400 | ||
| --ds-info-text | --ds-info-800 |
대비 검증
모든 색 조합은 WCAG 2.2 AA 기준(텍스트 4.5:1, UI 컴포넌트 3:1)으로 검증한다. 아래 비율은 모두 스크린샷에서 색을 뽑아 잰 값이 아니라, 실제로 배포되는 토큰 값으로 계산했다.
라이트 모드 (검증됨)
| Pairing | Foreground | Background | Ratio | Pass |
|---|---|---|---|---|
| 본문 텍스트 | #374151 | #FFFFFF | 10.31:1 | ✓ |
| 제목 | #111827 | #FFFFFF | 17.74:1 | ✓ |
| 흐린 텍스트 | #636971 | #FFFFFF | 5.54:1 | ✓ |
| 표면 위 흐린 텍스트 | #636971 | #F3F4F6 | 5.04:1 | ✓ |
| Primary 링크 | #0F62FE | #FFFFFF | 5.00:1 | ✓ |
Placeholder (--ds-placeholder) | #727780 | #FFFFFF | 4.50:1 | ✓ |
| Primary 버튼 위 흰 글자 | #FFFFFF | #0F62FE | 5.00:1 | ✓ |
| 파괴적 액션 위 흰 글자 | #FFFFFF | #BB4A4A | 5.01:1 | ✓ |
| Info 텍스트 | #1E4D8C | #F0F7FF | 7.80:1 | ✓ |
| Success 텍스트 | #166534 | #F0FAF3 | 6.68:1 | ✓ |
| Warning 텍스트 | #713F12 | #FDF8EA | 8.17:1 | ✓ |
| Error 텍스트 | #922525 | #FDF3F3 | 7.67:1 | ✓ |
| Tip 텍스트 | #002D9C | #F5F8FF | 10.65:1 | ✓ |
다크 모드 대비 검증은 Themes에 있다.
텍스트 외 대비
기준을 넘어야 하는 건 텍스트만이 아니다. WCAG 2.1 SC 1.4.11은 UI 컴포넌트와 의미를 담은 그래픽에도 3:1을 요구한다.
| Element | Foreground | Background | Ratio | Pass |
|---|---|---|---|---|
| 입력 테두리 | {gray-200} #E5E7EB | #FFFFFF | 1.24:1 | ✗ (아래 참고) |
| 입력 테두리 (포커스) | #0F62FE | #FFFFFF | 5.00:1 | ✓ |
| 포커스, 테두리 있는 컨트롤의 테두리 | #0F62FE | #FFFFFF | 5.00:1 | ✓ (테두리가 곧 포커스 표시) |
| 포커스, 테두리 없는 컨트롤의 외곽선 | #0F62FE 3px | #FFFFFF | 5.00:1 | ✓ |
| 20% 포커스 후광 | #CFE0FF | #FFFFFF | 1.33:1 | 해당 없음 (포커스 표시가 아닌 장식) |
| 상태 점, success | #22915A | #FFFFFF | 3.99:1 | ✓ |
| 상태 점, warning | #C07C14 | #FFFFFF | 3.42:1 | ✓ |
| 상태 점, error | #BB4A4A | #FFFFFF | 5.01:1 | ✓ |
| 구분선 | {gray-200} | #FFFFFF | 1.24:1 | ✓ (장식) |
| Info 테두리 | {info-400} | {info-50} | 1.71:1 | ✗ (아래 참고) |
| Success 테두리 | {green-400} | {green-50} | 1.89:1 | ✗ (아래 참고) |
| Warning 테두리 | {yellow-400} | {yellow-50} | 1.82:1 | ✗ (아래 참고) |
| Error 테두리 | {red-400} | {red-50} | 2.40:1 | ✗ (아래 참고) |
피드백 테두리 네 개는 알고 있고, 허용한 예외다. SC 1.4.11은 컴포넌트가 있다는 것, 또는 그 상태를 알려 주는 표시가 경계뿐일 때 적용된다. 피드백 알림은 옅은 색 배경, 700 단계 아이콘, 텍스트 라벨로 이미 스스로를 드러내고, 셋 모두 각자의 기준을 통과한다. 400 단계 테두리는 이미 구분되는 표면을 다듬는 역할일 뿐, 표면을 구분하게 해 주는 요소가 아니다. 테두리를 3:1에 맞추려면 600 단계까지 올려야 하는데, 그러면 알림이 옅은 색 상자가 아니라 테두리가 굵은 경고 상자처럼 보이고, 앞에서 정한 차분한 상태 색 원칙이 깨진다. 이 절충을 말없이 안고 가지 않고 기록해 둔다.
기본 상태 입력 테두리의 1.24:1이 두 번째 알고 있고, 허용한 예외다. SC 1.4.11은 경계 말고는 컨트롤이 있다는 걸 알 방법이 없을 때 적용된다. Senco 입력 필드는 항상 위에 라벨이 보이고 48px 높이의 채워진 컨트롤 영역이 있어서, 테두리 없이도 필드를 알아볼 수 있다. 테두리를 gray-400으로 진하게 해 봤더니, 필드가 많은 폼(연락처 주소 폼은 필드가 아홉 개다)이 철창처럼 보였다. 이 예외도 말없이 넘기지 않고 기록한다.
색만으로 신호를 주지 않는다
시스템의 모든 상태는 반드시 색 + 아이콘 + 텍스트 세 가지로 함께 표시한다.
| Status | Color | Icon | Text |
|---|---|---|---|
| Completed | {green-600} | Check | "Completed" |
| Pending | {yellow-600} | Clock | "Pending" |
| Failed | {red-600} | Alert triangle | "Failed" |
| Processing | {accent-600} | Spinner | "Processing" |
남성 약 12명 중 1명은 색각 이상이 있다. 그중 제2색맹은 이 앱이 쓰는 초록/노랑/빨강 구분을 정확히 무너뜨린다. 시뮬레이션해 보면 green-600과 yellow-600이 거의 같은 탁한 색으로 보인다. 의미를 전하는 건 아이콘과 라벨이고, 색은 거드는 역할이다. Status indicators 참고.
대비 검증 방식
검증은 한 번 하고 끝내는 감사가 아니다. 기본 토큰이 바뀌면 이 표의 조합을 전부 다시 확인한다. 기본 값 하나만 고쳐도 시맨틱 계층을 타고 모든 컴포넌트에 영향이 가기 때문이다. --ds-accent-600을 바꾸면 링크, 포커스 링, Primary 버튼, tip 토큰 행을 한꺼번에 다시 검증해야 한다.
/ BG: 사용 가이드라인
- 컴포넌트에서는 항상 시맨틱 토큰을 쓴다. 기본 팔레트를 직접 참조하지 않는다
- 상태 표시에는 색과 함께 아이콘과 텍스트를 쓴다 (색만 쓰지 않는다)
- 포커스: 테두리 있는 컨트롤은 테두리를
--ds-focus로 바꾸고 20% 후광을 더한다. 테두리 없는 컨트롤은 2px 오프셋을 둔 3px 실선--ds-focus외곽선을 쓴다. Tokens 참고 - 비활성 상태도 색만이 아니라 다른 시각적 단서를 함께 쓴다
권장과 금지
| Do | Don't |
|---|---|
Use --ds-primary for the one action you want taken on a screen | Use brand blue for every clickable thing until nothing is primary |
Use --ds-error-* for states the user must resolve | Use red for a neutral "remove" affordance that is easily undone |
| Let a status chip carry icon and label alongside its color | Ship a bare colored dot as the only status indicator |
| Put green on a completed transfer | Put green on a submitted transfer — money has not moved yet |
Use --ds-surface for an inset region inside a card | Nest a second white card inside a white card |
Reference --ds-muted for helper text | Reach for gray-400 because the helper text "looks heavy" |
가장 중요한 건 제출과 완료를 구분하는 규칙이다. 돈을 다루는 앱에서 초록은 끝났다로 읽히는데, 해외 송금은 제출된 뒤 이틀이 지나서도 실패할 수 있다. 제출은 --ds-info-*, 완료는 --ds-success-*로 표시한다.
어두운 표면 위의 색
카메라 뷰파인더, QR 스캐너처럼 항상 어두운 표면은 테마를 따르지 않는다. 두 모드 모두 #111827을 쓰고, 오버레이에는 밝은 값을 하드코딩한다. 라이트 모드에서 뷰파인더가 뒤집히면 사용자는 익숙한 기준을 잃기만 하고 얻는 건 없다. 시맨틱 테마에서 유일하게 문서화한 예외이고, Tokens에 정리했다.
알려진 공백
| Item | Status |
|---|---|
| 기본 상태 입력 테두리 1.24:1 | 허용한 예외. 근거는 위에 있다 |
고대비 모드 (forced-colors) | 구현 안 됨. 시스템 색이 덮어쓸 것으로 보지만 테스트 안 함 |
| 차트 팔레트 | 기존 토큰을 --chart-1…5로 연결함. 범주끼리 구분되는지는 검증 안 함 |
| 스켈레톤 토큰 | 정의 안 됨. 지금은 스켈레톤이 --ds-surface를 직접 쓴다 |
background: var(--ds-surface);
Semantic tokens adapt to themes automatically.
background: #F3F4F6;
Hardcoded hex breaks in dark mode and ignores token updates.
Semantic colors with icon + background reinforcement. Success is clear at a glance.
Colored text on white — no background, no icon. Easy to miss and inaccessible.
Heading at 17.7:1, muted at 5.5:1 — both pass AA. Financial data must be readable.
Light gray text fails contrast. Users can't read their own transfer summary.