UX가 무너지는 자리는 대개 정해져 있습니다. 작은 탭 표적, 라벨 없는 칸, 이유 없는 비활성, 지워진 포커스 같은 것들이지요. 여기서는 자주 밟는 함정을 나쁜 코드와 고친 코드로 나란히 놓고 하나씩 짚어 보겠습니다.
32.1 너무 작은 탭 표적
아이콘 버튼을 그림 크기 그대로 두면 손끝에 비해 표적이 너무 작아 옆 버튼이 눌립니다. 눌리는 영역을 넉넉히 키워 줍니다.
.icon-btn {
width: 20px;
height: 20px;
}
실제로 눌리는 판이 20px밖에 안 되어 손가락이 조금만 어긋나도 빗나갑니다. 표적을 44px로 넓힙니다.
.icon-btn {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
아이콘은 그대로 20px여도 눌리는 판이 44px로 커져 손끝이 어긋나도 제대로 눌립니다. 모바일에서 오작동이 확 줄어드는 지점입니다.
32.2 placeholder를 라벨로 오용
이름표를 칸 안 흐린 글씨로 대신하면 입력을 시작하는 순간 사라져 무슨 칸인지 잊게 됩니다. 라벨을 밖으로 꺼냅니다.
<input type="text" placeholder="이름">
입력 중엔 이 칸이 무엇을 받는지 안 보입니다. 라벨을 따로 두고 placeholder는 예시로만 씁니다.
<label for="name">이름</label>
<input id="name" type="text" placeholder="홍길동">
라벨은 입력 중에도 남아 있고 placeholder는 예시 역할만 맡습니다. 사용자는 무엇을 쓰는 칸인지 언제든 알 수 있습니다.
32.3 이유 없는 disabled
조건이 안 맞는다고 버튼을 흐리게 막아 두면, 사용자는 왜 못 누르는지 몰라 답답해합니다. 막는 대신 눌렀을 때 이유를 알려 줍니다.
<button disabled>가입하기</button>
왜 비활성인지 아무 단서가 없어 사용자는 화면 앞에서 멈춥니다. 버튼은 살려 두고 부족한 곳을 짚어 주는 편이 낫습니다.
<button aria-describedby="why">가입하기</button>
<p id="why">약관에 동의하면 가입할 수 있어요.</p>
이제 사용자는 무엇을 채우면 되는지 알고 다음 걸음을 뗍니다. 길을 막기보다 길을 열어 주는 안내가 훨씬 친절합니다.
32.4 포커스 스타일 제거
외곽선이 보기 싫다고 outline: none 으로 지워 버리면, 키보드 사용자는 지금 어디에 있는지 알 길이 없어집니다. 지우지 말고 다듬습니다.
button:focus {
outline: none;
}
보기엔 깔끔해도 키보드로 이동하는 사람은 길을 잃습니다. 마우스엔 감추고 키보드엔 보이게 나눕니다.
button:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
:focus-visible 는 키보드로 이동할 때만 또렷한 외곽선을 보여 주고, 마우스 클릭엔 나타나지 않습니다. 깔끔함과 접근성을 둘 다 챙기는 자리입니다.
32.5 낮은 색 대비
연한 배경에 더 연한 글자를 얹으면 예뻐 보여도 잘 안 읽힙니다. 특히 흐린 회색 본문이 흔한 실수입니다.
.text {
color: #b0b0b0;
background: #ffffff;
}
흰 바탕에 밝은 회색 글자는 대비가 모자라 눈이 금세 지칩니다. 글자색을 충분히 짙게 내립니다.
.text {
color: #374151;
background: #ffffff;
}
본문 대비는 최소 4.5 대 1을 넘기는 것이 안전한 기준입니다. 짙은 회색으로 내리면 오래 읽어도 편하고, 밝은 화면에서도 글자가 묻히지 않습니다.
32.6 로딩 상태 없음
버튼을 눌렀는데 아무 기척이 없으면 사용자는 고장인 줄 알고 또 누릅니다. 진행 중이라는 신호를 보여 줍니다.
<button>주문하기</button>
처리하는 동안 화면이 멈춘 듯 보여 사용자가 중복으로 누릅니다. 상태를 겉으로 드러냅니다.
<button class="btn" aria-busy="true" disabled>
처리 중...
</button>
버튼 글자가 처리 중으로 바뀌고 aria-busy 가 낭독기에도 진행 중임을 알립니다. 잠시 막아 두면 같은 요청이 두 번 나가는 사고도 막힙니다.
32.7 레이아웃 시프트
크기를 안 정한 이미지는 늦게 도착하며 아래 글을 밀어내, 읽던 줄이 튀어 오릅니다. 자리를 미리 잡아 둡니다.
<img src="photo.jpg" alt="사진">
도착 전엔 높이가 0이라 이미지가 오는 순간 아래가 왈칵 밀립니다. 크기를 지정해 상자를 먼저 잡습니다.
<img src="photo.jpg" alt="사진" width="800" height="600">
width 와 height 를 적어 두면 브라우저가 비율만큼 빈 자리를 먼저 그려 둡니다. 이미지가 늦게 와도 아래 문단이 흔들리지 않습니다.
32.8 div를 버튼으로 쓰기
모양만 버튼처럼 꾸민 div 는 키보드로 누를 수도 없고 낭독기가 버튼으로 읽지도 않습니다. 진짜 버튼 태그를 씁니다.
<div class="btn">보내기</div>
이 가짜 버튼은 Tab으로 닿지 않고 Enter로도 눌리지 않습니다. 의미를 담은 태그로 바꿉니다.
<button type="button" class="btn">
보내기
</button>
button 은 키보드 포커스와 Enter 실행, 낭독기 역할까지 공짜로 딸려 옵니다. 모양을 흉내 내느라 애쓰는 대신, 제 역할을 가진 태그를 고르는 것이 정답입니다.
32.9 색으로만 상태 알리기
필수 칸을 빨간 테두리로만 표시하면, 색을 구분하기 어려운 사용자는 무엇이 잘못됐는지 알 수 없습니다. 색에 글과 기호를 더합니다.
.field.invalid {
border: 1px solid red;
}
붉은 테두리 하나만으로는 정보가 색에 갇혀 일부 사용자에게 닿지 않습니다. 문구를 함께 붙여 이중으로 알립니다.
<input class="field invalid" aria-invalid="true"
aria-describedby="err">
<p id="err">! 이메일 형식이 아니에요.</p>
색에 더해 문구와 기호가 함께 상태를 전하니, 색을 못 가리는 사람도 무엇이 문제인지 압니다. aria-invalid 가 낭독기에도 오류 상태를 알려 줍니다.
32.10 사라지는 호버 메뉴
마우스를 올려야만 열리는 메뉴는 손끝으로 쓰는 화면에서 아예 열 방법이 없습니다. 클릭으로도 열리게 만듭니다.
.menu:hover .submenu {
display: block;
}
이 메뉴는 호버가 없는 터치 기기에서 펼쳐지지 않습니다. 눌러서 여닫는 방식으로 바꿉니다.
<button aria-expanded="false">메뉴</button>
<ul class="submenu" hidden>...</ul>
버튼을 눌러 hidden 을 벗기고 aria-expanded 값을 바꾸면 마우스든 손끝이든 똑같이 열립니다. 특정 입력 방식에만 기대지 않는 것이 안전합니다.
32.11 정리
흔한 실수는 하나같이 원칙 하나를 잊었을 때 생깁니다. 표적은 넉넉히, 라벨은 밖으로, 포커스는 남기고, 대비는 충분히, 의미는 제 태그로 담는 이 기본만 지켜도 대부분의 함정을 피합니다. 나쁜 코드와 고친 코드를 나란히 놓고 보면, 좋은 UX가 대단한 기교가 아니라 작은 정직함의 쌓임임을 알게 됩니다.