Chapter 07

컴포넌트와 상태

엘리베이터 버튼을 눌렀는데 불이 들어오지 않으면 사람들은 한 번 더, 또 한 번 더 누른다. 버튼이 "알아들었다"고 대답하지 않았기 때문이다. 웹 화면의 버튼도 같다. 마우스를 올렸을 때, 키보드로 다가왔을 때, 누르는 순간, 누를 수 없을 때, 누른 뒤 처리하는 동안, 화면은 매번 다르게 대답해야 한다. 그리고 그 버튼은 손가락 끝으로도 눌린다. 손가락은 마우스 포인터보다 훨씬 굵고, 걸으면서 누르면 더 흔들린다. 이 장은 웹 화면의 부품들이 상태로 말하는 법과, 손과 포인터가 실제로 닿을 수 있는 크기를 다룬다.

화면의 부품

웹 페이지는 수많은 작은 부품으로 이루어진다. 버튼, 링크, 입력 칸, 체크 상자, 카드, 탭, 알림 띠. 이렇게 한 번 설계해 두고 여러 화면에서 되풀이해 쓰는 화면의 부품을 컴포넌트(Component)라고 한다. 같은 버튼을 행사 페이지에서도, 신청 폼에서도, 신간 목록에서도 쓰므로, 버튼 하나를 잘 만들면 사이트 전체가 좋아지고 하나를 잘못 만들면 그 실수가 사이트 전체에 퍼진다. 6장에서 색과 글자를 토큰으로 정리한 것은 이 부품들을 만들 재료를 마련한 일이었다. 이 장은 그 재료로 부품을 조립한다.

부품을 고를 때 가장 먼저 물을 것은 생김새가 아니라 하는 일이다. 웹에서 가장 흔히 헷갈리는 두 부품이 링크와 버튼이다. 링크(Link)는 다른 곳으로 데려간다. 다른 페이지, 같은 페이지의 다른 자리, 내려받을 파일. 버튼(Button)은 지금 이 자리에서 무언가를 한다. 신청서를 보내고, 메뉴를 열고, 장바구니에 담는다. 생김새는 바꿀 수 있다. 버튼처럼 생긴 링크("행사 전체 보기")도, 링크처럼 생긴 버튼("더 불러오기")도 있다. 하지만 HTML에서는 하는 일에 맞는 요소를 써야 한다. 링크는 <a href>, 버튼은 <button>이다.

모임 신청하기 안쪽 여백 글자 폭이 정한다 높이: 최소 44 글자 따라 자람 글자 둘레 위아래 여백 포커스 링: 3 두께, 2 띄움 모서리 반지름 8~10 단위는 CSS px. 크기를 정하는 것은 고정값이 아니라 글자 + 여백 + 최소값이다.
그림 1. 버튼 하나의 해부. 버튼의 크기는 디자이너가 그린 사각형이 아니라 글자, 안쪽 여백(3장의 패딩), 최소 높이가 함께 정한다. 바깥의 점선은 키보드로 다가왔을 때만 나타나는 포커스 링이다. 버튼에서 조금 띄워 두면 버튼 색과 상관없이 바탕 위에서 보인다.
부품하는 일HTML 요소흔한 실수
링크다른 곳으로 데려간다<a href="…">"여기를 클릭"처럼 어디로 가는지 모르는 문구(8장)
버튼이 자리에서 무언가를 한다<button><div>에 누르기만 붙여 키보드로 닿지 않는다
입력 칸사람에게서 값을 받는다<input>, <textarea>, <label>라벨 없이 자리표시자만 둔다(9장)
카드한 대상(행사, 책)의 요약을 묶는다<article> 안의 제목 링크카드 안의 모든 글자를 링크로 감싸 화면 낭독기가 긴 문장을 통째로 읽는다

<div>에 누르기 동작만 붙인 "가짜 버튼"은 마우스로는 잘 눌린다. 그래서 디자이너도 개발자도 문제를 모르고 지나간다. 그런데 키보드의 Tab 키로는 그 자리에 갈 수 없고, Enter나 스페이스로 누를 수도 없으며, 화면 낭독기는 그것이 버튼인 줄 모른다. 진짜 <button>은 이 모든 것을 브라우저가 공짜로 해 준다. 웹 표준은 이것을 부품의 이름, 역할, 값이 기계에 전달되어야 한다는 요구로 적어 둔다(WCAG 2.2 성공 기준 4.1.2, 이름·역할·값). 접근성 전체는 11장에서 다루고, 이 장에서는 부품이 사람에게 대답하는 방식, 곧 상태에 집중한다.

상태: 부품이 대답하는 방식

같은 버튼도 때에 따라 다른 얼굴을 한다. 부품이 지금 어떤 처지에 있는지를 상태(State)라고 한다. 버튼의 상태는 대개 여섯 가지로 정리한다. 아무 일도 없을 때의 기본, 마우스 포인터를 올렸을 때의 호버(Hover), 키보드로 다가와 지금 입력을 받을 차례가 되었을 때의 포커스(Focus), 누르고 있는 순간의 눌림(Active, Pressed), 지금은 누를 수 없을 때의 비활성(Disabled), 누른 뒤 처리를 기다리는 로딩(Loading).

상태언제CSS에서사람에게 알려야 할 것
기본아무 일도 없을 때기본 규칙"나는 누를 수 있다"(버튼다운 모양)
호버마우스 포인터가 위에 있을 때:hover"지금 누르면 내가 눌린다"
포커스Tab 키 등으로 차례가 왔을 때:focus-visible"키보드 입력은 지금 여기로 온다"
눌림누르고 있는 동안:active"알아들었다"
비활성조건이 맞지 않아 누를 수 없을 때:disabled, [aria-disabled="true"]"지금은 안 된다, 그리고 그 이유"
로딩누른 뒤 서버의 대답을 기다릴 때클래스, [aria-busy="true"]"처리하는 중이다, 다시 누르지 않아도 된다"

