모든 사용자를 위한 더 보기 메뉴 만들기
안녕하세요. 엔비전스입니다.
컴퓨터에는 무수히 많은 메뉴가 있습니다. 흔히 메뉴라고 하면, 무엇이 생각나시나요?
사용자가 고를 수 있는 연관성 있는 무언가를 그룹별로 모아서 나열하고, 정리해놓은 판 또는 팜플렛이 생각나실 겁니다. 컴퓨터에서의 메뉴는 무엇일까요? 그리고 어떤 형태가 있을까요?
이번 시간에는 메뉴 형태와 역할을 간략하게 살피고, 드롭다운 형태의 예제로 어떻게 하면 모든 사용자에게 프렌들리한 메뉴를 만들 수 있는지 살펴보고자 합니다.
➊ 들어가기 앞서: 배경지식 - 참고자료
이번 주제는 드롭다운 메뉴를 주로 다룰 것이므로, 메뉴의 구현에 대해서는 WAI-ARIA 바르게 사용하기 6부: menu role 바르게 사용하기에서 정리하였습니다. 또는 명세에 대한 다음의 원서 페이지를 참고할 수 있습니다.
- Menu - Accessible Rich Internet Application
- Menubar - Accessible Rich Internet Application
- Menuitem - Accessible Rich Internet Application
- MenuitemRadio - Accessible Rich Internet Application
- MenuitemCheckbox - Accessible Rich Internet Application
➋ GUI에서 메뉴란?
메뉴는 Windows나 MacOS 등, 그래픽 유저 인터페이스에서, 여러 개의 항목 중, 하나를 선택하면 기능이 실행되며 닫히는 형태의 목록 팝업을 말합니다. macOS이건 Windows이건 이 메뉴 요소는 우리가 의식하는 것보다 많은 곳에서 사용합니다.
➌ 활성화 방법에 따른 메뉴 분류
메뉴는 크게, 마우스 오른쪽 버튼 클릭, 키보드의 팝업 키를 통해 특정 요소를 눌렀을 때 나오는 고정적이지 않은 형태의 컨텍스트(맥락) 메뉴와 특정 버튼이나, 특정 요소를 통해 확장하는 드롭 다운 형태의 고정적인 메뉴가 있습니다.
컨텍스트 메뉴는 Context(맥락) 뜻 그대로, 특정 요소를 마우스로 오른쪽 버튼 클릭을 했을 때, 상황에 맞는 동작을 구성해주는 메뉴들을 일컫습니다.
웹에서, 이미지를 마우스 오른쪽 버튼 클릭했을 때, ’이미지를 다른 이름으로 저장’과 같이 필요한 항목을 보여주는 것을 생각하면 쉽습니다. 컨텍스트 메뉴는 맥락과 관련된 메뉴이기 때문에, 시각적으로도 열린 메뉴가 요소와 관련 있음을 표현하기 위해 주변에 표시되어야 해요.
당연한 얘기라고 생각하시겠지만, 생각보다 지켜지지 않아서 일반 응용프로그램에서 마우스 오른쪽 버튼으로 클릭했을 때는 요소 주변에 컨텍스트 메뉴가 잘 나타나지만, 버튼과 같은 요소에 키보드의 팝업 키나, Windows의 메뉴 열기 단축키인 Shift+F10을 사용했을 때, 요소와는 전혀 상관없는 위치에 메뉴가 표시되는 경우를 심심치 않게 볼 수 있습니다.
드롭 다운 메뉴는 두 가지 종류가 있습니다. 일반적인 버튼을 눌렀을 때, 아레에 나타나는 더 보기와 같은 메뉴가 있으며, 화면 맨 위에 고정된 메뉴 막대 항목을 펼쳤을 때 나타나는 하위 메뉴 형태가 있습니다.
이 둘의 공통점은 드롭 다운 형태이며, 고정적인 기능을 수행할 때 주로 사용됩니다. 그런데, 이런 의문점이 들 수 있습니다. 바로 “그럼 도대체, 콤보 상자와 뭐가 다른가요?”라는 생각을 할 수 있다는 것이지요. 누르면 한 가지가 선택이 되며, 팝업이 닫히는 이벤트가 발생한다는 점은 동일합니다. 시각적인 효과나 구조는 비슷하나, 둘은 전혀 다른 기획 구조를 가지고 있습니다.
위에서 언급했듯, 하위 메뉴가 있는 항목을 제외한 모든 메뉴 항목은 누르면 레이어가 닫히면서 기능이 바로 실행됩니다. 하지만, 콤보 상자는 기능이 바로 실행되지 않으며, 그래서는 안 됩니다.
콤보 상자는 기본적으로, 입력 서식에 속하며, 입력 서식은 모든 정보를 다 입력하고, 마지막에 특정 버튼을 눌러 제출하여 기능을 수행하는 사용하는 것이 정상적인 과정입니다. 선택 시, 기능이 바로 실행되고, 특히나 화면 맥락이 바뀌는 형태의 위젯은 반드시 메뉴로 구현해야 한다는 점을 짚고 넘어갑니다.
기본적으로 메뉴 막대를 제외한 메뉴는 세로로 나열하는 것이 일반적이며, 최근에는 세로로 나열된 항목과 가로로 나열된 항목을 혼합하는 사례가 늘고 있습니다. 다만, 메뉴 막대가 아닌 메뉴 항목은 기본적으로 세로 배열을 기본으로 하기 때문에, 수평 항목에 접근해도 메뉴를 탐색하는 화살표 키가 바뀌거나 하진 않습니다.
메뉴 막대는 위 사진과 같이 특정 위치에 고정되어 언제든 접근할 수 있는 가로 형태로 항목을 나열한 메뉴를 말합니다. 메뉴 막대의 항목은 어떤 상황에서든 사용되는 보편적인 기능들로 구성합니다. 메뉴 막대의 대표적인 메뉴 항목들로는 Windows에서는 메모장과 같은 기본 앱에서 Alt 키를 누르면 나오는 파일(F) 편집과 같은 것들을 예로 들 수 있으며, macOS에서는 항상 상단에 고정되어 있습니다.
메뉴 막대는 대부분 하위 메뉴를 가지는 것들로 구성하지만 꼭 그래야만 하는 것은 아닙니다. 메뉴 막대의 메뉴 항목도 일반 메뉴와 마찬가지로 하위 메뉴가 없다면 Enter를 눌렀을 때, 기능이 바로 실행됩니다.
메뉴 막대는 일종의 응용 프로그램 상의 랜드마크로, 한 개의 창, 한 개의 화면에 하나만을 배치하는 것이 일반적이며, 바람직한 구조로 여겨집니다. 그렇기 때문에 만약, 메뉴 막대와 비슷한 형태이지만, 전역에서 사용하는 기능이 아닐 때는 메뉴 막대보다는 도구 막대(toolbar) 유형을 사용하는 것이 바람직할 것입니다.
➍ 스크린리더, 키보드 사용자에게 친숙한 드롭다운 메뉴 만들기
많은 웹사이트 또는 앱에서 점 세 개를 가로 또는 세로로 나열한 아이콘 버튼을 배치하고, 화면에 꺼내 두기에는 너무 과한 버튼들을 메뉴로 제공하고 있습니다. 특히 웹사이트에는 별도의 메뉴 위젯이 없었기 때문에 높은 확률로 키보드 접근성을 고려하지 않고, 시멘틱을 고려하지 않은 사이트를 만나게 됩니다. 이 섹션에서는 메뉴 요소를 어떻게 만들어야 사용자가 레이아웃을 이해하기 쉬운지에 대해 설명하고자 합니다.
menu, menuitem을 통한 커스텀 메뉴 구현
시멘틱한 메뉴를 만들기 위해 menu 컨테이너에 menuitem을 사용한 개발자분들은 접근성에 관심이 있으신 분들이고, 매우 고마운 일입니다. 하지만, role만 제공하는 것은 사용자에게 없는 것만도 못한 상황을 만들기도 합니다.
NVDA나 JAWS같은 외국 스크린리더는 별도의 키보드 단축키를 사용해서 조작하는 요소에 대해서는 웹을 탐색할 때 사용하는 가상 커서를 자동으로 꺼 줍니다. 가상 커서는 웹 브라우저에서 초점을 받을 수 없는 일반 텍스트 요소나 이미지 요소 등을 읽을 수 있는 보이지 않는 가상의 커서로, 스크린리더를 켜고 웹사이트를 이용하면, 스크린리더와 기능이 겹치는 키는 스크린리더의 단축키로 동작하게 됩니다. 따라서, 이를 해결하고자 Tab을 통해 특정 요소에 초점을 보내면, 자동으로 가상 커서 기능을 해제하는 것이지요. HTML상에서는 input 요소가 이에 해당합니다. 그런데, Enter로 메뉴 항목을 누르고, 화살표 키로 탐색하는 등의 상호작용 동작을 구현하지 않는다면 가상 커서는 꺼질 것이며, 기본 키보드 단축키로 동작할 것이기 때문에 아무런 동작도 하지 않는 문제가 생깁니다.
- menu와 menuitem 등을 사용하려면 반드시 참고 자료 섹션의 이전 아티클을 참고하여 올바른 키보드 조작 방법을 제공해야 합니다.
- 메뉴를 여는 menuitem나 button에는 aria-expanded와 aria-haspopup 속성을 제공해야 합니다.
- 확장된 모든 메뉴는 빈 영역을 클릭하여 전체 닫기를 수행할 수 있어야 합니다.
menu 유형을 사용하지 않는 메뉴 구현하기
menu 유형으로 메뉴를 만드는 것이 의미론적으로는 매우 좋으나, 꼭 제공하지 않아도 훌륭한 메뉴 구조를 만들 수도 있습니다. 다음과 같은 형태로 WAI-ARIA 사용을 최소화한 메뉴를 만들 수 있습니다.
aria-label로 시멘틱한 컨테이너 만들기
아래와 같이 ul이나 ol, dl 등의 목록 컨테이너 같은 요소에 aria-label을 사용하면 목록을 처음 탐색할 때, 목록 이름으로 레이블을 읽게 됩니다.
<ul aria-label=“OOO에 대한 더 보기 메뉴”>…</ul>
만약, div로 만들었다면, role=“region”을 함께 사용하면, “OOO에 대한 더 보기 목록, 구역”과 같이 읽게 됩니다.
누르기 동작만으로 조작 가능한 HTML 기본 태그로 메뉴 항목을 구성하기
버튼과 같은 기본 태그로 메뉴 항목을 구성한다면 Tab키로 메뉴를 탐색하게 됩니다. 이는 웹 또는 응용 프로그램 경험이 적은 사용자에게 훨씬 유리한 구조이기도 합니다.
또한, 컴포넌트를 개발함에 있어 복잡한 과정이 생략되는 효과도 있습니다. 그러나, input 요소와 같이 특정 동작을 요구하는 요소는 사용해선 안 됩니다. 메뉴는 한 가지 요소를 선택하고 활성화하면 바로 닫히기 때문입니다.
HTML 기본 태그에 선택정보 제공하기
input을 제외한 HTML 기본 태그에는 선택정보 관련 API가 대부분 존재하지 않습니다. 이러한 부분을 WAI-ARIA를 통해 메꿔줘야 합니다. 우선, 체크 상자에 해당하는 버튼에는 role=“checkbox”에 aria-checked 속성을 사용하여, menuitemcheckbox와 유사한 효과를 낼 수 있습니다.
그러나, radio는 기본적으로 화살표 키를 통해 선택하는 요소이므로, 구현하기에 조금 애매한 부분이 있습니다. 따라서, 일반 버튼을 사용하되, aria-current=“true”를 사용하여, 현재 어떤 것이 선택되어 있는가를 사용자에게 안내할 수 있을 것이며, 라디오 메뉴는 하나의 그룹이므로, 메뉴 유형을 사용할 때와 마찬가지로 role=“group”으로 감싸주어야 합니다.
메뉴를 대화상자로 구현하기
모달 형태의 메뉴 팝업을 만든다면 테크닉 2번과 같이 menu 유형을 사용하지 않는 메뉴로 만드는 것이 간편합니다.
role=“menu” 대신에 컨테이너에 role=“dialog”와 aria-modal=“true”를 제공하고, 테크닉 2번에서 설명했던 것처럼, aria-label로 메뉴 이름을 제공할 수 있습니다.
모달 팝업은 해당 요소 밖은 탐색되어선 안 되므로, Tab 키로 해당 대화상자 내부만을 탐색할 수 있도록 초점을 관리해 주어야 합니다.
대화상자는 ESC로 닫을 수 있어야 하므로, 닫기 동작에 대한 키보드 지원을 제공해 주어야 합니다.
아코디언 요소로 구현하기
menu 유형을 사용하지 않고, 아코디언 형태의 팝업을 구현하려면 마크업에 많은 신경을 기울여야 합니다. 아코디언 형태는 팝업이 열려도 초점을 따로 보내주지 않기에 가장 손이 덜 가는 방법이지만 마크업 구조에 따라 달라질 수 있습니다.
- 메뉴를 여는 버튼에는 반드시 aria-expanded를 사용하여 메뉴가 표시되어 있는지, 숨겨져 있는지 사용자가 알 수 있도록 해야 합니다.
- 특정 스크린리더는 aria-controls로 연결된 컨테이너로 바로가는 단축키가 있으므로, 이 기능을 활용할 수 있게 버튼에 aria-controls로 컨테이너의 HTML id를 연결해 주세요.
- 초점 관리를 하지 않으려면 아코디언 메뉴 컨테이너는 반드시 메뉴를 여는 버튼 바로 아래에 마크업되어야 합니다. 사용자가 버튼을 눌러 확장했으나, 코드 순서상 레이어가 맨 마지막에 위치하게 되면 해당 메뉴에 빠르게 접근할 수 없는 문제가 생깁니다.
➎ 다양한 환경과 입력장치와 친숙한 메뉴 요소 만들기
메뉴는 기기에 따라서 우리가 아는 것과 모습이 달라져야 할 필요가 있습니다. 그리고 특정 요소는 모바일과 적합하지 않을 수도 있습니다. 이 섹션에서는 모바일 환경 특징에 맞는 메뉴에 대해 말하고자 합니다.
모바일 웹사이트에서 메뉴 막대 형태를 사용하는 것은 되도록 지양해야 합니다
데스크톱은 화면의 넓이가 모바일에 비해 작게는 2배, 많게는 10배도 넘게 큽니다. 때문에 메뉴 막대(role=“menubar”)가 매우 큰 이점을 가져다줍니다. 그러나, 모바일의 경우, 화면의 가로 넓이가 좁기 때문에, 메뉴 막대를 구성하게 되면 기본적으로 미관상 페이지가 지저분해 보이는 가벼운 단점이 표면적으로 나타납니다.
그러나, 메뉴 막대를 모바일 페이지에 제공하게 되면, 가로 스크롤이 생긴다면, wcag의 리플로우 이슈가 발생할 가능성이 높고, 이 리플로우 이슈를 해결하기 위해, 메뉴 막대의 폭을 모바일에 맞게 가변 한다면, 메뉴 막대가 여러 줄이 될 수도 있습니다. 메뉴 막대는 한 줄로 표시되는 것이 일반적입니다. 따라서, 공간 활용에 제한이 있으므로, 메뉴 막대 형태는 적합하지 않다고 말할 수 있습니다.
그럼, 메뉴 막대를 대체할 수 있는 요소나 테크닉은 무엇이 있는지 한번 살펴보도록 합시다.
-
메뉴 막대처럼 중첩 메뉴를 사용하지 않고, 가장 기초가 되는 메뉴만을 구성하고, 하위 메뉴는 레이어나 다른 페이지를 통해 이용하게끔 나누는 것이 좋습니다. 일반적으로, 링크를 모아둔 GNB 메뉴를 구성할 때도 PC에서는 중첩된 메뉴를 사용하더라도, 모바일에서는 메뉴를 중첩하지 않는 방향으로 형태를 변경합니다.
-
햄버거 메뉴(대화상자 메뉴)로 메뉴를 만들어서 한 화면에 표시할 수 있는 메뉴의 수를 늘릴 수 있습니다. 온 화면을 전부 덮는 전체 화면 레이어나 반 이상의 화면을 세로로 덮는 사이드 레이어 메뉴를 제공하고, 대화상자 메뉴로 구현하여 메뉴 막대의 기능을 대체할 수 있습니다.
모바일 웹 환경에서 세로 막대 메뉴 구현 시, 하위 메뉴를 배치하지 않는 것이 좋습니다.
꼭 중첩된 메뉴를 사용해야 한다면 화면 중앙에 나타나는 대화상자 형태로 메뉴를 디자인하는 것이 좋으며, 하위 메뉴가 열리면 상위 메뉴를 덮는 구조로 기획하는 것이 좋습니다. 대표적인 사례로는 Android의 select 태그 디자인을 예로 들 수 있습니다. 가급적이면 모바일 화면에서 세로 막대 형태의 메뉴 항목은 하위 메뉴를 가지지 않는 것이 좋습니다. 세로로 나열된 메뉴를 중첩하면 왼쪽 또는 오른쪽에 하위 메뉴가 표시되는데, 가로 넓이가 좁은 모바일 환경에서 이는 리플로우를 유발하는 원인이 됩니다.
안드로이드에서는 select 태그를 팝업 메뉴로 인식하며, 실제로도 옵션이 나열된 팝업창이 나타납니다. 또한, Chrome과 같은 모바일 브라우저의 메뉴 구성을 참고해보자면, 하위 메뉴를 가진 항목은 존재하지 않는 것을 볼 수 있습니다.
모바일 웹에서는 메뉴나 대화상자 요소에 닫기 버튼, 취소 버튼 등을 꼭 제공해야 합니다.
모바일 웹에서 닫기 버튼을 제공하지 않는 사례를 종종 볼 수 있습니다. 대부분 이런 사례는 메뉴나 대화상자는 메뉴가 아닌 빈 곳을 터치하여 메뉴를 닫는 것에 의존하게끔 만들어진 사이트입니다.
앱에서는 운영체제에 맞는 API를 사용할 수 있으나, 웹에서는 사용할 수 없는 기능이 많습니다. 모바일 웹에서 레이어나 대화상자, 메뉴 등에 닫기 버튼이 없다면 사용자는 메뉴를 닫기 위해 무조건 원하지 않는 기능을 눌러 실행하거나 페이지를 다시 불러오는 불합리한 과정이 발생하게 될 수 있습니다.
터치 디바이스가 주가 되더라도 키보드 접근성은 지켜줘야 합니다.
모바일 기기는 누가 뭐라 해도 터치 디바이스가 주 입력 방법이자 상호작용 방법입니다. 그러나, 소근육이 발달되지 않아 터치 동작이 불편한 사용자를 포함한 여러 이유로 하드웨어 키보드를 사용하게 됩니다.
그런데, 하드웨어 키보드 동작을 지원하지 않는다면 어떨까요? 몸이 불편한 사람이라면 해당 메뉴를 전혀 사용하지 못할 것입니다. 또한, 테블릿을 노트북을 대신하여 사용하거나, Samsung Dex로 휴대폰을 모니터와 연결하여 키보드로 작업하는 사용자도 마찬가지로 불편을 겪게 될 것입니다.
컨텍스트 메뉴를 여는 기능을 마우스 클릭 이벤트로 구현하는 것은 이제 그만!
컨텍스트 메뉴는 마우스 오른쪽 버튼을 누르면 열립니다. 그런데, 이것을 이벤트로 구현할 때, 두 가지 방법을 사용할 수 있습니다.
click과 같은 마우스 이벤트 리스너에서 눌린 버튼의 value를 받아와서, 우클릭을 감지하는 방법과 contextmenu 이벤트 리스너를 사용하는 방법입니다.
이 중에 문제가 되는 것은 눈치 채셨을지도 모르지만, 바로 일반 마우스 이벤트를 통해 열기 기능이 동작하게 만든 페이지입니다. 터치 디바이스나 키보드 사용자는 전혀 고려하지 않은 방법인 것이지요.
입력장치에 따른 컨텍스트 메뉴를 여는 다양한 방법
마우스를 제외한 여러 환경에서 컨텍스트 메뉴를 여는 방법은 다음과 같습니다.
- 터치 디바이스: 한 손가락을 길게 눌렀다 때기 (터치 스크린리더는 두 번 탭하고 길게 눌렀다 때기)
- 트랙패드: 버튼이 있다면 마우스와 동일, 버튼이 없는 형태라면 두 손가락으로 터치하기
- 하드웨어 키보드: 스페이스 바 오른 편에 있는 팝업(메뉴) 키(키보드에 따라 없을 수도 있습니다)
- Windows: Shift + F10 단축키
- macOS: macOS는 기본적으로 마우스 키를 사용해야 하며, VoiceOver는, Capslock+Command+Space로 동작 메뉴에서 컨텍스트 메뉴를 열거나, Capslock + Shift + M을 눌러 열 수 있습니다.
이러한 다양한 방법으로 컨텍스트 메뉴를 열 수 있는 반면, mousedown, mouseup, click과 같은 이벤트 타입으로 컨텍스트 메뉴를 열게끔 한다면, 위에서 정리한 유형 중에, 마우스를 대체하는 트랙패드를 제외한 나머지 동작으로는 컨텍스트 메뉴를 열 수 없는 상황이 발생하게 됩니다.
간단한 코드 예제 - 왜 contextmenu 이벤트 타입을 사용해야 하나요?
위에서 언급했듯, 정상적인 커스텀 컨텍스트 메뉴를 만들려면, 컨텍스트 메뉴 동작으로 약속된 이벤트 타입 키워드인 ’contextmenu’를 리스너로 등록해야 합니다.
const FileContextMenu = new Menu(...);
const CustomListViewFileItems = document.querySelectorAll('.web_listview_item.type_file');
[...CustomListViewFileItems].forEach((el,idx)=>{
el.addEventListener('contextmenu',ShowFileContextHandler);
})
function ShowFileContextHandler(evt){
evt.preventDefault();
/* preventDefault()는 아시다시피 이벤트 객체를 통해 브라우저에서 기본으로 제공하는 이벤트를 막는
용도로 사용합니다. 아시는 분들은 아시겠지만, 이것을 설정하지 않으면 커스텀 컨텍스트 메뉴를 열었을 때 브라우저의 기본 컨텍스트 메뉴도 함꼐 열리기 때문에 커스텀 컨텍스트 메뉴가 정상적으로 작동하지 않습니다.
*/
FileContextMenu.open();
}
JQuery 라이브러리를 사용한다면 배열의 반복문을 사용하지 않고, 자체 API를 사용하므로 아래와 같은 코드가 됩니다.
/*
const FileContextMenu = new FileContextMenu(...);
const CustomListViewFileItems = $('.web_listview_item.type_file');
...
// ---핸들러 생략---
...
CustomListViewFileItems.on('contextmenu',ShowFileContextHandler);
*/
contextmenu 타입의 이벤트 리스너는 Windows나 macOS, Android, iOS 등 대중적인 GUI 운영체제의 기본 메뉴 동작을 바인딩 합니다. 그렇기 때문에 지금 잘 사용하지 않는 구형 브라우저 또는 OS를 제외한 환경의 특성에 맞는 동작을 자동으로 지원해 주게 됩니다. 마우스를 사용할 때는 똑같은 마우스 오른쪽 버튼 클릭일지라도, 다른 디바이스에서 똑같은 페이지를 볼 때, 최대한 같은 경험을 제공하기 위해서는 contextmenu 리스너를 사용해야 한다는 것이지요.
만약, 이 contextmenu 타입을 사용하지 않고, 이러한 기능들을 구현하려면, 아래와 같은 수많은 리스너를 등록해야 할 겁니다.
- keydown: 메뉴 팝업 키나, Shift + F10를 지원하려면 keydown이 필요하게 되겠지요?
- click: 가장 기본이 되는 마우스 오른쪽 버튼 클릭으로 여는 것을 구현할 때 click이 사용됩니다.
- touchstart와 touchend: setTimeout, clearTimeout을 사용하여 길게 누르기 동작을 구현해야 합니다.
어떠신가요? 한 가지 동작을 만드는 데 굉장히 번거롭고 효율적이지 않아 보이지요?
➏ 마치며
이번에는 다양한 사용자를 위한 메뉴의 구조 패턴을 중심으로 다뤄보았습니다. 이번 아티클은 정답이나 모범 답안만을 얘기하는 아티클보다는 사용자 관점에서 바라보는 시간이었습니다. 이 글이 많은 도움이 되기를 바랍니다.
이번 아티클은 다소 복잡했으리라 생각하며, 항상 아티클을 작성할 때 마치 코드처럼 리펙토링이 필요하다는 갈증을 느낍니다. 독자분들도 만족하고, 저 또한 만족하는 유익하고 읽기 좋은, 더욱 정제된 아티클을 만들도록 노력하겠습니다. 읽어주셔서 감사합니다.