Chapter 06

색과 글자의 체계

지후가 행사 일정 페이지의 코드에서 "초록"을 검색했다. #2f7d5b, #2e7d5a, #307c5b… 눈으로는 구별되지 않는 초록이 여섯 가지 나왔고, 글자 크기는 17px, 1.0625rem, 1.1em이 뒤섞여 있었다. 다크 모드를 넣어 보자며 모든 색의 명도를 뒤집었더니 책방 초록은 검은 바탕에 가라앉아 탁해졌고, 카드의 그림자는 아예 사라졌다. 색과 글자 크기를 하나하나 고르는 일은 ColorBook과 TypeBook에서 이미 했다. 이 장은 그 결정들을 한 곳에 이름 붙여 두고, 한 곳을 바꾸면 화면 전체가 따라 바뀌게 만드는 체계를 다룬다.

흩어진 값, 이름 붙은 결정

웹사이트를 몇 달 만들다 보면 같은 결정이 여러 자리에 복사된다. 버튼의 초록, 링크의 초록, 날짜 배지의 초록. 처음에는 같은 값을 붙여 넣었지만, 어느 날 디자인 도구에서 눈으로 색을 찍어 온 값이 한 자리 섞이고, 다른 날 반올림한 값이 섞인다. #2f7d5b와 #2e7d5a는 화면에서 구별되지 않지만 코드에서는 다른 색이다. 그래서 "초록을 조금 밝게"라는 결정 하나를 반영하려면 모든 파일을 뒤져 비슷한 값을 다 찾아야 하고, 하나라도 놓치면 화면에 초록이 두 개가 된다.

디자인 토큰(Design token)은 이 문제를 이름으로 푼다. 토큰은 이름이 붙은 디자인 결정 하나다. "버튼 바탕은 이 초록", "본문 글자는 이 크기", "카드 사이 간격은 24픽셀". 값은 한 곳에만 적고, 쓰는 곳은 모두 이름을 부른다. 이 이름은 세일즈포스의 디자인 시스템 팀이 2014년 무렵 쓰기 시작해 널리 퍼진 것으로 알려져 있고, 지금은 W3C의 디자인 토큰 커뮤니티 그룹(Design Tokens Community Group)이 디자인 도구와 코드 사이에 토큰을 주고받는 파일 형식을 정리하고 있다.

웹에서 토큰을 담는 가장 단순한 그릇은 CSS의 사용자 정의 속성(Custom property), 흔히 "CSS 변수"라고 부르는 것이다. --로 시작하는 이름에 값을 적고, 쓰는 곳에서 var()로 부른다. 변수는 다른 변수를 부를 수 있어서, 토큰을 여러 층으로 쌓을 수 있다.

:root {
  /* ① 원시 토큰: 실제 값. 색 이름과 단계 번호 */
  --green-600: #2f7d5b;
  --green-400: #5eaa85;
  --neutral-50: #f6fcf8;

  /* ② 의미 토큰: 쓰임의 이름. 원시 토큰을 가리킨다 */
  --color-action: var(--green-600);
  --color-bg: var(--neutral-50);

  /* ③ 컴포넌트 토큰: 부품의 자리. 의미 토큰을 가리킨다 */
  --button-bg: var(--color-action);
}
.button { background: var(--button-bg); }

세 층은 맡은 질문이 다르다. 원시 토큰(Primitive token)은 "어떤 색들이 있는가"에 답한다. ColorBook 14장에서 서윤이 만든 초록 사다리 열 칸과 중립 사다리 열 칸이 그대로 원시 토큰이 된다. 의미 토큰(Semantic token)은 "이 색은 무엇에 쓰는가"에 답한다. 바탕, 본문 글자, 흐린 글자, 테두리, 주된 행동. 컴포넌트 토큰(Component token)은 "이 부품의 이 자리는 무엇을 쓰는가"에 답한다. 아래 시뮬레이터의 왼쪽은 책방 E 행사 카드의 토큰 나무, 오른쪽은 그 토큰으로 칠한 카드다. 원시 초록의 색상을 돌리거나, 의미 토큰 "주된 행동"이 가리키는 칸을 바꿔 본다.

SIMULATOR

토큰 나무: 한 곳을 바꾸면 따라 바뀌는 것

토큰 위에 올리면(누르면) 이어진 길이 보인다
버튼 글자 대비—
링크 대비(카드 위)—
따라오지 않은 부품—
해볼 것: ① 색상을 162°에서 200° 쪽으로 돌린다. 원시 초록 사다리가 청록으로 바뀌고, 그것을 가리키는 의미 토큰과 부품 토큰을 따라 버튼, 링크, 배지가 한꺼번에 바뀐다. 바꾼 곳은 원시 토큰 하나뿐이다. ② "action → 500"을 누른다. 원시 값은 그대로이고 화살표 하나가 옮겨 갔을 뿐인데, 버튼과 링크가 함께 밝아지고 흰 글자 대비가 4.5 아래로 떨어진다. ③ "값을 직접 적은 부품 섞기"를 켜고 다시 색상을 돌린다. 버튼, 링크, 배지에 hex가 직접 적혀 있어 옛 초록에 남는다. 나무에서 붉은 테두리로 보이는 마디다. (사다리는 ColorBook 14장의 웹 팔레트 v1을 OKLCH 명도·채도 그대로 색상만 돌려 만들었다. 대비는 이 책의 엔진 ART.contrast로 잰 WCAG 2 대비비다.)