디자인 시안이 기본 상태 하나만 그려져 있으면, 나머지 다섯 상태는 개발자가 짐작으로 채우거나 브라우저의 기본값으로 남는다. 아래 시뮬레이터의 위쪽은 버튼 하나의 여섯 상태를 나란히 놓은 상태 표(State matrix)다. "첫 시안"은 호버 색만 정해 둔 버튼, "다섯 상태 + 로딩"은 상태마다 다른 단서를 준 버튼이다. 아래쪽에는 같은 두 설계로 만든 진짜 버튼이 있다. 마우스를 올려 보고, 페이지를 누른 다음 Tab 키로 다가가 보고, 눌러 본다.

SIMULATOR

버튼의 상태 표

그림책 함께 읽기2월 5일 토 오전 11시 · 남은 자리 4
진짜 버튼의 지금 상태: 기본
기본과 구별되지 않는 상태—
색으로만 구별되는 상태—
진짜 버튼으로 접수된 신청—
해볼 것: ① 첫 시안에서 상태 표를 본다. 포커스·비활성·로딩이 기본과 똑같다. 진짜 버튼에 Tab 키로 다가가면 상태 글자는 "포커스"로 바뀌는데 버튼에는 아무 표시가 없다. ② 첫 시안의 진짜 버튼을 빠르게 세 번 누른다. 버튼이 대답하지 않으니 신청이 세 건 접수된다. "다섯 상태 + 로딩"으로 바꿔 다시 누르면 첫 번째 누름 뒤 "신청 중…"이 되고 나머지 누름은 무시된다. ③ "진짜 버튼을 비활성으로"를 누른다. 첫 시안에서는 겉모습이 그대로라 눌러도 아무 일이 없는 이유를 알 수 없다. ④ 제2색각으로 보기를 켠다. 호버와 눌림처럼 색으로만 다른 상태는 차이가 줄지만, 링·내려앉음·글자 같은 단서는 남는다. (구별 판정은 이 장의 단순 규칙이다: 바탕색의 ΔE2000이 5 이상이면 색 단서, 링·위치·글자·아이콘이 다르면 모양 단서로 센다. 색 값은 책방 E의 초록에서 OKLCH 명도만 낮춘 것이다.)

첫 시안의 문제는 색이 아니라 침묵이다. 포커스가 왔는데 말하지 않고, 누를 수 없는데 말하지 않고, 처리 중인데 말하지 않는다. 사람은 대답이 없으면 다시 누른다. 그래서 로딩 상태가 없는 신청 버튼은 같은 신청을 두세 번 보내게 만든다. 서버에서 중복을 막는 일은 개발자의 몫이지만, "처리 중"이라고 말하는 일은 디자인의 몫이다.

개선안이 상태마다 쓴 단서를 다시 보자. 호버는 색만 조금 어둡게 했다. 호버는 포인터가 바로 그 자리에 있을 때만 생기므로 작은 변화로 충분하다. 포커스는 색이 아니라 링이다. 눌림은 더 어두운 색에 더해 버튼이 2 px 내려앉고 아래 그림자가 사라진다. 비활성은 회색으로 바뀌면서 글자도 "신청 마감"으로 바뀐다. 로딩은 돌아가는 표시와 "신청 중…"이다. 상태를 색 하나로만 알리면 색을 구별하기 어려운 사람(ColorBook 13장), 햇빛 아래의 휴대폰 화면, 흑백으로 보는 사람에게는 상태가 사라진다(WCAG 2.2 성공 기준 1.4.1, 색의 사용).

비활성 버튼은 이유를 삼킨다 제출 버튼을 미리 비활성으로 두고 "칸을 다 채우면 켜진다"는 방식은 흔하지만, 왜 눌리지 않는지 알려 주지 않으면 사람은 막힌 채로 화면을 뒤진다. 게다가 HTML의 disabled 속성을 단 버튼은 키보드 포커스도 받지 않아서, 화면 낭독기 사용자는 버튼이 있다는 것조차 모를 수 있다. WCAG는 비활성 부품의 글자를 대비 요구에서 빼 주지만(성공 기준 1.4.3의 예외), 그렇다고 읽을 수 없게 흐리게 만들라는 뜻은 아니다. 대안은 둘이다. 버튼은 늘 눌리게 두고 누르면 무엇이 빠졌는지 알려 주거나(9장), 비활성으로 둔다면 바로 옆에 이유를 글자로 적는다("모집이 끝났습니다"). 이 장의 개선안이 비활성 버튼의 글자를 "신청 마감"으로 바꾼 것도 그 때문이다.
시안에 상태 표를 붙여 넘긴다 개발자에게 넘기는 시안에는 화면 그림과 함께 부품마다 상태 표를 붙이는 디자이너가 많다. 가로로 상태(기본·호버·포커스·눌림·비활성·로딩), 세로로 변형(주 버튼·보조 버튼·글자 버튼, 큰 것·작은 것)을 놓고 칸마다 색 토큰 이름과 바뀌는 속성을 적는다. 빈칸이 보이면 그 상태를 아직 설계하지 않았다는 뜻이다. 표를 만들다 보면 "로딩 중에 버튼 폭이 줄어들면 옆의 글이 출렁이지 않을까" 같은 질문이 저절로 나온다. 그런 질문이 개발 중에 나오면 비싸고, 시안 단계에서 나오면 싸다.

포커스: 키보드의 커서

