커스텀 UI의 숨겨진 복잡성: 왜 시맨틱 HTML 버튼을 사용해야 하는가

커스텀 UI의 숨겨진 복잡성: 왜 시맨틱 HTML 버튼을 사용해야 하는가

네이티브 HTML 버튼을 처음부터 다시 만드는 것은 시시포스의 형벌과도 같은 작업입니다. 단순한 <button> 요소 하나가 수십 년간의 접근성 표준, 브라우저 동작, 그리고 운영 체제 통합을 모두 포함하고 있기 때문입니다. 커스텀 요소를 사용하여 네이티브 버튼을 복제하려면, 개발자는 키보드 포커스부터 ARIA 역할(roles)부터 복잡한 폼 검증 및 제출 로직에 이르기까지 모든 것을 수동으로 구현해야 합니다.

네이티브 버튼의 요구 사항

네이티브 버튼을 진정으로 대체하려면, 커스텀 컴포넌트는 엄격한 UX "법칙"과 접근성 요구 사항을 충족해야 합니다. MDN, W3C ARIA 사양, 그리고 WCAG 2.2에 따르면, 기능적인 버튼은 다음과 같아야 합니다:

  • 역할 및 레이블링: button 역할을 가져야 하며 명확한 접근 가능한 레이블을 보유해야 합니다 (WCAG SC 4.1.2, 1.3.1).
  • 포커스 관리: 키보드로 포커스할 수 있어야 하며, 포커스되었을 때 시각적으로 보여야 합니다 (WCAG SC 2.4.3, 2.4.7).
  • 입력 방식: 마우스 클릭, 터치, 스타일러스, 그리고 기타 포인팅 장치를 통해 활성화되어야 합니다.
  • 키보드 상호작용: 포커스되었을 때 특히 Space 키와 Enter 키로 활성화되어야 합니다 (WCAG SC 2.1.1).
  • 폼 통합: 타입(submit, reset, button)을 지원하고, 폼 검증에 참여하며, formaction, formmethod, formtarget과 같은 속성을 처리해야 합니다 (WCAG SC 3.3.1).
  • 상태 관리: disabled와 같은 상태를 지원해야 합니다 (WCAG SC 4.1.2).
  • 현대적 API 지원: Popover API 또는 Invoker Commands API와 같은 최신 브라우저 기능과 통합되어야 합니다.

구현의 격차: 마크업에서 500줄의 코드까지

버튼을 처음부터 구축하는 것은 단순한 HTML 태그와 모든 사용자를 위해 작동하게 만드는 데 필요한 기저 로직 사이의 거대한 격차를 드러냅니다.

1. 접근성 및 포커스

커스텀 태그(예: <sagan-button>)는 기본적으로 <span>으로 취급됩니다. 이를 버튼으로 만들기 위해서는, 개발자는 접근성 트리와 페이지의 탭 순서에 나타나도록 role="button"tabindex="0"을 수동으로 추가해야 합니다. 아이콘 버튼의 경우, 장식용 이모지나 아이콘을 aria-hidden="true"를 사용하여 숨기고, 스크린 리더가 이모지의 문자 그대로의 이름을 읽어주는 것을 방지하기 위해 설명적인 aria-label을 제공하는 추가 작업이 필요합니다.

2. 이벤트 핸들링 및 입력 방식

네이티브 버튼은 다양한 입력 방식을 자동으로 처리합니다. 커스텀 구현체는 마우스, 터치, 스타일러스 상호작용을 모두 커버하기 위해 mouseup, touchend, 그리고 onpointerup을 명시적으로 리스닝해야 합니다. 또한, 키보드 접근성을 위해서는 네이티브 브라우저의 기대치에 맞추기 위해 keydown (Enter 키)과 keyup (Space 키)에 대한 별도의 로직이 필요합니다합니다.

3. 폼 연관성 및 검증

네이티브 버튼의 가장 복잡한 측면 중 하나는 HTML 폼과의 관계입니다. 이를 복제제하려면, 커스텀 요소는 static formAssociated = true를 설정하고 type, form, formaction, formmethod, formenctype, formnovalidate, formtarget, name, value를 포함한 속성들에 대한 방대한 양의 getter와 setter를 구현해야 합니다합니다.

또한, Validation API와 통합되어야 하며, checkValidity()reportValidity()와 같은 메서드를 구현하고 ElementInternals를 통해 validity 상태를를 관리해야 합니다합니다.

4. 상태 및 이벤트 순서

커스텀 버튼이 불안정한 동작을 하지 않도록 하려면, 개발자는 addEventListener 메서드를 래핑(wrap)해야 합니다. 이는 내부 로직(예:

Sources