원시 토큰을 바꾸면 그 값을 쓰는 모든 의미와 부품이 바뀐다. 의미 토큰의 화살표를 바꾸면 그 쓰임만 바뀐다. 컴포넌트 토큰을 바꾸면 그 부품의 그 자리만 바뀐다. 층이 있으면 결정의 크기에 맞는 곳을 고칠 수 있다. 브랜드 색을 바꾸는 큰 결정은 원시 층에서, "버튼을 한 칸 진하게"는 의미 층에서, "이 카드의 테두리만 없애자"는 컴포넌트 층에서 한다. 그리고 ③에서 본 것처럼 값을 직접 적은 자리가 하나라도 있으면 체계는 그 자리에서 끊긴다. 지후가 검색으로 찾은 여섯 초록이 바로 그런 끊긴 자리들이었다.

이름 짓기: 원시에서 의미로

토큰 체계의 절반은 이름이다. ColorBook 14장의 작업실 노트가 말했듯 토큰 이름을 green-600처럼 색으로 지으면, 버튼을 청록으로 바꾸는 순간 이름이 거짓말이 된다. 그래서 색 이름은 원시 층에만 둔다. 의미 층은 "무엇에 쓰는가"로 짓는다. 이름을 짓는 틀은 팀마다 다르지만 대개 몇 조각을 정해진 순서로 잇는다.

의미 토큰 이름의 조각 -- color - text - muted 범주색 · 글자 · 간격 대상바탕 · 글자 · 테두리 변형흐림 · 강함 · on- "신청하기" 버튼 바탕이 거쳐 가는 길 부품 button-bg 의미 color-action 원시 green-600 밝은 화면 · #2f7d5b green-400 어두운 화면 · #5eaa85 부품 코드는 그대로, 화살표만 화면 모드를 따른다
그림 1. 토큰 이름의 조각과 화살표. 위는 의미 토큰 이름을 범주(color) · 대상(text) · 변형(muted) 순서로 이은 것이다. 아래는 행사 카드의 "신청하기" 버튼 바탕이 거쳐 가는 길이다. 부품은 의미 토큰만 부르고, 의미 토큰은 화면 모드에 따라 다른 원시 토큰을 가리킨다. 밝은 화면에서는 초록 600, 어두운 화면에서는 초록 400이다. 부품 쪽 코드는 한 글자도 바뀌지 않는다.

이름을 정할 때 지키면 좋은 규칙이 몇 가지 있다. 첫째, 부품은 원시 토큰을 직접 부르지 않는다. 버튼이 --green-600을 직접 부르면 다크 모드에서 버튼만 따로 고쳐야 한다. 둘째, 짝을 이름에 드러낸다. 주된 행동의 바탕이 --color-action이면 그 위에 얹는 글자는 --color-on-action이다. 바탕과 글자를 짝으로 부르면 대비 검사도 짝으로 할 수 있다(4절). 셋째, 의미 토큰의 수는 적게 시작한다. 바탕, 표면, 글자, 흐린 글자, 테두리, 주된 행동, 주된 행동 위 글자 일곱 개면 대부분의 화면이 칠해진다. 새 토큰은 "기존 토큰으로는 이 자리의 쓰임을 말할 수 없을 때"만 더한다.

글자 크기와 간격도 같은 방식으로 토큰이 된다. 3장에서 정한 간격 단계 4·8·12·16·24·32·48·64가 --space-1부터 --space-8까지가 되고, TypeBook에서 정한 본문 크기와 5장의 제목 식이 --text-body, --text-h1이 된다. 간격 토큰에 번호를 붙이는 까닭은 간격에는 "쓰임"보다 "크기 순서"가 중요하기 때문이다. 다만 자주 쓰는 자리에는 의미 이름을 덧붙인다. 예를 들어 --space-card-gap: var(--space-5).

디자인 파일과 코드에 같은 이름을 토큰은 디자이너와 개발자가 같은 말을 쓰게 하는 장치다. 시안 파일의 색 견본 이름이 "메인 초록 2"이고 코드의 변수 이름이 --color-action이면, 시안을 넘길 때마다 누군가 번역을 해야 하고 번역은 틀린다. 대부분의 디자인 도구에는 변수나 스타일에 이름을 붙이고 밝은·어두운 모드 값을 따로 넣는 기능이 있다. 시안의 이름을 코드의 이름과 한 글자도 다르지 않게 맞추고, 시안에서 어떤 요소를 골랐을 때 hex 대신 토큰 이름이 보이게 한다. 지후는 이렇게 말했다. "시안에서 #2f7d5b가 보이면 저는 그걸 또 복사해요. color-action이 보이면 복사할 게 없죠."

다크 모드는 반전이 아니다

운영체제와 브라우저는 사용자가 어두운 화면을 원하는지를 웹 페이지에 알려 준다. CSS의 @media (prefers-color-scheme: dark)가 그 신호를 받는다(Media Queries Level 5). 토큰이 있으면 다크 모드는 의미 토큰의 화살표를 한 벌 더 적는 일이 된다. 문제는 그 화살표가 어디를 가리켜야 하는가다. 가장 먼저 떠오르는 방법은 반전이다. 밝은 것은 어둡게, 어두운 것은 밝게. 그런데 반전에는 두 가지가 있고, 둘 다 다른 이유로 실패한다.