모든 사람이 마우스나 손가락으로 웹을 쓰지는 않는다. 손을 정밀하게 움직이기 어려운 사람, 화면을 보지 않고 화면 낭독기로 쓰는 사람, 빠른 입력을 좋아하는 사람은 키보드로 페이지를 돌아다닌다. Tab 키를 누르면 다음 링크나 버튼으로, Shift+Tab을 누르면 이전으로 간다. 이때 지금 입력을 받을 부품이 어디인지 보여 주는 표시가 포커스 표시(Focus indicator)다. 글을 쓸 때 깜빡이는 커서와 같은 일을 한다. 커서가 보이지 않는 문서 편집기를 상상해 보면 포커스 표시가 없는 웹 페이지가 어떤지 알 수 있다.

브라우저는 포커스 받은 부품에 기본 링을 그려 준다. 그런데 이 링이 시안과 어울리지 않는다며 CSS 한 줄 outline: none으로 지워 버리는 일이 아주 흔하다. 마우스로 누를 때도 링이 생겨 보기 싫다는 이유다. 그 문제는 이제 :focus-visible이라는 선택자로 풀 수 있다. 브라우저가 "이 포커스는 키보드로 온 것이라 표시가 필요하다"고 판단할 때만 해당하는 상태라, 마우스로 누를 때는 링이 생기지 않고 키보드로 올 때만 생긴다.

.btn:focus-visible {
  outline: 3px solid #22342a;   /* 짙은 색 링 */
  outline-offset: 2px;          /* 버튼에서 2px 띄운다 */
}

포커스 표시에 관한 WCAG 2.2의 성공 기준은 여럿이다. 2.4.7(포커스 보임, AA)은 키보드로 조작할 수 있는 화면에서 포커스 표시가 보여야 한다고 요구한다. 1.4.11(텍스트가 아닌 콘텐츠의 대비, AA)은 부품의 상태를 알아보는 데 필요한 시각 정보가 이웃한 색과 3:1 이상 대비되어야 한다고 요구하고, 포커스 링도 여기에 든다. 2.2에서 새로 생긴 2.4.11(포커스 가려지지 않음 최소, AA)은 포커스 받은 부품이 고정된 머리 띠나 쿠키 알림에 완전히 가려지지 않아야 한다고 하고, 2.4.13(포커스 모양, AAA)은 링의 면적이 부품 둘레에 2 CSS px 두께로 두른 것 이상이고 포커스 전후의 같은 화소가 3:1 이상 달라져야 한다고 더 엄격하게 정한다.

포커스 링이 어려운 이유는 버튼이 늘 같은 바탕 위에 있지 않기 때문이다. 흰 본문 위의 버튼, 크림색 알림 띠 위의 버튼, 짙은 바닥글 위의 버튼에 같은 링을 쓰면 어딘가에서는 링이 바탕에 녹는다. 아래에서 버튼을 끌어 바탕을 바꿔 가며 링 스타일을 비교한다.

SIMULATOR

어떤 바탕에서도 보이는 포커스 링

버튼을 끈다
링과 바탕의 대비(가장 잘 보이는 켜)—
버튼과 바탕의 대비—
판정—
해볼 것: ① "링 없음"으로 바꾼다. 어느 바탕에서도 포커스가 어디 있는지 알 수 없다. ② "짙은 3px"로 두고 버튼을 흰 본문에서 짙은 바닥글까지 끈다. 흰 바탕과 크림 띠에서는 선명하지만 짙은 바닥글에서는 대비가 1:1이 되어 링이 사라진다. ③ "어두운 바탕에서는 크림색 링으로"를 켜고 다시 끈다. ④ "이중 링"을 고른다. 흰 안쪽 켜와 짙은 바깥 켜 중 하나는 어느 바탕에서나 3:1을 넘는다. ⑤ 버튼을 "책방 초록" 띠로 옮긴다. 링과 별개로 버튼 자체가 바탕에 녹는다(대비 1:1). (대비는 WCAG 2 대비비다. 링이 버튼에서 떨어져 있으므로 링 화소는 포커스 전에 바탕색이었다고 보고 링 색과 바탕색을 비교한다. 판정은 1.4.11의 3:1과 2.4.13의 두께 2px를 함께 본 이 장의 단순 판정이다.)

바탕이 하나뿐인 사이트라면 짙은 링 하나로 충분하다. 바탕이 여러 가지라면 두 가지 방법이 있다. 바탕마다 링 색을 바꾸는 규칙을 정하거나(어두운 영역 안에서는 밝은 링), 서로 대비가 큰 두 색을 겹친 이중 링을 쓴다. 이중 링은 규칙이 하나라 개발자가 실수할 여지가 적고, 바탕 위에 사진이 깔린 곳처럼 바탕색을 미리 알 수 없는 자리에서도 버틴다. 마지막 단계에서 본 것처럼, 링을 아무리 잘 만들어도 버튼 자체가 바탕에 녹으면 소용없다. 책방 초록 띠 위에는 크림색 버튼을 놓는 식으로 부품의 변형을 따로 둔다.

:focus-visible은 언제 켜지는가 CSS 선택자 표준(Selectors Level 4)은 :focus-visible을 "브라우저가 포커스를 눈에 띄게 표시해야 한다고 판단할 때"로만 정하고, 판단 방법은 브라우저에 맡긴다. 주요 브라우저는 대체로 키보드로 포커스를 옮겼을 때 켜고, 마우스로 버튼을 눌렀을 때는 켜지 않는다. 다만 글자를 입력하는 칸은 마우스로 눌러도 켜지는 경우가 많다. 곧 입력할 자리이기 때문이다. 판단 규칙이 브라우저마다 조금씩 다를 수 있으므로 결과는 실제 브라우저에서 키보드로 확인한다. 포커스 표시를 지우는 대신 :focus-visible로 바꾸는 것만으로도 "마우스로 누르면 링이 생겨 싫다"는 불만과 접근성을 함께 해결할 수 있다.

크기는 내용이 정한다

