Missed the action at the 2018 Chrome Dev Summit? Catch up with our playlist on the Google Chrome Developers channel on YouTube. Watch now.

tabindex 사용

네이티브 요소의 DOM 위치가 제공하는 기본 탭 순서가 편리하기는 하지만 탭 순서를 수정하고 싶은 경우도 있을 것입니다. HTML에서 요소를 물리적으로 이동하는 것은 때로 적합하지 못하거나 심지어 실현 불가능할 수도 있습니다. 이런 경우에는 tabindex HTML 속성을 사용하여 요소의 탭 위치를 설정합니다.

tabindex는 어떤 요소에든 적용할 수 있습니다. 그러나 모든 요소에서 반드시 필요한 것은 아니며 다양한 정수값 범위를 취합니다. tabindex를 사용하면 포커스 가능한 요소에 대해 명시적 순서를 지정하고 포커스 불가능한 요소를 탭 순서에 삽입하고 탭 순서에서 요소를 제거합니다. 예를 들면 다음과 같습니다.

tabindex="0": 일반적인 탭 순서에 요소를 삽입합니다. Tab 키를 누르거나 focus() 메서드를 호출하는 방식으로 요소에 포커스를 맞출 수 있습니다.

<custom-button tabindex="0">Press Tab to Focus Me!</custom-button>

tabindex="-1": 일반적인 탭 순서에서 요소를 삭제하더라도 focus() 메서드를 호출하여 계속 요소에 포커스를 맞출 수 있습니다.

<button id="foo" tabindex="-1">I'm not keyboard focusable</button>
<button onclick="foo.focus();">Focus my sibling</button>

tabindex="5": tabindex가 0보다 크면 요소가 일반적인 탭 순서 앞으로 점프합니다. tabindex가 0보다 큰 요소가 여러 개 있다면 탭 순서는 0보다 크고 가장 낮은 값부터 시작해서 계속 높은 값으로 이동합니다. 0보다 큰 tabindex를 사용하는 것은 안티패턴으로 간주됩니다.

<button>I should be first</button>
<button>And I should be second</button>
<button tabindex="5">But I jumped to the front!</button>

헤더, 이미지 또는 문서 제목과 같은 비입력 요소에는 더욱 그렇습니다. 이런 종류의 요소에 tabindex를 추가하면 오히려 역효과를 낳습니다. 가능하면 DOM 시퀀스가 논리적인 탭 순서를 제공하도록 소스 코드를 적절히 배치하는 것이 좋습니다. tabindex를 사용한다면 버튼, 탭, 드롭다운, 텍스트 필드와 같이 사용자가 입력을 제공할 것으로 예상되는 요소인 사용자설정 대화형 컨트롤로 제한하세요.

스크린 리더에는 tabindex가 없으므로 사용자가 중요한 콘텐츠를 놓치지 않을까 걱정하지 마세요. 이미지와 같이 매우 중요한 콘텐츠라도 사용자가 상호작용할 수 있는 대상이 아니라면 포커스 가능하게 만들 이유가 없습니다. 스크린 리더 사용자는 적절한 alt 속성 지원을 제공하기만 한다면 이미지 콘텐츠를 이해하는 데는 문제가 없습니다. 이 속성 지원에 대해서는 잠시 후 다룰 것입니다.

페이지 수준에서 포커스 관리

tabindex가 단순히 유용할 뿐 아니라 꼭 필요할 때가 있습니다. 다 표시할 수는 없지만 여러 가지 다른 콘텐츠 섹션을 한 페이지에 통합하는 강력한 기능을 구현할 수 있습니다. 이 페이지에서는 내비게이션 링크를 클릭하면 페이지 새로고침을 수행하지 않고도 화면에 표시되는 콘텐츠를 변경할 수 있습니다.

이 경우, 여러분은 아마도 선택한 콘텐츠 영역을 식별하고 tabindex를 -1로 부여해서 일반적인 탭 순서에 나타나지 않게 한 다음 focus 메서드를 호출할 것입니다. 이 기술은 포커스 관리라고 하는데 사용자가 인지한 컨텍스트를 사이트의 시각적 콘텐츠와 일치된 상태로 유지할 수 있습니다.

구성 요소에서 포커스 관리