하나는 명도 뒤집기다. 모든 토큰의 OKLCH 명도 \(L\)을 \(1 - L\)로 바꾸고 채도와 색상은 둔다. 바탕(0.985)은 거의 검정(0.015)이 되고 짙은 글자(0.306)는 밝은 회색(0.694)이 된다. 그런데 명도가 가운데쯤인 색은 거의 제자리다. 책방 초록(0.532)은 0.468이 되어, 검은 바탕 위에서 오히려 더 어둡고 무겁게 가라앉는다. 다른 하나는 브라우저의 색 반전 필터(filter: invert(1) hue-rotate(180deg))다. sRGB 값을 통째로 뒤집은 뒤 색상을 반 바퀴 돌려 되돌린다. 초록은 의외로 그럴듯하게 살아남지만, 사진은 필름 음화가 되고 그림자는 빛나는 테두리가 된다.

아래 시뮬레이터의 왼쪽은 밝은 화면의 행사 카드, 오른쪽은 세 방법으로 만든 어두운 화면이다. 읽음값은 이 책의 엔진으로 잰 대비와 명도다.

SIMULATOR

어두운 화면을 만드는 세 가지 방법

어두운 화면의 주색—
주색 대비(바탕)—
흐린 글자 대비—
카드와 바탕의 구분—
해볼 것: ① "명도 뒤집기"에서 오른쪽 카드를 본다. 초록 버튼과 링크가 검은 바탕에서 탁하게 가라앉고(대비 3.2), 흐린 글자는 3.1로 읽기 어렵다. 흰 카드와 거의 흰 바탕이 둘 다 검정이 되어 카드의 경계가 사라지고, 검은 그림자는 검은 바탕에 묻힌다. ② "반전 필터"로 바꾼다. 글자와 초록은 그럴듯하지만 사진이 음화가 되고 그림자가 밝은 테두리로 빛난다. 크림색 안내 띠는 짙은 밤색이 된다. ③ "따로 설계한 값"을 고른다. 주색은 사다리의 400번, 카드는 바탕보다 한 단계 밝은 표면으로 떠오르고, 그림자 대신 표면의 밝기 차이가 높이를 말한다. "카드 그림자"를 끄고 켜서 세 방법에서 그림자가 하는 일을 비교한다. (명도 뒤집기는 OKLCH L → 1 − L 뒤 색역 맞춤, 반전 필터는 CSS Filter Effects의 invert와 hue-rotate 행렬을 화소마다 적용했다. 화면 색은 기기마다 다르다.)

설계한 어두운 화면에는 몇 가지 원칙이 들어 있다. 바탕은 순수한 검정이 아니라 아주 어두운 회색(중립 900, OKLCH 명도 0.21)이다. 글자도 순수한 흰색이 아니라 아주 밝은 회색(중립 100)이다. 주색은 밝은 화면보다 두 칸 밝은 초록 400을 쓴다. ColorBook 9장에서 본 것처럼 같은 색도 어두운 바탕 위에서는 더 어둡고 무겁게 보이기 때문이고, 대비로 재도 600번은 어두운 바탕에서 3.55에 그친다. 그리고 주색이 밝아졌으니 그 위에 얹는 글자는 흰색이 아니라 짙은 색이 된다. 의미 토큰 --color-on-action이 짝으로 함께 바뀌는 까닭이다.

