클래스 선택자는 CSS Specificity3에서 엘리먼트 선택자보다 순위가 높고, ID 선택자보다는 낮아 범용성이 높은 특징을 갖습니다. 이러한 특성 때문에 많은 개발자들이 대부분의 CSS 규칙을 클래스 이름으로 정의하는 것이 흔한 관행이 되었습니다.
그러나 W3C CSS 명세에서는 클래스 이름 위주의 CSS 규칙 작성을 권장하지 않는다고 나와 있습니다. HTML의 구조적 의미를 약화할 수 있기 때문입니다. 개인적 의견으로 HTML을 벗어나 CSS 가상 클래스(Pseudo-classes)에서도 :checked, :default, :disabled, :invalid, :required 등 충분히 참조 가능한 빌트인 상태를 제공합니다.
CSS는 클래스 어트리뷰트에 강력한 기능을 부여하여, 고유한 역할이 거의 없는 엘리먼트(예: HTML의 <div> 및 <span>)를 기반으로 나만의 “문서 언어"를 설계하고 “class” 어트리뷰트를 통해 스타일을 지정할 수 있습니다. 하지만 문서 언어의 구조적 요소는 일반적으로 정해진 의미가 있지만, 사용자가 임의로 정의한 클래스는 그렇지 않을 수 있습니다. 따라서 이러한 관행을 피하는 것이 좋습니다. — “Class Selectors” Selectors Level 3 | W3C
*원문에서 “Authors (작성자)” 키워드를 직역해 보니 어색해서 생략하거나 “사용자"로 대치했습니다.
참고:@ui/Tooltip 모듈은 수도코드 이해를 돕기 위해 가정한 가상의 Tooltip UI 컴포넌트입니다.
<scriptlang="ts">importTooltipfrom'@ui/Tooltip';interfaceProps{errorMessages: string[];}let{errorMessages}:Props=$props();letinvalidPrice=$derived(errorMessages.length>0);consterrorTooltipId='price-input-error';</script><Tooltipenabled={invalidPrice}><!--
aria-live="assertive":
엘리먼트 안의 콘텐츠가 바뀔 때마다 보조 기술(Assistive Technology) 도구가 즉시 읽어줍니다.
--><spanslot="message"id={errorTooltipId}aria-live="assertive">{errorMessages.join('<br/>')}</span><inputname="price"aria-errormessage={errorTooltipId}aria-invalid={invalidPrice}/></Tooltip><style>input[name="price"][aria-invalid="true"]{border:1pxsolidred;background-color:rgb(255,0,0,0.5);}</style>
가격 Input UI에 유효하지 않은 값이 입력되면, aria-invalid 어트리뷰트를 ture으로 주입합니다. <style /> 블록 내 코드를 보시면, 가격 Input UI의 aria-invalid 어트리뷰트 값이 true인 경우 red 색상의 외곽선과 rgb(255, 0, 0, 0.5)의 배경색이 스타일링 됩니다.
두 번째 하위 항목으로 “오류 메시지를 Tooltip UI로 표시하는 피드백"이 있습니다. Tooltip을 사용하여 상세 오류 메시지를 전달할 때, aria-errormessage의 값을 오류 메시지 엘리먼트 ID인 errorTooltipId 상숫값으로 할당하여 오류 메시지와 입력 필드 간의 관계성을 명시할 수 있습니다.
button[aria-expanded='true']{background-color:lightgray;&>i.chevron-icon{/* 버튼 레이블 옆의 🔼 아이콘을 반대로 돌림 */transform:rotate(0.5turn);}}button>i.chevron-icon{transition:transform0.5s;}
위 CSS 규칙은 글꼴 선택 버튼이 열림 상태일 때 버튼의 배경색을 lightgray로 스타일링합니다. 그리고 버튼 레이블 옆의 🔼 아이콘을 180도 회전하면서(rotate(0.5turn)) 해당 엘리먼트의 transform 프로퍼티에 대해 0.5초의 전환(transition) 효과를 지정했습니다.
이로 인해 시계 방향으로 회전하며 🔽 아이콘으로 바뀌는 전환 애니메이션이 0.5초 동안 재생됩니다.
ul[role='listbox']{display:none;}button[aria-expanded='true']+ul[role='listbox']{/* top, left 지정 코드 생략: JS 실행 시점에서 popover 라이브러리로 구현함 */display:block;position:absolute;}
위 CSS 규칙은 버튼의 aria-expanded 상태를 선택자로 활용하여 목록 상자의 열림 여부를 제어합니다. 버튼의 aria-expanded 어트리뷰트 값이 true일 경우, 인접 형제 선택자(Next-sibling combinator) + 로 바로 다음에 위치한 ul 엘리먼트로 감싸진 목록 상자가 화면에 나타나도록 적용됩니다.
이와 같은 선택자 조합을 통해 목록 상자의 숨김 표시에 관한 제어를 JavaScript 실행 시간에 직접 수행하지 않아 UI 상태 표현을 정적으로 분리할 수 있습니다.
li[role='option']>i.selected-icon{visually:hidden;}li[role='option'][aria-selected='true']>i.selected-icon{/* 선택된 경우 글꼴 이름 옆에 ✅ 아이콘 표시하기 */visually:visible;}
aria-selected은 어떤 항목이 선택되었는지 여부를 가리키는 데에 사용됩니다. 위의 코드 블록을 보시면, 목록의 모든 옵션 엘리먼트에서 이 어트리뷰트를 바탕으로 ✅ 체크 아이콘을 시각적으로 표시하거나 숨기는 CSS 규칙이 정의되어 있습니다.
따라서 선택한 옵션에 대한 피드백을 ARIA 어트리뷰트와 함께 시각적으로 전달하여 사용자의 장애 여부와 관계없이 동등한 정보를 제공할 수 있습니다.
여담: 저는 특이도에 따라 기본 상태의 CSS 규칙이 다른 규칙에 의해 덮어 씌워지는 것이 나쁜 코드 냄새라고 생각하여, 분기에 따른 스타일을 분리하여 정의하는 것을 선호합니다.
- ul[role='listbox'] {
+ button[aria-expanded='false'] + ul[role='listbox'] {
display: none;
}
button[aria-expanded='true'] + ul[role='listbox'] {
/* top, left 지정 코드 생략: JS 실행 시점에서 popover 라이브러리로 구현함 */
display: block;
position: absolute;
}
- li[role="option"] > i.selected-icon {
+ li[role="option"][selected="false"] > i.selected-icon {
visually: hidden;
}
li[role="option"][selected="true"] > i.selected-icon {
/* 선택된 경우 글꼴 이름 옆에 ✅ 아이콘 표시하기 */
visually: visible;
}
아래 두 어트리뷰트 모두 첫 번째 사례 내용 중에 Input UI와 Tooltip 사이의 관계성과 동등한 이점으로, 자동화된 웹 브라우저 테스트 구성에 유용하게 쓰이는 단서로 쓰일 수 있습니다.
aria-controls는 다른 HTML 엘리먼트를 직접 제어하고 있음을 명시하는 어트리뷰트입니다. 예시의 버튼에서 aria-controls는 버튼을 클릭했을 때 나타나는 목록 상자의 ID를 참조하고 있습니다.
이는 스크린 리더와 같은 보조 기술(Assistive Technology) 사용자가 버튼과 제어 대상 UI의 관계를 명확히 파악할 수 있도록 돕습니다.
aria-activedescendent는 복합적인 UI에서 초점(Focus)이 실제로 전환하지 않고 가상적으로 활성화된 자식 엘리먼트를 보조 기술에 전달하는 데에 사용합니다.
목록 내 탐색 시 피드백: 보조 기술 도구를 통한 목록 탐색 시 항상 현재 활성 항목의 정보를 알 수 있게 됩니다.
Roving Tabindex: 접근성 표준 가이드에 따르면 목록 상자의 옵션 간의 이동은 방향키로 제한됩니다. 그리고 Tab 키를 누른 경우의 목록 박스를 벗어나는 게 올바른 인터렉션입니다. 따라서 두 번째 예시의 마크업 코드 블록을 유심히 읽어보면 활성화한 옵션만 tabindex가 0이고, 나머지 옵션은 모두 -1로 설정되어 있습니다.
키보드 조작에 관한 모범 사례에 대해서 궁금하시다면, 제가 직접 외부 오픈소스에 컨설팅하는 과정에서 나눈 penxle/readable#1128 대화를 참고 바랍니다.
“특별한 도움을 필요로 하는 이들을 위해 길을 닦는 것은 모두를 위해 길을 닦는 것과 같습니다." 장애를 가진 공립학교 학생에게 영감을 받았습니다 - 일러스트 작가 케빈 앨리
유니버설 디자인을 추구하는 ‘하스미 다카시 씨는 공공시설물의 건축설계 공모에서 심사위원장을 맡았습니다. 공공시설물은 2층 짜리 건물인데도 엘리베이터가 설계에 없었습니다. 그래서 그는 엘리베이터를 당연히 설치해야 하는 것 아니냐는 질문을 심사 위원들에게 던졌습니다. 이 질문을 들은 심사위원들은 부정적인 반응을 보였는데, 어떤 심사위원은 건물 사용자 가운데 장애인이 없다는 이유로 반대했고, 어떤 심사위원은 젊은 사람들이 편한 것만 찾는다고 핀잔을 주었다고 합니다. 다카시는 심사 위원들을 설득하려고 묘안이 없을까 고민하다가 이렇게 말했습니다. “2층에 많은 사람을 수용할 수 있는 회의실이 있습니다. 이 회의실로 짐을 옮길 때 엘리베이터를 사용할 수 있습니다.” 이 말을 듣고서야 심사위원 들의 마음을 조금씩 움직였다고 합니다.
일반인은 지하철에 설치된 장애인 리프트, 공공장소에 있는 장애인 화장실, 장애인용 슬로프를 보면서 자신과 관계없는 것이라고 생각합니다. 이런 인식은 (소프트웨어를 포함한) 제품을 만드는 개발자나 디자이너에게도 찾아 볼 수 있습니다. 즉, 이런 인식을 가진 개발자가 장애인이 사용하는 물건을 만들 때 장애인만이 사용할 수 있는 제품을 만드는 경향이 있습니다. 예를 들어 휠체어를 탄 장애인을 지하철 플랫폼까지 데려다 주는 리프트가 이런 인식에서 출발한 것 입니다. 하지만 장애인만 사용 하는 제품을 만들자는 특수해에서 일반인도 사용할 수 있는 제품을 만들자는 일반해로 전환한다면, 장애인용 휠체어보다는 엘리베이터를 설치했을 겁니다.
(중략) 우리가 필요한 것은 장애인 뿐만 아니라 아이, 어른, 노인 등의 모든 사람이 사용할 수 있는 일반해 혹은 융합해입니다.6 우리는 지금까지 더욱 완벽한 일반해를 이끌어 내기 위한 첫 걸음으로 우리 개발자가 몰랐던 ‘사용자’ 에 대해서 알아봤습니다. 이제부터는 우리가 만들었던 소프트웨어에서 어떤 문제가 있는지 살펴 보겠습니다.
이 글을 작성하게 된 이유로 웹 접근성에 대한 제 멘탈 모델7을 글로 옮겨 “웹 접근성이 단순히 스크린리더 전용이(특수해) 아닌 UX와 DX의 설계 도구로, 포괄적으로(융합해) 쓰일 수 있구나"를 외부에 알리고 싶었습니다.
웹 접근성을 같이 신경 쓰는 것이야말로 더 좋은 경험을 만든다고 생각합니다. 어찌 되었든 웹 페이지를 이루는 기본 구성은 의미론적인 마크업 문서이기도 하고, 스스로 이 글의 짜임새를 구조화하면서 “웹 접근성은 사용자 경험의 부분 집합이다“라는 통찰을 얻게 되었습니다.
웹 접근성 기술은 다른 웹 프런트엔드 기술에 비해 상대적으로 관심과 모범 사례가 흔하지 않은 상황입니다. 여전히 제도적으로 의무화된 항공사 웹 서비스 외에 모범 사례가 흔하지 않은 현실에 안타까움을 느낄 따름입니다. 웹 접근성 또한 중요한 프런트엔드 기술이라는 인식이 퍼졌으면 좋겠습니다.
마지막으로 ARIA 활용을 고려하기 전에, HTML을 통한 기본 접근성을 우선 확보한 상태에서 신중히 결정해야 한다는 “No ARIA is better than Bad ARIA.”2 원칙을 다시 당부드리며 마칩니다.
Open UI: W3C 커뮤니티 그룹 중 하나로, 기본 HTML 사양에는 없어 별도 구현이 필요한 UI 컴포넌트에 대한 모범 사례를 정리하여 연구하는 커뮤니티입니다.
<input type="checkbox" /> 엘리먼트를 확장하여 구현하는 Switch UI가 대표적으로 HTML에서 제공하지 않은 UI 패턴에 해당합니다. 이 경우 ARIA Role 중에 switch를 통해 접근성 힌트를 제공할 수 있습니다. ↩︎
“올바르지 못한 ARIA 사용은 애초에 하지 않는 것이 낫다”라는 메시지를 적은 문단은 W3C의 APG Practices 페이지 맨 상단 배너에 소개하는 “No ARIA is better than Bad ARIA.” 원칙과 같이 링크된 문서를 직접 참조해서 작성했습니다.