그래픽 디자인 도구에서 버튼을 그리면 사각형부터 그리게 된다. 가로 120, 세로 36. 그 안에 글자를 넣는다. 화면이 이 그림대로만 보인다면 문제가 없다. 하지만 2장에서 본 것처럼 사람들은 휴대폰과 브라우저의 글자 크기 설정을 바꾼다. 한도경처럼 글자를 가장 크게 해 두고 쓰는 사람에게 120×36 상자 안의 "모임 신청하기"는 상자 밖으로 넘치거나 잘린다. 번역된 페이지에서 글자 수가 늘어날 때도 같은 일이 생긴다.

그래서 웹의 부품은 크기를 고정하지 않고 내용 + 안쪽 여백 + 최소값으로 정한다. 버튼의 폭은 글자 폭에 좌우 여백을 더한 만큼, 높이는 글줄 높이에 위아래 여백을 더한 만큼이되 손가락으로 누르기에 모자라지 않게 최소 높이를 둔다. 글자 크기는 6장에서 정한 대로 rem으로 적어 사용자 설정을 따른다. 아래에서 휴대폰 화면(폭 360)의 행사 카드에 들어간 두 버튼을 두 방식으로 짜고 글자 크기를 키워 본다.

SIMULATOR

글자가 커지면 버튼은?

주 버튼 크기—
글자—
누르는 면—
해볼 것: ① 고정 크기에서 글자 크기를 130%로 올린다. "모임 신청하기"의 끝이 상자 밖에서 잘린다. ② 200%까지 올린다. 글자가 위아래로도 잘려 무슨 버튼인지 읽을 수 없다. ③ "안쪽 여백 + 최소 44"로 바꾸고 다시 200%까지 올린다. 버튼이 글자를 따라 자라고, 두 버튼이 한 줄에 들어가지 않으면 위아래로 쌓인다. ④ 100%로 돌아가 두 방식의 높이를 비교한다. 고정 크기 36은 이 장이 정한 누르는 면 44에 모자란다. (본문 글자 17 px, 버튼 글자 16 px을 기준으로 설정 비율만큼 키운다. 글줄 높이는 1.25, 여백은 위아래 10·좌우 20 px로 둔 이 장의 예시 값이다. 실제 브라우저의 글자 크기 설정이 반영되는 방식은 기기와 설정 메뉴에 따라 다르다.)

안쪽 여백으로 짠 버튼은 글자가 두 배가 되어도 읽힌다. 대신 화면을 더 차지하고, 줄이 바뀌고, 옆의 부품을 밀어낸다. 이것은 실패가 아니라 웹이 원래 그렇게 움직이는 방식이다. 디자이너의 일은 "글자가 커졌을 때 무엇이 어디로 가는가"까지 정해 두는 것이다. 두 버튼이 한 줄에 들어가지 않으면 주 버튼을 위로 쌓는다, 버튼 글자는 두 줄까지 허용한다, 같은 규칙들이다. 3장에서 정한 간격 단계와 "안쪽 간격 < 바깥 간격" 규칙이 여기서 그대로 쓰인다.

가장 긴 글자로 시안을 그린다 버튼 시안에는 흔히 "확인"처럼 짧은 글자가 들어간다. 실제 화면에는 "모임 신청 내용 확인하기"처럼 긴 글자가 들어오고, 다른 언어로 옮기면 더 길어진다. 시안을 그릴 때 가장 긴 글자, 가장 큰 글자 크기, 가장 좁은 화면을 한 번씩 넣어 보는 습관이 있으면 부품이 깨지는 자리를 미리 찾는다. 도구에서 버튼을 "자동 크기"(내용에 맞춰 늘어나는 상자)로 만들어 두면 이 시험이 쉬워진다.

손가락이 닿는 크기

마우스 포인터의 끝은 화소 하나다. 손가락은 그렇지 않다. 화면에 닿는 손가락 끝의 면적은 넓고, 사람은 자기가 누른 자리를 정확히 알지 못하며, 손가락 끝이 누르려는 곳을 가린다. 버스 안이나 걸으면서 한 손 엄지로 누르면 더 흔들린다. 그래서 같은 자리를 노리고 여러 번 눌러도 닿는 자리는 노린 점 둘레에 흩어진다. 손가락이나 포인터로 누를 수 있는 부품의 면을 터치 타깃(Touch target, 누르는 면)이라고 한다. 눈에 보이는 모양보다 누르는 면을 더 크게 잡을 수도 있다.

누르는 면의 크기에 관해서는 성격이 다른 기준 몇 가지가 함께 돌아다닌다. 하나로 섞어 쓰면 안 된다.

기준값성격
WCAG 2.2 성공 기준 2.5.8 타깃 크기(최소), AA24×24 CSS px접근성 표준의 하한. 더 작으면 둘레에 충분한 간격이 있어야 한다(아래)
WCAG 2.2 성공 기준 2.5.5 타깃 크기(강화), AAA44×44 CSS px더 엄격한 등급의 권고
Apple Human Interface Guidelines44×44 ptiOS 앱 설계 지침의 최소 누르는 면
Material Design(구글)48×48 dp안드로이드 앱 설계 지침. 지침은 이것이 실제 크기로 약 9 mm라고 설명한다

2.5.8의 24는 "이보다 작으면 많은 사람이 누르기 어렵다"는 최소선이지 목표가 아니다. 이 기준에는 예외가 있다. 누르는 면이 24보다 작아도, 그 면의 가운데에 지름 24 CSS px의 원을 그렸을 때 그 원이 다른 타깃이나 다른 작은 타깃의 원과 겹치지 않으면 통과한다. 크기가 모자라면 간격으로 메우라는 뜻이다. 문장 안의 링크처럼 글줄에 섞인 타깃도 예외다. 44와 48은 앱 플랫폼의 설계 지침에서 왔고, 단위(pt, dp)가 CSS px와 같은 개념은 아니지만 휴대폰에서는 대체로 비슷한 크기가 된다.