페이지에서 무언가를 변경할 때 포커스를 관리하는 것은 중요합니다. 그러나 때로는 컨트롤 수준에서 포커스를 관리해야 할 경우도 있습니다. 예를 들어, 사용자설정 구성 요소를 빌드하는 경우가 이에 해당합니다.

네이티브 select 요소를 생각해 보세요. 이 요소는 기본적인 포커스를 받을 수 있지만, 포커스를 받으면 화살표 키를 사용하여 추가적인 기능(선택 가능한 옵션)을 노출할 수 있습니다. 사용자설정 select 요소를 빌드한다면 주로 키보드를 사용하는 사용자가 여전히 컨트롤과 상호작용할 수 있도록 이와 동일한 유형의 동작을 노출하고 싶을 것입니다.

<!-- Focus the element using Tab and use the up/down arrow keys to navigate -->
<select>
  <option>Aisle seat</option>
  <option>Window seat</option>
  <option>No preference</option>
</select>

어떤 키보드 동작을 구현할지 알기 어려울 수 있지만 참조하면 도움이 될 만한 문서가 있습니다. Accessible Rich Internet Applications(ARIA) Authoring Practices 가이드는 구성 요소 유형과 이들이 지원하는 키보드 동작 유형을 보여줍니다. 나중에 ARIA에 대해서는 더욱 상세히 설명하겠지만 지금 이 가이드는 새로운 구성 요소에 키보드 지원을 추가하는 데 활용하겠습니다.

라디오 버튼 모음과 비슷하지만 개발자 자신의 고유한 모양과 동작을 구현하는 사용자설정 요소를 만들고 고유한 외형과 동작을 적용할 수 있습니다.

<radio-group>
  <radio-button>Water</radio-button>
  <radio-button>Coffee</radio-button>
  <radio-button>Tea</radio-button>
  <radio-button>Cola</radio-button>
  <radio-button>Ginger Ale</radio-button>
</radio-group>

어떤 종류의 키보드 지원이 필요할지 결정하려면 ARIA Authoring Practices 가이드를 참조하세요. 섹션 2에는 디자인 패턴 목록이 있으며, 이 목록에는 라디오 그룹의 특성표가 있습니다. 이들은 새로운 요소에 가장 근접하게 일치하는 기존 구성 요소입니다.

표에 나와 있듯이, 공통적으로 지원해야 할 키보드 동작 중에 위쪽/아래쪽/왼쪽/오른쪽 화살표 키가 있습니다. 새로운 구성 요소에 이 동작을 추가할 때는 이동 tabindex라는 기술을 사용할 것입니다.

W3C 사양에서 라디오 버튼에 대한 부분 발췌

현재 활성 상태인 하위 항목을 제외한 모든 하위 항목에 대해 tabindex를 -1로 설정하면 이동 tabindex가 작동합니다.

<radio-group>
  <radio-button tabindex="0">Water</radio-button>
  <radio-button tabindex="-1">Coffee</radio-button>
  <radio-button tabindex="-1">Tea</radio-button>
  <radio-button tabindex="-1">Cola</radio-button>
  <radio-button tabindex="-1">Ginger Ale</radio-button>
</radio-group>

이 구성 요소는 키보드 이벤트 리스너를 사용하여 사용자가 어느 키를 눌렀는지 확인합니다. 이때 이전에 포커스를 맞춘 하위 항목의 tabindex를 -1로 설정하고 포커스를 맞출 하위 항목의 tabindex를 0으로 설정한 다음 여기에 포커스 메서드를 호출합니다.

<radio-group>
  // Assuming the user pressed the down arrow, we'll focus the next available child
  <radio-button tabindex="-1">Water</radio-button>
  <radio-button tabindex="0">Coffee</radio-button> // call .focus() on this element
  <radio-button tabindex="-1">Tea</radio-button>
  <radio-button tabindex="-1">Cola</radio-button>
  <radio-button tabindex="-1">Ginger Ale</radio-button>
</radio-group>

사용자가 마지막(또는 첫 번째, 포커스 이동 방향에 따라 달라짐) 하위 요소에 도달하면 첫 번째(또는 마지막) 하위 요소로 다시 루프를 실행하여 포커스를 맞춥니다.

아래에서 완성된 예시를 실행해 볼 수 있습니다. DevTools에서 이 요소를 검사해 보면서 tabindex가 라디오 버튼 사이를 이동하는지 살펴보세요.

