키보드 접근성
키보드 초점이 보이지 않는 UI를 재현하고 수정한 과정
WCAG 2.2 성공 기준 2.4.11(Focus Not Obscured) 규정과 position: sticky 상단 네비게이션에 의한 초점 가림 현상 해결법을 다룹니다.
실무 검증 기록
키보드 조작 및 초점 경로 검증
본 기록은 공개 fixture와 재현 조건을 기준으로 정리한 기술 설명입니다. 실제 브라우저·스크린리더 조합의 동작은 별도 수동 검토가 필요합니다.
| 검증 상태 | 공개 fixture 기반 재현 완료 |
|---|---|
| 검증 기준 | 공개 fixture 및 재현 조건 |
| 검증 범위 | 키보드 초점 표시 가림(Focus Obscured) 및 scroll-padding 검증 및 명시된 입력값에 한정 |
| 수동 확인 | 실제 브라우저·키보드·스크린리더 환경에서 별도 확인 필요 |
| fixture에서 확인한 기대 결과 | [실패 상태] 포커스 링이 고정 헤더 뒤에 가려짐 ➔ [수정 목표] scroll-padding-top과 실제 Tab 이동 조정 |
검증에 사용된 실제 픽스처 / 재현 조건
60px sticky header, 앵커 이동, focus-visible- 동적으로 확장되는 챗봇 위젯이나 쿠키 배너가 런타임에 덮는 경우 정적 CSS 분석으로 감지 불가
- CSS overflow: scroll 컨테이너 내부의 중첩 스크롤 초점 이동
웹페이지 상단에 고정되어 화면을 아래로 스크롤해도 항상 따라다니는 고정 내비게이션 바(Fixed/Sticky Header)는 현대 웹 디자인의 가장 보편적인 패턴 중 하나입니다. 사용자가 언제 어디서든 메뉴를 쉽게 찾을 수 있도록 돕는 훌륭한 시각적 장치이지만, 키보드로 웹사이트를 탐색하는 지체 장애인이나 전맹/저시력 사용자에게는 뜻하지 않은 '초점 실종'이라는 치명적인 함정을 만들어냅니다. 사용자가 Tab 키를 눌러 페이지 내부의 링크나 폼 컨트롤로 이동할 때, 브라우저의 기본 스크롤 동작은 초점을 받은 요소를 뷰포트 맨 위(Top: 0)에 딱 맞추어 스크롤하기 때문에, 해당 요소가 상단 고정 헤더의 뒷배경 밑으로 쏙 들어가 완전히 가려져 버리기 때문입니다. 이에 따라 W3C는 WCAG 2.2에 성공 기준 2.4.11(Focus Not Obscured, Minimum, Level AA)을 신설했습니다. 이 글에서는 초점 가림 현상의 발생 원인을 시뮬레이션하고, 최신 CSS 스펙인 scroll-margin-top을 활용해 완벽하게 해결하는 표준 기법을 다룹니다.
WCAG 2.2 2.4.11과 2.4.12: 초점 가림 방지 기준의 분리
W3C가 2023년 WCAG 2.2를 발표하면서 초점 가림에 관한 기준을 두 개의 레벨로 명확하게 나눈 이유를 먼저 이해해야 합니다.
성공 기준 2.4.11(Focus Not Obscured, Minimum, Level AA)은 사용자가 키보드로 초점을 맞춘 인터랙티브 요소가 고정 헤더, 플로팅 배너, 챗봇 위젯 등에 의해 '완전히(Entirely)' 가려져서는 안 된다고 규정합니다. 즉, 요소의 일부분이라도 눈에 보인다면 AA 레벨은 가까스로 통과합니다. 반면 향상된 기준인 성공 기준 2.4.12(Focus Not Obscured, Enhanced, Level AAA)는 초점을 받은 요소의 '어떤 부분도(No part)' 단 1픽셀조차 가려져서는 안 되며 100% 온전하게 노출되어야 한다고 엄격하게 요구합니다.
실무에서는 사용자의 조작 안정성을 보장하기 위해 단순히 AA의 최소 조건에 만족하지 않고, 상단 헤더 뒤로 초점이 1px도 침범하지 않는 AAA 수준의 완전 노출 설계를 기본 목표로 삼는 것이 바람직합니다.
전통적인 자바스크립트 스크롤 보정의 한계와 결함
과거에는 이 문제를 해결하기 위해 window.addEventListener('scroll', ...)이나 요소의 focus 이벤트를 자바스크립트로 가로채서, 헤더 높이만큼 window.scrollBy(0, -headerHeight)를 수동으로 밀어내는 스크립트 트릭을 자주 사용했습니다.
그러나 이러한 스크립트 방식은 다음과 같은 치명적인 한계를 노출했습니다. 첫째, 브라우저가 기본 스크롤을 먼저 수행한 뒤 자바스크립트가 뒤늦게 스크롤을 튕겨내므로 눈에 거슬리는 화면 깜빡임과 진동(Jitter)이 발생합니다. 둘째, 반응형 웹에서 모바일과 데스크톱의 헤더 높이가 달라지거나 스크롤 다운 시 헤더가 축소되는 인터랙션이 들어갔을 때 계산 오차가 빈번하게 발생하여 유지보수 비용이 기하급수적으로 증가했습니다.
순수 CSS scroll-margin-top을 활용한 완벽하고 우아한 해결책
모던 웹 브라우저는 자바스크립트의 도움 없이도 브라우저 네이티브 수준에서 스크롤 안착 지점을 오프셋할 수 있는 CSS Scroll Snap 모듈의 scroll-margin 속성을 완벽하게 지원합니다.
원리는 매우 간단하면서도 강력합니다. 키보드 초점을 받을 수 있는 모든 본문 앵커 대상, 제목(Heading), 폼 입력창, 버튼 요소에 상단 고정 헤더의 높이보다 약간 더 큰 여유 공간을 scroll-margin-top으로 지정하는 것입니다. 예를 들어 상단 헤더의 높이가 64px라면, CSS에 :target, [tabindex='-1'], input, select, textarea, button, a { scroll-margin-top: 80px; }을 선언해 둡니다.
이렇게 선언해 두면 사용자가 내부 앵커 링크를 클릭하거나 Tab 키로 해당 요소로 초점을 이동시킬 때, 브라우저 렌더링 엔진이 자체적으로 상단 80px의 안전 여백을 계산하여 스크롤을 멈춥니다. 그 결과 초점을 받은 요소는 고정 헤더 밑으로 숨지 않고, 헤더 아래 16px의 쾌적한 여백을 두고 시야에 완벽하게 들어오게 됩니다.
반응형 헤더 높이 변화와 CSS 변수(--header-height)의 유기적 결합
실제 프로덕션 환경에서는 모바일에서는 56px, 데스크톱에서는 72px, 스크롤을 내리면 48px로 축소되는 등 헤더의 높이가 동적으로 변하는 경우가 많습니다. 이때 고정된 픽셀값을 CSS에 적어두면 오차가 생길 수 있습니다.
가장 성숙한 실무 패턴은 CSS 사용자 정의 속성(CSS Variables)을 연동하는 것입니다. :root { --site-header-height: 64px; }로 기본 높이를 선언하고, 미디어 쿼리나 헤더 컴포넌트 내부에서 ResizeObserver를 통해 실시간 헤더 높이를 --site-header-height 변수에 동기화합니다. 그런 다음 공통 스타일 시트에서 scroll-margin-top: calc(var(--site-header-height) + 1.25rem);으로 지정하면, 어떤 디바이스나 스크롤 상태에서도 초점 가림 현상을 100% 방지할 수 있습니다.
또한 초점 표시자(:focus-visible)의 외곽선(Outline)이 2px 이상의 두께와 주변 배경 대비 3:1 이상의 명암비를 확보하도록 스타일링함으로써, 사용자가 광활한 웹페이지에서 현재 자신의 위치를 단 0.1초 만에 식별할 수 있는 최고의 키보드 접근성 경험을 완성할 수 있습니다.
WCAG 2.2 SC 2.4.11(최소)과 SC 2.4.12(전면)의 정량 판정 기준
WCAG 2.2에서 웹 접근성의 신규 지침으로 채택된 초점 가려짐(Focus Not Obscured) 기준은 두 가지 단계로 세분화됩니다.
SC 2.4.11(Level AA)은 초점을 받은 요소가 상단 고정 헤더나 하단 플로팅 바 등 작성자가 생성한 UI에 의해 완전히 100% 가려져서는 안 되며, 사용자가 초점의 위치를 식별할 수 있는 최소한의 영역이 노출되어야 합니다. SC 2.4.12(Level AAA)는 요소의 경계 상자 전체가 단 1픽셀도 가려지지 않고 온전히 노출되어야 합니다.
대부분의 상용 사이트에서 발생하는 실패 사례는 position: fixed 헤더 뒷편으로 링크나 입력창이 100% 파묻히는 현상이며, 이는 Level AA에 직접 위배되는 중대한 결함입니다.
- SC 2.4.11 (Level AA): 대화형 요소가 고정 UI에 의해 100% 완전히 가려지면 위반, 최소 식별 영역 노출 필수
- SC 2.4.12 (Level AAA): 대화형 요소의 경계 상자 전체가 단 1픽셀의 가려짐도 없이 100% 온전히 노출 필수
- 영향을 미치는 고정 UI: Sticky Header, Sticky Footer, 플로팅 챗봇 버튼, 쿠키 동의 배너, 하단 독바
CSS scroll-padding 및 scroll-margin을 활용한 네이티브 초점 스크롤 보정
과거에는 고정 헤더에 의해 초점이 가려지는 문제를 막기 위해 window.scrollBy({ top: -80 })와 같은 자바스크립트 스크롤 연산을 강제로 실행했습니다. 그러나 이는 브라우저의 네이티브 스크롤과 충돌하여 화면이 덜덜 떨리는 지터(Jitter) 현상을 유발했습니다.
현대 CSS 표준은 이를 자바스크립트 없이 브라우저 레이아웃 엔진 수준에서 네이티브로 보정하는 scroll-padding-top 및 scroll-margin 속성을 제공합니다.
- html { scroll-padding-top: 86px; }: 상단 고정 헤더 높이(70px)에 안전 여백(16px)을 더해 뷰포트 상단 데드존 설정
- .target:focus-visible { scroll-margin-top: 90px; }: 특정 대화형 컴포넌트에 개별 오프셋 부여
- 성능 및 안정성: 자바스크립트 리스너 제로, 브라우저 합성 스레드에서 직접 처리되므로 스크롤 떨림 제로
초점 유실 및 상호작용 실패 원인 분석
| 초점 조작 실패 상황 | 정상 초점 제어 복구 방법 |
|---|---|
| 전역 CSS에 :focus { outline: none; } 속성을 적용하고 대체 포커스 스타일을 지정하지 않는 것 | :focus-visible을 활용하여 키보드 탐색 사용자를 위한 선명한 테두리(outline) 스타일을 유지합니다. |
| position: fixed 상단 바를 구현하면서 대화형 요소에 scroll-margin-top을 부여하지 않아 포커스 요소가 헤더 뒤로 숨는 것 | 고정 헤더 높이에 맞추어 대화형 요소 및 앵커(anchor) 태그에 scroll-margin-top 오프셋을 지정합니다. |
수정 후 키보드 조작 회귀 점검 순서
- 마우스를 떼고 페이지 상단에서 Tab 키를 연속으로 입력하여 포커스 링의 시각적 이동을 추적합니다.
- position: fixed/sticky 헤더 밑으로 포커스가 숨겨지는 지점을 발견합니다.
- 해당 포커스 가능 요소에 scroll-margin-top: [헤더높이]px 속성 및 :focus-visible 테두리 스타일을 지정합니다.
수동 키보드 탐색 시 주의할 한계
- 초점 링이 3:1 이상의 대비를 충족하더라도 배경과 동일한 색상 테두리를 사용할 경우 육안 식별이 불가능합니다.
- WCAG 2.2 SC 2.4.12(Focus Not Obscured Enhanced, Level AAA)는 일체의 가림이 없는 완전 노출을 요구하므로 마진을 넉넉히 주어야 합니다.