아래 시뮬레이터는 휴대폰의 행사 날짜 줄이다. 칩 하나를 노리고 손가락으로 여러 번 누른 자리를 점으로 찍는다. 노린 칩에 닿으면 초록, 옆 칩에 닿으면 빨강, 아무것도 없는 틈이나 줄 밖에 닿으면 회색이다. 칩을 눌러 노릴 칩을 바꿀 수 있다.

SIMULATOR

손가락은 흩어져 닿는다

칩을 눌러 노린다
노린 칩에 닿음—
옆 칩을 누름(잘못 누르기)—
빈 곳(반응 없음)—
기준—
해볼 것: ① 처음 값(칩 28, 간격 4, 앉아서)에서 빨간 점이 얼마나 되는지 본다. ② "걸으면서 한 손"으로 바꾼다. 같은 칩인데 옆 칩을 누르는 비율이 몇 배로 는다. ③ 칩을 44로 키운다. 잘못 누르기가 크게 준다. ④ 칩을 20으로 줄이고 간격을 0에서 4까지 올려 본다. 칩이 24보다 작으면 24 원이 그려지고, 원이 서로 겹치는 동안은 2.5.8을 통과하지 못한다. 간격을 넓히면 통과하지만, 닿지 않는 회색 점이 늘어난다. ⑤ 칩 크기를 고정하고 간격만 바꿔 본다. 간격은 잘못 누르기를 "반응 없음"으로 바꿔 준다. 옆 날짜를 신청하는 것보다 아무 일도 안 일어나는 편이 낫다. (모형의 가정: 누른 자리는 노린 칩의 가운데를 중심으로 가로세로 독립인 정규 분포로 흩어지고, 표준편차는 앉아서 1.2 mm, 걸으면서 2.0 mm로 두었다. 휴대폰에서 1 CSS px ≈ 0.16 mm로 환산했다. 모두 교육용 가정이며 실제 흩어짐은 사람, 손가락, 쥐는 법, 기기에 따라 다르다. 비율은 같은 분포로 계산한 값이고 점은 그중 160번을 뽑아 찍은 것이다.)

잘못 누르기를 정하는 것은 칩 크기 하나가 아니라 이웃한 칩까지의 거리다. 칩의 가운데에서 옆 칩의 가장자리까지가 멀수록 흩어진 손가락이 옆 칩에 닿기 어렵다. 그 거리는 칩 크기의 절반에 간격을 더한 것이다. 그래서 칩을 키우는 것과 간격을 넓히는 것은 둘 다 잘못 누르기를 줄인다. 다른 점은, 칩을 키우면 노린 칩에 닿는 비율도 오르지만 간격을 넓히면 그 몫이 "반응 없음"으로 간다는 것이다. 화면 폭에 여유가 있으면 칩을 키우고, 폭이 모자라면 적어도 간격을 둔다.

종이 위의 손가락 도장 종이에 지름 4 mm, 7 mm, 10 mm의 작은 원을 몇 개씩 그린다(7 mm는 휴대폰에서 대략 44 CSS px 안팎이다). 집게손가락 끝에 연필 가루나 스탬프 잉크를 살짝 묻히고, 원 하나를 노려 스무 번 찍는다. 앉아서 한 번, 종이를 든 채 걸으면서 한 번. 찍힌 자리의 가운데가 원 안에 들어간 횟수를 세어 본다. 원이 작을수록, 걸을수록 원 밖으로 나간 자국이 많아질 것이다. 시뮬레이터의 흩어짐 값은 가정이지만, 손가락이 생각보다 덜 정확하다는 것은 이 실험만으로도 몸으로 알 수 있다.
손가락은 자기가 누른 곳을 모른다 터치 정확도에 관한 연구들은 사람이 "여기를 눌렀다"고 느끼는 점과 기기가 기록한 접촉점이 체계적으로 어긋난다는 것을 보여 준다. 예를 들어 Holz와 Baudisch의 연구(「The Generalized Perceived Input Point Model and How to Double Touch Accuracy by Extracting Fingerprints」, CHI 2010)는 손가락의 각도와 사람마다 다른 습관이 이 어긋남을 만든다고 보고했다. 운영체제는 이런 어긋남을 어느 정도 보정하지만, 디자이너가 할 수 있는 가장 확실한 보정은 누르는 면을 넉넉하게 하는 것이다. 눈에 보이는 아이콘은 작게 두고 누르는 면만 넓히는 방법(안쪽 여백이나 투명한 테두리)도 자주 쓴다.

피츠의 법칙: 멀고 작으면 느리다

심리학자 폴 피츠(Paul M. Fitts)는 1954년 논문(「The information capacity of the human motor system in controlling the amplitude of movement」, Journal of Experimental Psychology)에서 사람이 손을 뻗어 목표를 짚는 데 걸리는 시간을 다뤘다. 목표가 멀수록, 목표가 작을수록 시간이 더 걸린다는 것, 그리고 그 관계가 거리와 크기의 비의 로그로 잘 정리된다는 것이다. 이것을 피츠의 법칙(Fitts's law)이라고 한다. 오늘날 화면 설계에서 가장 많이 쓰는 형태는 스콧 매켄지(I. Scott MacKenzie)가 1992년 논문(「Fitts' law as a research and design tool in human-computer interaction」, Human–Computer Interaction)에서 정리한 섀넌 형식이다.

$$MT = a + b \cdot ID, \qquad ID = \log_2\!\left(\frac{D}{W} + 1\right)$$
\(D\): 출발점에서 목표 가운데까지의 거리, \(W\): 움직이는 방향으로 잰 목표의 폭, \(ID\): 어려움의 지수(비트), \(MT\): 움직이는 데 걸리는 시간. \(a\)와 \(b\)는 사람과 기기와 실험 조건마다 실험으로 구하는 상수다. \(D\)와 \(W\)를 함께 두 배로 하면 \(ID\)는 그대로다.