GitHub에서 이 요소의 전체 소스를 확인할 수 있습니다.

모달 및 키보드 트랩

포커스를 관리할 때 때로는 피할 수 없는 상황에 직면할 수 있습니다. 포커스를 관리하고 탭 동작을 캡처하면서 이를 완료할 때까지 사용자가 떠나지 못하게 하는 자동완성 위젯을 생각해봅시다. 이것을 키보드 트랩이라고 하고 이는 사용자에게 매우 불만스러울 수 있습니다. Web AIM 검사 목록의 섹션 2.1.2에서는 이 문제에 대해 키보드 포커스는 특정 페이지 요소에서 잠그거나 트랩되어서는 안 됩니다라고 되어 있습니다. 사용자는 키보드만 사용하여 모든 페이지 요소를 이동할 수 있어야 합니다.

이상한 점은, 모달 창에서처럼 이 동작이 실제로 바람직할 때가 있다는 것입니다. 보통은 모달이 표시될 때 사용자가 모달 이면의 콘텐츠에 액세스할 수 있도록 하지는 않습니다. 오버레이를 추가하여 페이지를 시각적으로 덮을 수는 있지만 키보드 포커스가 우연히 모달 밖으로 나가지 못하게 막지는 못합니다.

사용자에게 작업을 저장할지 묻는 모달 창

이 경우, 임시 키보드 트랩을 구현하여 모달이 표시되는 동안만 포커스를 트랩하고 모달이 닫히면 이전에 포커스를 받았던 항목으로 다시 포커스가 돌아가게 할 수 있습니다.

개발자 입장에서 이 작업을 더욱 쉽게 처리할 수 있게 해주는 방법이 있지만(예: <dialog> 요소), 브라우저에서 아직은 널리 지원되지 않습니다.

MDN 기사에서 <dialog>에 대한 추가 정보를 얻을 수 있고 모달 예시에서 모달 창에 대한 추가 정보를 얻을 수 있습니다.

몇몇 요소를 포함한 div와 배경 오버레이를 나타내는 또 다른 div로 표시되는 모달 대화상자를 생각해 봅시다. 이 상황에서 임시 키보드 트랩 구현에 필요한 기본적인 단계를 수행해 봅시다.

  1. document.querySelector를 사용하여 모달 및 오버레이 div를 선택하고 관련 참조를 저장합니다.
  2. 모달이 열렸을 때 포커스가 주어졌던 요소에 대한 참조를 저장하여 모달이 열릴 때 그 요소로 포커스를 되돌릴 수 있도록 합니다.
  3. keydown listener를 사용하여 모달이 열려 있는 동안 키를 누를 때 키를 인식하도록 합니다. 배경 오버레이에서 클릭 동작을 수신하여 사용자가 배경 오버레이를 클릭하면 모달을 닫을 수도 있습니다.
  4. 다음으로, 모달 내에서 포커스 가능한 요소 모음을 가져옵니다. 첫 번째와 마지막의 포커스 가능한 요소는 포커스를 앞뒤로 루프 순환하면서 모달 내에 머무르도록 할 시점을 알 수 있게 해주는 '센티널' 역할을 합니다.
  5. 모달 창을 표시하고 포커스 가능한 첫 번째 요소를 포커스합니다.
  6. 사용자가 Tab 또는 Shift+Tab을 누를 때 포커스를 앞이나 뒤로 이동하면서 마지막 요소나 첫 번째 요소에서 적절히 루프를 수행하도록 합니다.
  7. 사용자가 Esc 키를 누르면 모달을 닫습니다. 이렇게 하면 사용자가 닫기 버튼을 찾지 않고도 모달을 닫을 수 있으므로 매우 유용합니다. 이는 마우스를 사용할 때도 유용한 기능입니다.
  8. 모달이 닫힐 때 모달과 배경 오버레이를 숨기고 앞서 저장한 이전에 포커스를 받던 요소로 포커스를 복원합니다.

이 절차에 따르면 모든 사용자가 유용하고 쉽게 효과적으로 사용할 수 있는 모달 창을 제공할 수 있습니다.

더 자세한 내용은 이 샘플 코드를 살펴보고 완료된 페이지의 라이브 예시를 살펴보세요.