리디자인을 거쳐도 살아남는 포커스 상태: 키보드 우선 체크리스트
요약
리디자인 과정에서 포커스 상태가 깨지는 문제를 방지하기 위한 개발 체크리스트를 제시합니다. 특히 `:focus` 대신 `:focus-visible`을 사용하여 마우스 사용자와 키보드 사용자 모두에게 최적화된 접근성을 확보하는 것이 핵심입니다.
핵심 포인트
- `:focus` 대신 `:focus-visible`을 사용하여 포커스 표시 방식을 개선하세요.
- 마우스를 분리한 상태에서 `Tab` 키로 페이지 전체를 탐색하며 테스트해야 합니다.
- 두 가지 테마 모두에 3:1 대비를 가진 아웃라인이 필수적입니다.
- 포커스 링은 레이아웃 이동을 유발하지 않는 `outline` 속성을 사용하세요.
-
리디자인 과정에서 포커스 상태가 조용히 깨집니다. 저는 배포 전에 6가지 항목을 점검합니다.
-
:focus대신:focus-visible을 사용하세요. 마우스 클릭은 조용하게 유지하고, 키보드는 확실하게 작동해야 합니다. -
두 가지 테마 모두에 대해 3:1 대비를 가진 아웃라인이 필수입니다.
-
마우스를 분리한 상태에서 페이지 전체를 한 번
Tab키로 탐색하세요. 예외는 없습니다.
시각적인 새로고침은 거의 항상 키보드 경험을 저해합니다. 디자인은 더 깔끔해지고, 색상은 더 보기 좋아지지만, 어딘가에서 포커스 아웃라인이 조용히 사라져 버립니다. 저는 충분한 수의 리디자인을 거치면서 이를 위험 요소가 아닌 확실한 것으로 간주하게 되었고, 그래서 매번 실행하는 짧은 체크리스트를 만들었습니다.
전체 내용은 여기에 있습니다. 약 20분 정도 시간을 들여 본인의 사이트에서 직접 테스트해 볼 수 있습니다.
리디자인이 포커스 상태를 먼저 해치는 이유
포커스는 필요할 때만 눈에 보입니다. 이것이 핵심 문제입니다. 제가 브라우저에서 새로운 레이아웃을 바라보며 작업할 때는 마우스를 클릭하고 있고, 마우스는 결코 포커스 링을 보여주지 않습니다. 따라서 키보드 사용자가 의존하는 유일한 것이, 제가 가장 알아차리기 어려운 것입니다.
일반적인 원인은 CSS 리셋입니다. 디자이너가 기본 브라우저 링을
저는 리디자인된 차이점(redesign diff)을 무죄로 입증될 때까지 유죄라고 간주합니다. 전역 스타일을 건드리거나, 컴포넌트를 교체하거나, 테마를 추가할 때마다 포커스가 깨졌다고 가정하고 확인해봅니다. 이는 모든 RAXXO 도구에 포함된 되돌리기 버튼(The Undo Button Every RAXXO Tool Ships With) 뒤에 숨겨진 방어적인 습관과 같습니다. 즉, 아무도 테스트하지 않을 것 같은 위험한 경로를 가정하고, 의도적으로 그 경로를 테스트하는 것입니다.
또 명확하게 말할 가치가 있는 것이 있습니다. 키보드 사용자는 소수의 엣지 그룹이 아닙니다. 파워 유저들은 더 빠르기 때문에 Tab 키로 탐색합니다. 스크린 리더 사용자들은 그것에 의존합니다. 마우스 사용이 고통스러운 운동 장애를 가진 사람들은 그것에 전적으로 의존합니다. 포커스 링(focus ring)이 사라지면, 이들 모두가 한 번에 페이지에서 길을 잃게 됩니다.
:focus 대신 :focus-visible로 교체하기
제가 하는 것 중 가장 가치가 높은 변화는 :focus를 :focus-visible로 대체하는 것입니다.
기존의 포커스 스타일링에는 짜증 나는 트레이드오프가 있었습니다. 만약 키보드 사용자를 위해 :focus를 충분히 눈에 띄게 꾸몄다면, 마우스 사용자는 버튼을 클릭할 때마다 큰 링(ring)을 보게 되어 부자연스러워 보였습니다. 그래서 사람들이 그것을 제거했고, 결국 두 그룹 모두 손해를 봤습니다.
:focus-visible이 이 문제를 해결합니다. 브라우저가 결정합니다. 마우스로 버튼을 클릭하면 링이 없습니다. 키보드로 Tab 키를 누르면 눈에 띄는 링이 나타납니다. 동일한 CSS이지만, 요소가 포커스를 얻은 방식에 따라 다른 동작을 합니다.
제 기본 규칙은 다음과 같습니다:
:focus-visible {
outline: 2px solid currentColor;
...
outline-offset이 사람들이 생각하는 것보다 더 중요합니다. 요소 가장자리에 딱 붙는 링은 포커스 상태가 아니라 테두리(border)로 인식됩니다. 두 픽셀의 간격은 요소가 선택되었음을 명확하게 보여주며, 둥근 모서리에서 링이 잘리는 것을 방지해줍니다.
저는 링에 box-shadow 대신 outline을 사용합니다. outline은 레이아웃 이동(layout shift)을 유발하지 않으며, 부모 요소의 overflow: hidden에 의해 잘리지 않습니다. box-shadow 포커스 링은 스크롤 컨테이너 내부에서 항상 사라지며, 무언가가 스크롤되기 전까지는 알아차리기 어렵습니다.
리셋(reset)으로 outline: none을 사용하는 것에는 함정이 있습니다. 절대, 절대로 이를 :focus-visible 스타일 없이 전역적으로 적용해서는 안 됩니다. 만약 *:focus { outline: none }만 작성한다면, 단 한 줄로 사이트 전체의 키보드 탐색 기능을 삭제한 것입니다.
저는 이것이 프로덕션 환경에서 발생하는 것을 여러 번 목격했습니다.
마우스를 뽑거나 잠시 옆에 두고 Tab 키를 눌러 스왑(swap)을 테스트해 보세요. 이동할 때 모든 인터랙티브 요소가 빛나야 합니다. 만약 그렇지 않다면, 실제로 포커스 가능하지 않은 경우(버튼인 척하는 div 등)이거나, 또는 여러분의 링 스타일이 그 요소에 적용되지 않는 경우입니다.
이것은 제가 다시는 생각할 필요가 없도록 시작 지점에 심어두는 작고 지루한 기본값이며, The Starter Template Every RAXXO Tool Begins From과 같은 아이디어입니다. 한 번 설정하면 모든 곳에 상속됩니다.
두 테마 모두와 대비되는 링(Ring) 만들기
존재하지만 보이지 않는 포커스 링은 아예 없는 것과 같습니다. 제가 지키는 수치는 윤곽선 색상과 그 바로 뒤에 있는 배경색 사이의 3:1 명암비입니다.
3 대 1은 접근성 가이드라인에서 비(非)텍스트 UI 요소가 갖춰야 할 기준치입니다. 포커스 인디케이터는 계산 대상입니다. 그래서 저는 이 링을 요소 자체의 배경색과 페이지 배경색 모두에 대해 확인합니다. 왜냐하면 오프셋 간격 때문에 링이 종종 한쪽에는 절반, 다른 쪽에는 절반씩 걸쳐 있기 때문입니다.
테마 문제가 대부분의 실패가 숨어있는 곳입니다. 저는 이 요소가 나타날 수 있는 모든 배경색에 대해 검사를 실행합니다. 라이트 테마, 다크 테마, 그리고 히어로 배너나 인버티드 팔레트를 가진 푸터 같은 색상 섹션 모두 말이죠. 하나의 윤곽선 색상은 거의 모든 경우를 통과할 수 없습니다.
제가 고친 방법은 링 색상을 하드코딩하는 것이 아니라 테마에 연결하는 것입니다:
:root { --focus-ring: #1a1a1a; }
[data-theme=
배경이 예측 불가능한 요소(예: 사진이나 비디오 위에 놓인 버튼)의 경우, 대비되는 색상으로 두 번째 링을 추가하여 최소한 절반은 항상 보이도록 합니다:
:focus-visible {
outline: 2px solid #fff;
...
흰색 링과 검은색 헤일로(halo)입니다. 그 뒤에 무엇이 있든 둘 중 하나는 반드시 승리합니다. 저는 실제로 이중 레이어가 필요한 곳에서만 `box-shadow`를 사용하고, 폴백(fallback)이 더 중요하기 때문에 클리핑 위험을 감수할 것입니다.
어떤 대비 도구로도 비율을 확인하세요. 아웃라인 색상을 고르고 배경을 선택한 다음 숫자를 읽으세요. 3:1 미만이라면 아웃라인을 두꺼운 2px로 늘리거나 색상을 변경하세요. 은은하게 보이게 하려고 링을 얇게 만들지 마세요. 필요로 하는 사람에게는 '은은함'과 '보이지 않음'이 같은 것이기 때문입니다.
그 사이에, 포커스가 강제 색상 모드(forced-colors mode)에서도 작동하는지 확인하세요. Windows의 고대비 모드는 사용자의 색상을 완전히 무시하며, `box-shadow`가 때때로 실패하는 곳에서 `outline`은 그 재정의를 견뎌냅니다. 이것이 제가 아웃라인을 기본값으로 사용하는 또 다른 이유입니다.
## 페이지 전체를 Tab 키로 이동하기 (마우스 분리)
이것은 CSS 검사가 놓치는 모든 것을 잡아내는 테스트입니다. 저는 마우스를 치워두고, 마치 다른 선택지가 없는 것처럼 시작부터 끝까지 페이지 전체를 Tab 키로 이동합니다.
순서는 시각적 레이아웃을 따라야 합니다. 위에서 아래로, 왼쪽에서 오른쪽으로, 읽는 순서대로 말이죠. 만약 포커스가 헤더에서 푸터로 갔다가 다시 중간으로 점프한다면, DOM(문서 객체 모델) 순서가 시각적 순서와 일치하지 않는 것이고, 키보드 사용자는 지도가 없는 상태로 페이지를 돌아다니게 됩니다.
이것은 리디자인 후에 끊임없이 깨집니다. 왜냐하면 CSS는 DOM을 건드리지 않고도 시각적으로 요소를 이동시킬 수 있게 해주기 때문입니다. 플렉스박스(flexbox)의 `order`나 그리드 배치(grid placement)를 사용하여 HTML의 가장 아래에 위치하는 사이드바를 왼쪽에 두게 만들 수 있습니다. 보기에는 완벽하지만, Tab 순서는 틀립니다. Tab 순서를 고치지 말고 DOM 순서를 고치세요. 양수 값을 가진 `tabindex`는 저항하세요. 그것은 다음 사람의 작업을 더 어렵게 만드는 임시방편일 뿐입니다.
제가 확인하는 것들:
- 모든 정지 지점(stop)마다 보이는 포커스 링을 제공하고, 소리 없이 건너뛰는 부분이 없어야 합니다.
- 숨겨진 요소 중 포커스를 받을 수 있는 것이 없어야 합니다. 모바일 메뉴가 닫혀 있다면, 그 링크들은 탭 순서에 포함되어서는 안 됩니다.
- 트랩(trap)이 없어야 합니다. 모든 모달(modal), 드롭다운(dropdown), 날짜 선택기(date picker)에서 Tab 키를 이용해 빠져나올 수 있어야 합니다. 만약 무언가를 열었는데, 탈출구 없이 그 안에서 Tab 순환만 계속된다면 그것은 트랩이며, 이때 Escape 키가 이를 닫아야 합니다.
- 스킵 링크(skip link)를 첫 번째 정지 지점으로 두어, 키보드 사용자가 모든 페이지에서 네 개의 메뉴 항목을 차례로 탭하는 대신 내비게이션(nav)을 건너뛰고 메인 콘텐츠로 바로 이동할 수 있게 해야 합니다.
모달 트랩은 컴포넌트 교체 후에 문제가 발생하는 부분입니다. 좋은 모달은 열려 있는 동안 의도적으로 포커스를 가두었다가, 닫힐 때 그것을 연 버튼으로 포커스를 되돌려줍니다. 반면 나쁜 모달은 영원히 포커스를 가두거나, 사용자가 완전히 길을 잃게 만드는 페이지 상단으로 다시 던져버립니다. 모든 오버레이(overlay)를 열어보고, Tab 키로 순회하며, Escape 키를 누른 후, 포커스가 합리적인 곳에 착지하는지 확인하세요.
또한 화면 크기를 좁게 조정한 후에도 Tab 순회를 해봐야 합니다. 모바일 레이아웃은 요소들의 순서를 재배열하고 일부를 숨기기 때문입니다. 데스크톱의 탭 순서는 완벽할 수 있지만, 모바일 버전은 엉망일 수 있습니다.
이 워크(walk)를 주요 화면마다 한 번씩 실행하세요. 몇 분밖에 걸리지 않으며, 실제로 키보드 사용자가 되어보는 것과 가장 유사합니다. 만약 이 목록에서 단 하나의 항목만 수행해야 한다면, 이것을 수행하세요. [RAXXO가 따르는 모든 도구의 명령 명명 규칙](https://dev.to/blogs/lab/the-command-naming-convention-every-raxxo-tool-follows)에서의 명명 및 구조에 대한 규율은 같은 정신입니다. 예측 가능한 순서를 통해 아무도 길을 잃지 않게 하는 것입니다.
## 핵심 요약 (Bottom Line)
이 네 가지 검사를 순서대로 실행하면, 리디자인이 도입하는 거의 모든 포커스 회귀(focus regression)를 잡아낼 수 있습니다. `:focus-visible`을 적용하고, 링에 2px의 오프셋을 주며, 어두운 모드(dark mode)를 포함한 모든 배경색과 대비되는 3:1의 명암비를 유지하도록 설정한 다음, 마우스를 치우고 전체 페이지를 Tab 키로 순회해 보세요.
만약 하나만 남겨야 한다면, 그것은 탭 워크입니다. CSS는 스타일시트에서 완벽해 보일 수 있지만 실제 흐름에서는 실패할 수 있으며, 이를 알 수 있는 유일한 방법은 키보드 사용자가 페이지를 사용하는 방식대로 테스트하는 것입니다.
이것을 배포 전 루틴에 추가하면 근육 기억(muscle memory)이 됩니다. 저는 이것을 나머지 출시 게이트들과 같은 장소에 보관하는데, 기억해야 하는 체크리스트는 바쁜 날 건너뛰기 쉽기 때문입니다.
제가 이렇게 기본값을 설정하고 구축하는 모든 곳에 상속받는 전체적인 그림을 원한다면, [Claude Blueprint](https://dev.to/pages/claude-blueprint)가 전체 접근 방식을 설명합니다. 마우스를 분리하고 홈페이지로 탭해 보세요. 적어도 하나의 막히는 지점(dead stop)을 찾게 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기