식에서 가장 중요한 것은 시간이 거리나 크기 하나가 아니라 둘의 비에 달려 있다는 점이다. 멀리 있는 목표도 크면 쉽고, 가까운 목표도 아주 작으면 어렵다. 그리고 로그 때문에 이미 큰 목표를 더 키우는 효과는 점점 줄어든다. 아래에서 목표를 끌어 거리를 바꾸고, 폭을 바꿔 그래프 위의 점이 어떻게 움직이는지 본다.

SIMULATOR

거리와 폭의 비

목표를 좌우로 끈다
거리 D—
어려움 ID—
예상 시간 MT—
해볼 것: ① 폭을 8로 줄인다. 점이 그래프의 오른쪽 위로 간다. ② 폭 44에서 목표를 가까이 끌었다가 멀리 끈다. 거리가 두 배가 되어도 시간은 두 배가 되지 않고 조금씩 는다(로그). ③ 폭을 44에서 88로 늘리고 목표를 두 배 멀리 끈다. ID가 거의 그대로다. ④ 그래프의 작은 점 "24"와 "44"를 본다. 지금 거리에서 폭이 24일 때와 44일 때의 차이다. 폭이 커질수록 같은 폭을 더 늘려 얻는 시간은 줄어든다. (a, b는 교육용 가정이다: 마우스 a = 0.10 s, b = 0.15 s/비트, 손가락 a = 0.20 s, b = 0.10 s/비트. 실제 상수는 실험마다 다르므로 시간의 절대값보다 기울기와 모양을 본다. 손가락은 아주 작은 목표에서 이 식보다 더 느려지거나 아예 맞히지 못할 수 있다.)

피츠의 법칙이 화면 설계에 주는 처방은 단순하다. 자주 누르는 것은 크게, 가깝게. "신청하기"처럼 그 페이지의 목적인 버튼은 크게 만들고, 사람의 손이나 포인터가 이미 있는 자리 가까이 둔다. 반대로 "신청 취소"처럼 실수로 누르면 곤란한 것은 일부러 작게, 멀리 두기도 한다. 피츠의 법칙을 거꾸로 쓰는 것이다. 마우스를 쓰는 넓은 화면에서는 화면 가장자리도 특별하다. 포인터가 화면 끝에서 멈추기 때문에 가장자리에 붙은 목표는 움직이는 방향으로 폭이 끝없이 넓은 것과 같다. 운영체제들이 메뉴 막대나 작업 표시줄을 화면 끝에 두는 이유로 자주 설명된다. 손가락에는 이 효과가 없다. 화면 끝을 넘어 손가락이 멈추지는 않기 때문이다.

손가락에 맞춘 피츠의 법칙 원래 법칙은 손으로 막대를 짚는 실험과 마우스 같은 포인터에서 잘 맞았다. 손가락 터치에서는 목표가 아주 작아질 때 손가락 자체의 부정확성 때문에 예측이 어긋난다. Bi, Li, Zhai의 「FFitts law: modeling finger touch with Fitts' law」(CHI 2013)는 손가락이 원래 가진 흩어짐을 식에 따로 넣어 이 문제를 다룬 연구다. 앞 절의 시뮬레이터가 보여 준 흩어짐이 바로 그것이다. 결론은 같다. 손가락으로 누르는 화면에서는 작은 목표의 대가가 마우스보다 더 크다.

호버가 없는 기기

호버는 마우스의 상태다. 손가락에는 "올려 두기"가 없다. 화면에 닿는 순간이 곧 누르는 순간이다. 그런데 넓은 화면에서 설계를 시작하면 정보를 호버 뒤에 숨기기 쉽다. 행사 카드에 포인터를 올리면 "남은 자리"와 "신청하기"가 나타나는 식이다. 화면이 깔끔해 보이기 때문이다. 이 정보는 휴대폰에서 어떻게 될까. 브라우저마다 다르다. 어떤 브라우저는 첫 탭을 호버로 처리해 숨은 정보를 보여 주고 두 번째 탭에서야 누른 것으로 친다. 어떤 브라우저는 첫 탭에 곧바로 카드의 링크로 넘어가 숨은 정보를 볼 기회가 없다. 키보드 사용자에게는 :hover만으로 숨긴 정보가 아예 나타나지 않는다.

SIMULATOR

호버 뒤에 숨긴 정보

카드 위에 올리거나 누른다
보이는 "남은 자리"—
신청까지—
방금 일어난 일—
해볼 것: ① 마우스에서 카드 위에 포인터를 올린다. 남은 자리와 신청 버튼이 나타난다. ② 손가락으로 바꾸고 카드를 누른다. 첫 탭은 호버로 처리되어 정보가 나타나고, 신청하려면 한 번 더 눌러야 한다. ③ 키보드로 바꾸고 "Tab 키 누르기"를 여러 번 누른다. 포커스는 카드 제목을 차례로 지나가지만 숨은 정보와 신청 버튼에는 끝내 닿지 못한다. ④ 개선안을 켜고 세 기기를 다시 해 본다. 정보가 늘 보이고, 키보드 포커스는 신청 버튼에도 멈춘다. (이 목업은 "첫 탭을 호버로 처리하는 브라우저"를 가정했다. 실제 동작은 브라우저와 운영체제에 따라 다르며, 첫 탭에 바로 링크로 넘어가는 경우도 있다. 키보드의 경우는 숨은 부품이 화면에서 빠져 포커스를 받지 못하게 만든 흔한 구현을 가정했다.)