밝은 화면: 그림자 어두운 화면: 밝은 표면 바탕 neutral-50 카드 흰색 + 그림자 바탕 neutral-900 카드 800 · 메뉴 700 그림자가 아래 면과의 거리를 말한다 위의 면일수록 한 단계 밝게
그림 2. 높이를 말하는 두 가지 방법. 밝은 화면(왼쪽)에서는 떠 있는 카드가 그 아래에 그림자를 드리운다. 어두운 바탕에서는 그림자가 보이지 않으므로(오른쪽), 위에 있는 면일수록 조금씩 밝은 표면을 쓴다. 바탕 중립 900, 카드 중립 800, 카드 위에 뜬 메뉴는 그보다 한 단계 더 밝다. 빛이 위에서 온다면 위에 있는 면이 빛을 더 받는다는 생각과 같다.
왜 순수한 검정과 흰색을 피하나 검정 바탕(#000000)에 흰 글자(#ffffff)는 대비 21:1, 지침이 허용하는 가장 큰 대비다. 그런데 어두운 화면에서는 동공이 커지고, 밝은 글자의 빛이 주변으로 번져 보이는 일이 생긴다. 난시가 있는 사람에게 더 심하다는 설명이 흔히 실리지만, 이를 직접 잰 연구는 많지 않아 근거의 수준은 "실무의 통설"에 가깝다. 확실한 것은 대비를 조금 낮춰도 손해가 거의 없다는 점이다. 이 장의 어두운 화면에서 본문 글자의 대비는 15.8로, WCAG의 가장 엄격한 기준(7:1)보다 훨씬 높다. 반대로 어두운 화면을 이유로 대비를 너무 낮추면 흐린 글자가 먼저 기준 아래로 떨어진다. 어두운 화면에서도 대비 검사는 밝은 화면과 똑같이 한다.

다크 모드에서 바꾸지 말아야 할 것도 있다. 사진과 그림은 반전하지 않는다. 다만 밝은 사진이 어두운 화면에서 눈부시면 밝기를 조금 낮추는 정도는 쓴다. 로고는 ColorBook 9장에서 서윤이 만든 어두운 바탕용 변형처럼 따로 준비한 판을 쓴다. 그리고 CSS의 color-scheme: light dark를 적어, 스크롤 막대나 입력 칸처럼 브라우저가 그리는 부분도 어두운 화면에 맞춰 그리게 한다. 마지막으로, 운영체제 설정을 따르는 것을 기본으로 두되 페이지 안에서도 바꿀 수 있게 한다. 이 책의 오른쪽 위 테마 단추가 그런 예다.

반전 필터를 다크 모드로 내보내지 않는다 html { filter: invert(1) hue-rotate(180deg); } 한 줄로 다크 모드를 "완성"하는 요령이 돌아다닌다. 시안 단계에서 어두운 화면의 대략적인 느낌을 보는 데는 쓸 수 있다. 하지만 사진, 지도, 그림, 로고가 모두 뒤집히고, 그것을 되돌리려고 이미지마다 필터를 한 번 더 거는 순간 코드는 토큰 없는 시절보다 복잡해진다. 브랜드 색도 필터의 계산식이 정한 값이 되어, 그 값이 대비 기준을 넘는지는 우연에 맡겨진다.

짝을 정하면 대비 검사가 끝난다

ColorBook 13장과 TypeBook 7장에서 서윤은 글자와 바탕의 대비 기준을 정했다. 웹 콘텐츠 접근성 지침 WCAG 2.2의 성공 기준 1.4.3은 본문 글자에 4.5:1, 큰 글자(24px 이상, 또는 굵게 18.66px 이상)에 3:1을 요구하고, 1.4.6(AAA)은 각각 7:1과 4.5:1을 요구한다. 1.4.11은 버튼 테두리나 아이콘처럼 글자가 아닌 요소에 3:1을 요구한다. 문제는 화면의 모든 글자와 모든 바탕을 짝지어 검사하는 일이 끝이 없다는 것이다.

토큰이 있으면 이 일이 유한해진다. 글자 쪽 의미 토큰이 다섯 개, 바탕 쪽 의미 토큰이 세 개라면 짝은 열다섯 개뿐이다. 이 열다섯 짝을 밝은 화면과 어두운 화면에서 한 번씩 재고, 기준에 못 미치는 짝은 "쓰지 않는 조합"으로 표에 적는다. 그러면 새 화면을 만들 때 검사할 것은 "표에 없는 조합을 쓰지 않았는가" 하나다. 아래 표의 칸을 누르면 그 짝을 크게 보여 준다.

SIMULATOR

글자 토큰 × 바탕 토큰 대비표

칸을 누르면 그 짝을 크게 본다
기준을 넘는 짝—
가장 낮은 짝—
고른 짝—
해볼 것: ① "밝은 화면 · 본문 4.5"에서 붉은 칸 여섯 개를 찾는다. 강조색 #d24f39는 바탕 위 4.1이라 본문에는 못 쓰고, 크림 띠 위에서는 흐린 글자와 주색도 4.1, 위험색은 4.3에 그친다. TypeBook 7장에서 "크림 위에는 짙은 글자만"이라고 정한 이유가 표에 그대로 보인다. "큰 글자 3"으로 바꾸면 이 칸들이 통과한다. ② "어두운 화면"으로 바꾼다. 열다섯 짝이 모두 통과한다. 어두운 화면의 글자 토큰은 밝은 화면과 다른 칸을 가리키기 때문이다. ③ 어두운 바탕 명도를 한 칸(0.22)만 올려 본다. 바탕과 표면, 띠가 함께 밝아지면서 주색과 강조의 띠 칸이 먼저 붉어지고, 0.28부터는 흐린 글자도 떨어진다. 0.42에서는 본문 글자의 세 칸만 남는다. 이 장의 바탕 0.21이 "너무 검지 않게" 올릴 수 있는 한계에 거의 붙어 있다는 뜻이다. (표면은 바탕보다 OKLCH 명도 0.09 밝게, 어두운 크림 띠는 바탕 명도 + 0.11로 둔다. 대비는 ART.contrast로 잰 WCAG 2 대비비다.)

표에서 붉은 칸은 "그 색이 틀렸다"는 뜻이 아니라 "그 짝을 본문에 쓰지 않는다"는 뜻이다. 강조색은 큰 제목과 장식에 쓰고, 크림 띠에는 짙은 글자만 올린다. 이 규칙을 문서로만 남기면 잊힌다. 그래서 토큰 이름에 짝을 넣는 방법이 쓰인다. 크림 띠의 바탕이 --color-band라면 그 위에 쓸 글자는 --color-on-band 하나만 두고, 그 값은 짙은 색으로 고정한다. 크림 띠 위에 흐린 글자를 쓰고 싶어도 쓸 토큰이 없다. 체계가 실수를 막는 방식이다.

rem: 글자 크기 설정을 받아 주는 단위

색의 토큰이 화면 모드를 따라야 한다면, 글자의 토큰은 사람을 따라야 한다. 브라우저에는 기본 글자 크기 설정이 있다. 대개 16px이고, 시력이 약한 사람은 이것을 20px, 24px로 올려 둔다. 이 설정이 페이지에 전해지는지는 크기를 어떤 단위로 적었느냐에 달려 있다.

단위기준기본 글자 크기를 24px로 올리면
pxCSS 픽셀17px은 17px 그대로. 설정이 무시된다.
rem루트(html)의 글자 크기1.0625rem은 16 × 1.0625 = 17px에서 24 × 1.0625 = 25.5px로 커진다.
em그 요소(또는 부모)의 글자 크기함께 커지지만, 겹쳐 쓰면 곱해져 예상보다 커지거나 작아진다.

브라우저의 확대(Ctrl +)는 이것과 다르다. 확대는 CSS 픽셀 자체를 키우므로 px로 적은 글자도 커진다. 그 대신 화면 폭이 CSS 픽셀로 좁아져 5장의 중단점이 바뀐다. 기본 글자 크기 설정은 글자만 키운다. 그래서 px로 적은 페이지는 확대에는 반응하지만 글자 크기 설정에는 반응하지 않는다. 두 가지 모두를 받아 주려면 글자 크기를 rem으로 적고, 중단점을 em으로 적는다. TypeBook 11장에서 서윤이 루트 글자 크기를 건드리지 않기로 한 것도 이 때문이다. html { font-size: 62.5%; }처럼 루트를 10px로 바꾸는 요령은 계산을 쉽게 하지만, 사용자가 정한 기준을 디자이너가 덮어쓴다.

글자만 rem으로 바꾸고 나머지를 px로 두면 새로운 문제가 생긴다. 높이를 44px로 고정한 버튼 안에서 글자가 커지면 글자가 위아래 여백을 밀어내며 상자에 꽉 끼고(더 커지면 상자 밖으로 흘러나간다), 높이를 고정한 카드에서는 글이 카드 밖으로 넘친다. 아래 시뮬레이터는 폭 768px 화면(태블릿을 세로로 든 정도)의 행사 목록이다. 기본 글자 크기를 올리며 세 가지 적기 방식을 비교한다.

SIMULATOR

기본 글자 크기를 올리면

본문 글자—
모양—
끼거나 넘친 곳—
해볼 것: ① "모두 px"에서 기본 글자 크기를 24px로 올린다. 화면이 하나도 바뀌지 않는다. 깨지지 않으니 좋아 보이지만, 글자를 키워야 읽는 사람의 설정을 무시한 것이다. ② "글자만 rem"으로 바꾼다. 글자는 25.5px로 커지는데 높이를 고정한 배지(24px)와 버튼(44px)은 그대로라 글자가 상자에 꽉 끼거나 아래로 밀리고, 높이를 고정한 카드에서는 글이 카드 밖으로 넘친다. 중단점이 560px로 고정되어 있어 768px 화면은 여전히 행 모양이고 글 칸이 비좁다. ③ "rem + em 중단점"으로 바꾼다. 높이 대신 안쪽 여백을 rem으로 주어 상자가 글자를 따라 커지고, 35em 중단점이 24 × 35 = 840px로 밀려 768px 화면이 1단 쌓기로 바뀐다. (행 모양의 판정은 5장과 같은 가상 모형이다. 글 칸 12자 미만이면 비좁음으로 친다.)

세 번째 방식이 이 장의 결론이다. 글자 크기는 rem, 글자를 담는 상자는 높이 대신 rem 여백, 중단점은 em. 그러면 사용자가 글자를 키웠을 때 페이지는 "같은 모양을 크게"가 아니라 "더 좁은 화면의 모양"으로 바뀐다. 글자를 1.5배로 키운 768px 화면은 글자 기준으로 보면 512px 화면과 같기 때문이다. 다만 화면 가장자리 여백처럼 글자와 상관없는 값까지 rem으로 적으면, 글자를 키운 사람의 좁은 화면에서 여백이 글 칸을 먹는다. 무엇을 글자에 묶고 무엇을 화면에 묶을지 토큰마다 정한다.

색 견본 카드로 토큰 나무 만들기 문구점의 색종이나 페인트 가게의 색 견본 카드를 열 장쯤 모은다. 큰 종이에 세 단을 그리고 왼쪽 단에 견본을 밝은 것부터 어두운 것까지 붙인다. 이것이 원시 토큰이다. 가운데 단에는 견본 대신 이름표만 쓴다. "바탕", "글자", "흐린 글자", "주된 행동", "주된 행동 위 글자". 오른쪽 단에는 자기 방의 물건이나 즐겨 쓰는 앱의 부품(단추, 카드, 알림)을 적는다. 그리고 실이나 연필 선으로 오른쪽 → 가운데 → 왼쪽을 잇는다. 다 이었으면 가운데 단의 화살표만 다른 견본으로 옮겨 "어두운 방" 판을 만든다. 실을 몇 가닥 옮겨야 했는지 센다. 마지막으로 브라우저 설정에서 기본 글자 크기를 "매우 크게"로 바꾸고 즐겨 찾는 웹사이트 세 곳을 열어, 글자가 따라 커지는 곳과 그대로인 곳을 적는다.

글자 크기 단계

글자 크기 토큰은 몇 개가 적당할까. TypeBook 8장에서 본 대로 크기 단계가 너무 많으면 위계가 흐려지고, 단계 사이가 너무 좁으면 서로 구별되지 않는다. 그래서 흔히 본문 크기에 일정한 비율을 거듭 곱해 단계를 만든다. 비율에는 음정의 이름을 붙이곤 한다. 1.2(단3도), 1.25(장3도), 1.333(완전4도), 1.5(완전5도), 1.618(황금비). 웹 디자이너 팀 브라운(Tim Brown)이 2011년 A List Apart의 글 「More Meaningful Typography」에서 이런 모듈러 스케일(Modular scale)을 웹에 소개했다.

웹에서는 비율 하나로 모든 화면을 덮기 어렵다. 넓은 화면에 맞춘 큰 비율은 휴대폰에서 제목을 두세 줄로 꺾고, 휴대폰에 맞춘 작은 비율은 넓은 화면에서 위계를 밋밋하게 만든다. 그래서 좁은 화면에서는 비율을 조금 줄이고 넓은 화면에서는 늘이는 방법을 쓴다. 두 화면의 크기를 5장의 clamp()로 이으면 그 사이의 화면에서는 비율도 서서히 바뀐다. 아래에서 비율을 바꾸며 360px 화면과 1,280px 화면의 단계를 본다.

SIMULATOR

비율로 만드는 글자 크기 단계

이 화면의 비율—
페이지 제목(h1)—
점검—
해볼 것: ① 처음 상태는 넓은 화면 비율 1.333(완전4도), 360px 화면은 비율을 줄여 1.2다. 본문 17px에서 h1이 29px 남짓이고 "2월 행사와 모임 일정"이 한 줄에 들어간다. ② "좁은 화면은 비율 줄이기"를 끈다. 360px 화면에도 1.333을 쓰면 h1이 40px을 넘어 같은 제목이 두 줄로 꺾인다. ③ 비율을 1.618까지 올리고 "1,280px 화면"을 본다. h1이 본문의 네 배를 넘어 제목만 화면을 차지한다. 비율을 1.125로 내리면 h3와 본문이 거의 구별되지 않는다(이웃 단계 비가 1.15 미만이면 표시). (본문은 360px에서 17px, 1,280px에서 18px. 작은 글자는 비율과 상관없이 14px로 둔다(TypeBook 7장). 좁은 화면의 비율은 1 + (넓은 화면 비율 − 1) × 0.6. 제목 줄 수는 이 화면의 글꼴로 실제로 나누어 센 값이다.)

단계를 정했으면 실제 값은 반올림해 토큰으로 적는다. 계산기에서 나온 29.38px을 그대로 쓰기보다 30px처럼 기억하기 쉬운 값으로 고르고, 앞 장들에서 이미 정한 값(5장의 제목 30 → 40px)과 맞춘다. 비율은 출발점이고, 최종 값은 실제 글과 화면에서 눈으로 고른다. TypeBook 13장에서 서윤이 1.25와 1.5 사이에서 1.333을 고른 것도 비율 표가 아니라 휴대폰에서 꺾이는 제목을 보고서였다.

토큰 작업대: 흩어진 값에서 체계로

마지막으로 책방 E 행사 일정 페이지를 두 판으로 비교한다. "첫 코드"는 지후가 검색으로 찾아낸 그대로다. 색은 hex로, 크기는 px로 자리마다 적혀 있고, 어두운 화면은 명도 뒤집기로 만들었다. "서윤의 토큰"은 이 장에서 정한 체계다. 화면 모드와 기본 글자 크기를 바꾸면서 아래 칸에 적힌 CSS와 읽음값을 본다.

SIMULATOR

행사 일정 페이지: 첫 코드와 서윤의 토큰

서로 다른 색 값—
대비 미달 짝—
글자 크기 설정—
카드와 바탕 구분—

      
해볼 것: ① "첫 코드 · 밝은 화면"을 본다. 화면에서는 멀쩡해 보이지만 아래 CSS에 초록이 여섯 가지, 회색이 세 가지 적혀 있고, 대비 미달 짝도 벌써 셋(날짜 줄 2.85, 배지 3.79, 안내 띠 4.01)이다. 날짜 줄의 연회색 #999999는 TypeBook 7장에서 버린 값인데 다시 들어와 있다. ② "어두운 화면"을 누른다. 명도 뒤집기로 만든 화면이라 초록 버튼이 가라앉고 카드가 바탕에 녹는다. ③ "서윤의 토큰"으로 바꿔 같은 것을 본다. CSS는 원시 토큰 몇 줄과 의미 토큰 두 벌(밝은·어두운)로 줄고, 대비 미달 짝이 사라진다. ④ 기본 글자 크기를 24px로 올린다. 첫 코드는 그대로이고, 토큰 판은 글자와 버튼이 함께 커진다. (색 값과 대비는 이 책의 엔진으로 계산했다. 첫 코드의 값은 이 장의 이야기를 위해 만든 가상의 코드다.)

토큰이 해 주지 않는 일도 분명히 해 두자. 토큰은 결정을 기록하고 전파할 뿐, 결정을 대신 내려 주지 않는다. 주색이 어떤 초록이어야 하는지는 ColorBook에서, 본문이 몇 픽셀이어야 하는지는 TypeBook에서 따로 고민했다. 토큰은 그 고민의 결과를 잃어버리지 않게 하고, 다음에 바꿀 때 한 곳만 고치게 한다. 그래서 토큰 표에는 값과 함께 왜를 적어 두는 것이 좋다. "주색은 사다리 600, 흰 글자 대비 4.99 때문" 같은 한 줄이, 반년 뒤 누군가 "조금만 밝게"를 말할 때 무엇을 다시 재야 하는지 알려 준다.

토큰 표는 화면이 아니라 표로 검토한다 토큰을 바꿀 때 화면만 보고 판단하면 눈에 띄는 한두 자리만 확인하게 된다. 서윤은 토큰 표를 스프레드시트처럼 펼쳐 두고 검토한다. 열은 토큰 이름, 밝은 화면 값, 어두운 화면 값, 짝이 되는 토큰, 두 화면의 대비, 바꾼 날짜와 이유. 값을 하나 바꾸면 그 줄의 대비가 다시 계산되고, 기준 아래로 떨어진 줄은 바로 보인다. 화면은 표를 통과한 다음에 본다.

서윤의 웹 개편 노트

CASE 서윤웹 개편 노트 · 6장

여섯 개의 초록과 사라진 그림자

2월 첫 주, 행사 일정 페이지의 반응형 코드를 마친 지후가 다크 모드를 넣어 보자고 했다. "운영체제가 어두운 화면이면 따라가게요. 금방 해요." 지후는 짧은 스크립트로 모든 색의 OKLCH 명도를 뒤집었다. 결과는 금방 나왔다. 흰 바탕은 검정이 되었는데, 책방 초록 버튼은 명도 0.53에서 0.47로 거의 그대로라 검은 바탕 위에서 대비가 3.19로 떨어졌다. 서윤이 보기에는 "탁하게 가라앉은" 초록이었다. 흐린 날짜 줄은 3.06, 흰 카드는 검은 바탕과 똑같은 검정이 되어 경계가 사라졌고 카드의 그림자도 함께 묻혔다. 한도경은 휴대폰을 들여다보다가 "밤에 불 끈 방에서 그림을 보는 것 같네. 액자도 벽도 다 까매" 했다.

더 큰 문제는 그다음에 보였다. 지후가 다크 모드용 값을 넣으려고 코드에서 초록을 검색했더니 #2f7d5b 말고도 #2e7d5a, #307c5b, #2f7c5a, #2d7b59, #317e5c가 나왔다. 시안에서 스포이트로 찍은 값, 반올림한 값, 다른 페이지에서 복사한 값이었다. 회색도 셋이었고, TypeBook 여름 노트에서 버린 날짜 줄의 연회색 #999999가 신간 목록에 다시 들어와 있었다. 글자 크기는 17px과 1.0625rem이 섞여, 도경의 휴대폰에서는 같은 본문이 페이지마다 다른 크기로 보였다.

서윤은 ColorBook 가을 노트의 웹 팔레트 v1과 TypeBook 여름 노트를 펼쳐 놓고 토큰 표를 새로 만들었다. 이름 규칙은 두 층이다. 원시 토큰은 색 이름과 단계로 짓는다(--green-50 … --green-900, --neutral-50 … --neutral-900, 그리고 책방의 색 --ink #22342a, --sage #567666, --cream #ffe2d7, --accent #d24f39). 의미 토큰은 --color- 뒤에 쓰임을 적고(bg, surface, text, text-muted, border, action, on-action, band, on-band), 부품은 의미 토큰만 부른다. 컴포넌트 토큰은 버튼처럼 상태가 많은 부품에만 나중에 더하기로 했다. 본문 글자를 ColorBook 표의 중립 900으로 할지 TypeBook 노트의 짙은 색 #22342a로 할지 잠시 망설였는데, 손님들이 이미 서점 소식 글에서 익숙해진 #22342a(바탕 위 12.68)를 택했다. 흐린 글자도 같은 이유로 ColorBook 표의 중립 700 대신 TypeBook의 차분한 초록 #567666(4.83)으로 했다. 대비는 낮아지지만 본문 기준 4.5를 넘는다.

어두운 화면의 값은 따로 지정했다. 바탕은 중립 900 #151917, 카드 표면은 한 단계 밝은 중립 800 #2a2f2d로 띄우고 그림자는 쓰지 않는다. 본문 글자는 중립 100(바탕 위 15.79), 흐린 글자는 중립 400(8.04), 주색은 사다리 400 #5eaa85(6.39)이고, 주색 위 글자는 흰색 대신 중립 900(6.39)이다. 크림 띠는 어두운 화면에서 #402e27이 되고 그 위에는 중립 100만 올린다. 강조색은 밝은 화면에서 바탕 위 4.11이라 큰 글자에만, 어두운 화면에서는 명도를 0.70으로 올린 #ed7761(6.26)을 쓴다. 글자 크기는 모두 rem으로 옮겼다. 작은 글자 0.875rem(14px), 본문은 TypeBook의 clamp 식, 소제목·부제목·제목은 넓은 화면 비율 1.333에서 출발해 반올림한 20 → 24px, 24 → 32px, 그리고 5장의 30 → 40px이다. 3장의 간격 단계도 --space-1(0.25rem)부터 --space-8(4rem)까지 rem으로 옮기되, 휴대폰 화면 가장자리 여백 16px만은 글자를 키운 손님의 화면에서 글 칸을 먹지 않도록 px로 남겼다. 지후가 같은 페이지를 다시 검색하자 hex는 원시 토큰을 적은 파일 하나에만 남았다. 도경은 글자를 가장 크게 한 휴대폰으로 어두운 화면을 열어 보고는 "이번엔 액자가 보이네" 했다.

의미 토큰밝은 화면어두운 화면대비(밝은 · 어두운)
bgneutral-50 #f6fcf8neutral-900 #151917—
surfacewhite #ffffffneutral-800 #2a2f2d—
textink #22342aneutral-100 #edf3f012.68 · 15.79
text-mutedsage #567666neutral-400 #aab0ac4.83 · 8.04
actiongreen-600 #2f7d5bgreen-400 #5eaa854.80 · 6.39
on-actionwhiteneutral-9004.99 · 6.39
band · on-bandcream · ink#402e27 · neutral-10010.74 · 11.40
이 장에서 정한 것토큰 이름 규칙: 원시 토큰은 색 이름 + 단계(--green-600, --neutral-50, 책방 색 --ink --sage --cream --accent), 의미 토큰은 --color- + 쓰임(바탕과 그 위 글자는 x · on-x 짝으로), 부품은 의미 토큰만 부른다. hex는 원시 토큰 파일에만. 다크 모드 값은 반전하지 않고 위 표처럼 따로 지정한다(높이는 그림자 대신 밝은 표면으로). 강조색 #d24f39는 밝은 화면에서 큰 글자와 장식에만, 어두운 화면에서는 #ed7761. 글자 크기는 모두 rem: --text-sm 0.875rem, --text-body clamp(1.0625rem, 1.038rem + 0.1087vw, 1.125rem), --text-h3 20 → 24px, --text-h2 24 → 32px, --text-h1 30 → 40px(5장 식). 간격 단계는 rem, 화면 가장자리 여백 16px만 px. 중단점은 5장대로 em.
아직 남은 문제토큰으로 칠한 "신청하기" 버튼은 마우스를 올리면 진해지지만, 키보드로 옮겨 가면 어디에 있는지 표시가 없다. 휴대폰에서는 행사 날짜를 고르는 작은 칩이 자꾸 잘못 눌린다.
다음 단계7장: 버튼과 칩의 상태와 크기를 정한다.

핵심 정리

  1. 디자인 토큰은 이름이 붙은 디자인 결정 하나다. 값은 한 곳에만 적고 쓰는 곳은 이름을 부른다. 웹에서는 CSS 사용자 정의 속성(--이름, var())이 가장 단순한 그릇이다.
  2. 원시 토큰은 "어떤 값이 있는가"(색 이름 + 단계), 의미 토큰은 "무엇에 쓰는가"(바탕, 글자, 주된 행동), 컴포넌트 토큰은 "이 부품의 이 자리"에 답한다. 부품은 원시 토큰을 직접 부르지 않는다.
  3. 바탕과 그 위의 글자는 x · on-x처럼 짝으로 이름 짓는다. 그러면 대비 검사가 유한한 짝의 표가 되고, 기준 미달 짝은 아예 쓸 토큰이 없게 만들 수 있다.
  4. 다크 모드는 반전이 아니다. 명도 뒤집기는 가운데 명도의 브랜드 색을 가라앉히고 표면의 구분을 없애며, 반전 필터는 사진과 그림자를 뒤집는다. 어두운 화면의 의미 토큰은 따로 고른다.
  5. 어두운 화면에서는 순수한 검정 대신 아주 어두운 회색을, 주색은 사다리의 더 밝은 칸을, 높이는 그림자 대신 더 밝은 표면으로 말한다. 대비 검사는 밝은 화면과 똑같이 한다.
  6. 글자 크기를 px로 적으면 브라우저의 기본 글자 크기 설정이 무시된다. 글자는 rem, 글자를 담는 상자는 고정 높이 대신 rem 여백, 중단점은 em으로 적으면 글자를 키운 사람에게 "더 좁은 화면의 모양"이 간다.
  7. 글자 크기 단계는 본문에 비율을 거듭 곱해 출발하고, 좁은 화면에서는 비율을 낮춘다. 최종 값은 반올림해 rem 토큰으로 적고, 실제 글과 화면에서 확인한다.

확인 퀴즈

1. 버튼이 --green-600을 직접 부르던 코드를, 버튼은 --color-action을 부르고 --color-action이 --green-600을 가리키게 바꿨다. 이렇게 한 층을 더 둔 가장 큰 이득은?

의미 토큰은 "무엇에 쓰는가"의 이름이라, 화면 모드나 브랜드 결정이 바뀔 때 화살표만 옮기면 된다. 부품 쪽 코드는 그대로다. 변수 한 층은 용량이나 속도와 거의 상관이 없고, 브라우저는 변수 이름의 뜻을 모른다. 실제 값은 여전히 원시 토큰에 있어야 한다.

2. 모든 색의 OKLCH 명도를 L → 1 − L로 뒤집어 다크 모드를 만들었더니, 책방 초록(L 0.53) 버튼이 검은 바탕에서 탁해 보였다. 가장 알맞은 설명과 처방은?

명도 뒤집기는 아주 밝은 색과 아주 어두운 색만 크게 바꾸고, 가운데 명도의 색은 거의 그대로 둔다. 그런데 어두운 바탕 위에서는 같은 색도 더 어둡고 무겁게 보이므로, 오히려 더 밝은 칸이 필요하다. 이 장의 값으로 사다리 400번은 어두운 바탕 위 6.39다. 채도를 없애면 브랜드가 사라지고, 바탕을 흰색으로 돌리면 다크 모드가 아니다.

3. 브라우저의 기본 글자 크기를 24px로 올린 사용자가 본문을 font-size: 1.0625rem으로 적은 페이지를 열었다. 본문 글자의 크기는?

rem은 루트(html)의 글자 크기를 기준으로 하고, 디자이너가 루트를 덮어쓰지 않았다면 그 값은 사용자의 기본 글자 크기다. 24 × 1.0625 = 25.5px. 같은 글자를 17px로 적었다면 설정과 상관없이 17px에 머문다. 기본 글자 크기 설정은 화면을 확대하는 기능(Ctrl +)과 다르다.

4. 본문 17px에 비율 1.333을 그대로 써서 h1을 본문의 세 단계 위(17 × 1.333³)로 두었더니, 360px 휴대폰에서 "2월 행사와 모임 일정"이 두 줄로 꺾였다. 이 장에서 다룬 방법으로 가장 알맞은 것은?

17 × 1.333³ ≈ 40px이면 360px 화면의 글 칸(양옆 여백 16px을 뺀 328px)에 열 자 남짓한 제목도 한 줄로 들어가지 않는다. 좁은 화면은 비율을 낮춰 1.2³배, 약 29px로 두고, 넓은 화면의 값과 clamp()로 이으면 사이 화면에서 비율이 서서히 바뀐다. vw만 쓰면 확대가 듣지 않고(5장), 본문을 줄이면 읽기가 나빠지며, 자간을 좁히는 것은 한글 제목을 답답하게 할 뿐 근본 처방이 아니다.

출처