원칙은 하나다. 호버는 꾸밈으로만 쓰고, 정보와 동작은 호버 없이도 닿게 한다. 호버로 그림자를 짙게 하거나 색을 조금 바꾸는 것은 좋다. 그러나 남은 자리, 가격, 신청 버튼처럼 결정에 필요한 것은 늘 보이게 둔다. CSS에는 기기의 포인터 성격을 묻는 미디어 쿼리가 있다(Media Queries Level 4). (hover: hover)는 주된 입력 장치가 호버할 수 있는지, (pointer: coarse)는 주된 포인터가 손가락처럼 정밀하지 않은지를 묻는다. 호버 꾸밈을 @media (hover: hover) 안에 넣으면 휴대폰에서 탭한 뒤 호버 색이 남아 있는 어색한 모습을 막을 수 있다. 다만 노트북에 터치스크린이 달린 경우처럼 한 기기에 여러 입력이 섞여 있을 수 있으므로, 이 질문은 꾸밈을 고를 때만 쓰고 정보를 숨길지 말지를 정하는 데는 쓰지 않는다.

@media (hover: hover) {
  .card:hover { box-shadow: 0 4px 16px rgb(0 0 0 / .12); }
}
/* 남은 자리와 신청 버튼은 미디어 쿼리 밖, 늘 보이게 */

호버나 포커스로 나타나는 내용(말풍선 같은 것)을 꼭 써야 할 때도 규칙이 있다. WCAG 2.2 성공 기준 1.4.13(호버나 포커스로 나타나는 콘텐츠, AA)은 그 내용을 Esc 키 등으로 닫을 수 있어야 하고, 포인터를 그 내용 위로 옮겨도 사라지지 않아야 하며, 사람이 치우기 전까지 머물러야 한다고 요구한다. 화면 확대를 쓰는 사람이 말풍선을 읽으려고 포인터를 옮기다가 말풍선이 사라지는 일을 막기 위해서다.

휴대폰부터 상태를 그린다 상태 표를 넓은 화면에서 시작하면 호버 칸부터 채우게 된다. 휴대폰 화면부터 상태를 그리면 호버 칸은 처음부터 비어 있고, 그 대신 "누르는 순간"과 "누른 뒤"가 중요해진다. 그 다음에 넓은 화면을 그리며 호버를 꾸밈으로 더한다. 5장에서 말한 모바일 우선이 레이아웃만이 아니라 상태 설계에도 통한다.

서윤의 웹 개편 노트

CASE 서윤웹 개편 노트 · 7장

"신청하기"가 대답하게 하기

6장에서 토큰을 정리한 다음 날, 지후가 개발 중인 행사 페이지를 키보드로만 써 보다가 손을 멈췄다. "Tab을 누를 때마다 어딘가로 가긴 하는데, 어디 있는지 모르겠어요." 서윤의 시안에서 "신청하기" 버튼에 정해 둔 상태는 호버 색 하나뿐이었다. 개발 템플릿에는 포커스 링을 지우는 outline: none이 들어 있었다. 모집이 끝난 행사의 버튼은 겉모습은 그대로인데 눌러도 아무 일이 없었고, 신청을 보내는 동안에도 버튼이 가만히 있어 시험 삼아 누른 주인의 신청이 두 번 들어갔다.

같은 주에 도경이 휴대폰으로 행사 날짜를 고르다가 옆 날짜를 눌렀다. 날짜 칩은 28 px에 간격 4 px. 도경은 열 번 중 서너 번은 옆 날짜를 눌렀다고 했다(가상 기록, 도경 한 사람의 간이 시험). 글자를 가장 크게 해 둔 도경의 휴대폰에서는 120×36으로 고정한 "모임 신청하기" 버튼의 글자 끝이 잘려 "모임 신청하"까지만 보였다.

서윤은 버튼 하나를 위해 상태 표를 처음부터 다시 그렸다. 호버는 책방 초록을 OKLCH 명도만 낮춘 색, 눌림은 더 낮춘 색에 2 px 내려앉기를 더했다. 포커스는 색이 아니라 링으로 정했다. 짙은 바닥글 위에서 짙은 링이 사라지는 것을 이 장의 시뮬레이터로 확인하고, 어두운 영역 안에서는 크림색 링을 쓰는 규칙을 더했다. 지후는 이중 링을 제안했지만, 서윤은 바탕이 흰 본문과 짙은 바닥글 두 가지뿐이라 규칙 하나로 충분하다고 보았다. 지후는 "사진 위에 버튼을 올리는 날이 오면 이중 링으로 가요"라고 메모했다.

터치 타깃은 기준을 고르는 데 시간이 걸렸다. WCAG 2.5.8의 24 px는 넘기면 되는 최소선이고, 서윤은 도경의 손과 걸으면서 누르는 손님을 생각해 앱 지침과 같은 44 px를 사이트의 최소로 정했다. 날짜 칩을 44로 키우면 휴대폰 한 줄에 일주일이 다 들어가지 않는다. 서윤은 칩을 줄이는 대신 날짜 줄을 가로로 넘기게 하고, 끝의 칩이 반쯤 잘려 보이게 해서 더 있다는 단서를 주었다. 호버에만 보이던 "남은 자리"는 카드에 늘 보이게 옮겼다.

항목서윤의 선택확인한 값
기본책방 초록 #2f7d5b, 흰 글자, 아래 그림자 2 px흰 글자 대비 4.99
호버#186b4a(OKLCH 명도 0.53 → 0.47), @media (hover: hover) 안에서만흰 글자 대비 6.48
포커스:focus-visible, 3 px 링 #22342a, 2 px 띄움. 어두운 영역 안에서는 #ffe2d7 링링 대비: 흰 바탕 13.19, 바닥글 위 크림 링 10.74
눌림#025f3f(명도 0.43), 2 px 내려앉고 그림자 사라짐흰 글자 대비 7.75
비활성#d9dfdb 바탕, #5f6b65 글자, 글자를 "신청 마감"으로글자 대비 4.11(대비 예외지만 읽히게)
처리 중 표시눌림 다음에 돌아가는 표시와 "신청 중…", 그동안 다시 누르기 무시(aria-busy)중복 신청 막기는 서버에서도
크기고정 크기 없음. 안쪽 여백 위아래 10·좌우 20 px, 최소 높이 44 px, 글자는 rem글자 200%에서도 잘리지 않음
터치 타깃사이트 최소 44×44 CSS px(WCAG 2.5.8의 24는 하한)날짜 칩·메뉴·닫기 단추 모두
날짜 칩44×44, 칩 사이 8 px(3장의 간격 단계), 줄은 가로로 넘김모형: 걸으면서도 잘못 누르기 2% 안팎(28·4일 때 약 11%)
이 장에서 정한 것버튼 상태 다섯 가지(기본·호버·포커스·눌림·비활성)와 처리 중 표시, 상태마다 색 말고 하나 이상의 단서(링, 내려앉기, 글자). 터치 타깃 최소 44×44 CSS px(서윤의 선택). 날짜 칩 44×44와 칩 사이 간격 8 px, 칩 줄은 가로로 넘기기. 호버에는 정보를 숨기지 않는다. 숫자 중 대비는 이 책의 엔진으로 계산한 값이고, 잘못 누르기 비율은 이 장의 단순 모형으로 낸 참고값이라 도경의 휴대폰으로 다시 확인하기로 했다.
아직 남은 문제버튼이 대답하게 되자 도경이 다른 것을 물었다. "그런데 신청 페이지로 가는 길은 어디 있니?" 휴대폰 화면 오른쪽 위의 세 줄 아이콘이 메뉴라는 것을 도경은 몰랐다.
다음 단계8장: 휴대폰 메뉴를 숨길지 펼칠지, 지금 어디 있는지 어떻게 알릴지 정한다.

핵심 정리

  1. 컴포넌트는 되풀이해 쓰는 화면의 부품이다. 생김새보다 하는 일로 고른다. 데려가면 링크(<a href>), 이 자리에서 하면 버튼(<button>).
  2. 부품은 기본·호버·포커스·눌림·비활성·로딩 상태로 대답한다. 상태 표로 빈칸 없이 설계하고, 상태마다 색 말고도 링·모양·글자 같은 단서를 하나 이상 준다.
  3. 포커스 표시는 키보드 사용자의 커서다. 지우지 말고 :focus-visible로 바꾼다. 링은 어떤 바탕에서도 3:1 이상 보여야 한다(WCAG 2.4.7, 1.4.11, 2.4.13).
  4. 부품의 크기는 고정하지 않고 내용 + 안쪽 여백 + 최소값으로 정한다. 사용자가 글자를 키워도 글자가 잘리지 않는다.
  5. 손가락 터치는 노린 점 둘레에 흩어진다. 잘못 누르기는 이웃까지의 거리(크기의 절반 + 간격)가 정한다. WCAG 2.5.8의 24 CSS px는 하한이고, 44·48은 플랫폼 지침이다.
  6. 피츠의 법칙: \(MT = a + b \log_2(D/W + 1)\). 시간은 거리와 폭의 비에 달려 있다. 자주 누르는 것은 크고 가깝게. 호버는 꾸밈으로만 쓰고 정보와 동작은 호버 없이 닿게 한다.

확인 퀴즈

1. WCAG 2.2의 성공 기준 2.5.8(타깃 크기 최소, AA)이 요구하는 누르는 면의 최소 크기와 그 예외를 바르게 설명한 것은?

2.5.8은 24×24 CSS px를 최소로 두고, 간격 예외(지름 24 원이 겹치지 않음), 문장 안의 링크 같은 예외를 둔다. 44×44는 더 엄격한 2.5.5(AAA)와 Apple 지침의 값이고, 48×48 dp와 약 9 mm는 Material Design 지침의 설명이다.

2. 피츠의 법칙(섀넌 형식)으로 볼 때, 버튼을 지금보다 두 배 먼 곳으로 옮기면서 폭도 두 배로 키우면 어려움의 지수 ID는 어떻게 되는가?

\(ID = \log_2(D/W + 1)\)에서 \(D\)와 \(W\)가 함께 두 배가 되면 \(D/W\)가 변하지 않으므로 ID도 그대로다. 시간은 거리나 폭 하나가 아니라 둘의 비에 달려 있다.

3. 개발 템플릿에 "마우스로 누를 때 생기는 테두리가 보기 싫다"는 이유로 button:focus { outline: none; }가 들어 있다. 가장 알맞은 고침은?

포커스 표시를 지우면 키보드 사용자는 지금 위치를 잃는다(WCAG 2.4.7). :focus-visible을 쓰면 마우스로 누를 때는 링이 생기지 않아 원래의 불만도 해결된다. 호버는 포인터의 상태라 포커스와 다르고, 아주 작은 색 차이는 1.4.11의 3:1을 넘기 어렵고 색을 구별하기 어려운 사람에게는 사라진다.

4. 넓은 화면 시안에서 행사 카드의 "남은 자리"와 "신청하기"가 포인터를 올릴 때만 나타난다. 휴대폰과 키보드 사용자를 생각한 판단으로 가장 알맞은 것은?

첫 탭을 호버로 처리하는지는 브라우저마다 다르고, 키보드 사용자는 :hover로 숨긴 정보에 닿지 못한다. 길게 누르기는 발견하기 어렵다. 미디어 쿼리는 한 기기에 여러 입력이 섞일 수 있어 정보를 숨길지 정하는 데 쓰기에 믿을 만하지 않다. 결정에 필요한 정보는 늘 보이게 둔다.