본문 바로가기
프로젝트/Prolog

[Prolog 시즌3] SelectBox (dropdown) 리팩토링 - 접근성 UP

by g2hans 2021. 8. 22.

🌸 프로젝트를 하면서 정리한 글입니다. 피드백은 언제나 격한 환영 🌸


전에 UX 공부하다가 '드롭다운과 스크롤 디자인'이라는 주제의 글을 본 적이 있다. 기억에 남는 이야기로 드롭다운의 경우 활성화되었을 때, 화살표의 방향에 대해서 어떻게 보아야하는지 고찰한 내용과, 요즘에는 화살표를 가리고 모달 느낌으로 만들어버리는 트렌드로 자리잡고 있다는 내용이 있다. 

 

시즌 1에서 드롭다운 메뉴를 직접 구현해보고 싶다는 의지로 페어와 함께 유튜브를 참고해서 커스텀한 dropdown 메뉴를 만들었다. <select> 태그와 <option> 태그를 커스텀하면 되지 않냐는 의견도 있지만, 모든 브라우저에서 동일한 모습을 보여주고 싶어서 ul 태그와 li태그를 통해서 모달처럼 위에 떠있는 형태로 만들었다.

 

구현된 모습

 

🛠 Custom Select Tag(dropdown menu) 리팩토링

리포트 영역에 width를 줄이고 넣어야해서, 코드도 다시 살펴볼겸 리팩토링을 진행했다. 다시보니, 코드가 불필요한 부분도 몇개있기도 했고 과거의 나 똑똑했자나..? 하는 부분도 많았다. ㅎㅅㅎ 그때 고생했던 루터에서의 모습이 새록새록 (루터 갈수있는 곳 맞나요ㅠ 신기루설..)   

 

리팩토링을 할 부분은 다음과 같았다.

 

1. 당시 커스텀한 selectbox가 안보일 경우를 생각하면서 option 태그를 그대로 살려두었는데, 정작 option 태그에는 이벤트핸들러 함수를 적용하지 않았다. 즉, 스크린리더와 같이 커스텀한 selectbox를 사용하지 못하는 경우에는 아무리 option을 선택해도 적용되지 않는 문제가.. (이건 버그였다)

2. useState로 elements를 생성하고 지우는 과정을 반복했는데,  굳이 그렇게 할 필요가 없을 것 같았다.

3. 필요없는 스타일 삭제 

4. label을 커스텀 화살표를 만드는 것 외에 만들지 않아서, 내용이 없는 빈 껍데기를 사용한 셈이었다. 태그를 수정하고 title 필요!

 

💬 리팩토링 전 코드 훑기 1

들어가기 전에 코드의 구성을 보자면 대략 다음과 같다.

 

<label>
  <select>
    <option></option>
    <option></option>
    <option></option>
  </select>
  <ul>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</label>

 

label의 ::after 가상 요소를 사용해서 드롭다운 화살표를 만들었다.

먼저 그러기 위해서는 select에 기본적으로 제공하는 화살표를 안보이게 해야한다. css에서 appreance:none을 해주면 되는데, 모든 브라우저에서 적용해야하기 때문에 다음과 같이 적용하면 된다. 모두 표준은 아니긴 하다.(링크 참고) 접두사별로 -webkit(사파리, 크롬)-moz(파이어폭스)-ms(엣지) 의 브라우저를 지원한다. 우리 프로젝트는 사파리와 크롬만 다루긴하지만, 혹시몰라 파이어폭스까지는 적어뒀다. (미안해 엣지) 

 

  appearance: none;
  -moz-appearance: none;
  -webkit-appearance: none;

 

가상 요소는 다음과 같이 구현하였다. (현재 emotion 라이브러리를 통해 CSS in JS로 스타일링을 하고 있다) 리팩토링을 하겠다고 생각한 가장 큰 이유 중 하나가 label의 width가 유동적이어야 한 부분이다. 따라서 width를 props로 사용하는 쪽에서 넘겨받게 코드를 구현했다. 만약 화살표를 아래를 향하는 화살표가 아니라 위를 향하는 화살표로 만들고 싶다면 border-top-color를 border-bottom-color로 수정해서 코드를 작성하면 된다.

 

const Label = styled.label`
  position: relative;
  display: inline-block;
  width: ${({ width }) => width ?? `${width}`};

  &::after {
    content: '';
    display: inline-block;
    position: absolute;
    right: 2rem;
    top: 45%;

    border: solid 0.8rem transparent;
    border-top-color: ${COLOR.BLACK_600};
    border-radius: 0.125rem;
    cursor: pointer;
  }
`;

 

 

👁 접근성 UP

앞서 리팩토링 할 부분에서 1번과 4번 모두 접근성에 관련된 부분이다. (역시 접근성이 제일 재밌어 🧚‍♀️)

먼저 쉬운 4번부터 말하자면, 기존의 코드는 label이 제 역할을 하고있지 않았고 느꼈다. 라벨은 말그대로 사용자 인터페이스의 설명하는 역할을 해야한다.

label 텍스트는 텍스트 입력과 시각적으로 관련이 있을뿐만 아니라 프로그래밍적으로도 관련이 있습니다. 예를 들어, 화면리더기(screenreader) 는 폼 입력(form input)에서 label 을 읽어서 보조기술(assistive technology) 사용자가 입력해야하는 텍스트가 무엇인지 더 쉽게 이해할 수 있게 합니다.  - mdn <label>

 

또한, KWCAG 검사항목 24개 중 레이블 제공의 항목이 있다. 

 

레이블 제공
사용자 입력에는 대응하는 레이블을 제공해야 한다.

 

현재 구조가 label 태그가 select 태그를 감싸고있는 형태라서, id와 for 속성을 사용하여 한번 더 연관시켜줄 필요는 없었다. 하지만 현재  label는 단순히 wrapper 역할을 위한 것이라서  aria-label 속성을 부여했다. input 태그라면 title 속성을 사용해도 되지만, select는 title 속성이 없다..! 또 wrapper 역할만 하기 때문에 div 태그로 변경해도 되지 않을까? 라는 생각이 들지만 시멘틱하게 사용하기 위해서(뭐가 더 시멘틱한지는 잘 모르겠다) 그대로 두었다. 그렇게 해서 바뀐 코드를 HTML만 간략히 써보자면 다음과 같다.

 

<label aria-label={title}>
  <select>
    <option></option>
    <option></option>
    <option></option>
  </select>
  <ul>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</label>

위 코드에서 실제로 사용자에게 보여지는 부분은 <ul>, <li> 부분이다. 하지만, 어떠한 이유에서 커스텀한 ul 태그가 보이지 않는다면, select 는 제 역할을 하지 못하게 된다. 따라서 브라우저가 기본으로 제공하는 select가 보이는 경우를 대비해서 option 태그에도 동일하게 이벤트를 적용해야 한다. 시즌1에서 코드를 작성할때만 해도 스크린리더를 사용할 줄 몰라서 확인할 생각을 안해보았는데, 스크린 리더를 켜보니 대참사 😅 나는 분명 선택을 했는데 적용이 select 태그에 선택됨이 표현이 안되는 것이다..! 왜..? 처음에는 voiceOver에서 selectbox는 다른 선택 방법이 있는 줄 알고 voiceOver 설명을 열심히 찾았다😀 (절대 내탓이 아닐거라 생각하지만, 응- 아니야 내잘못이야~) 그런 경우가 없게 코드를 짜면 되는거 아니야? 라는 생각도 할 수 있다. 그러나 모든 브라우저 버전을 알 길이 없고 현재 mousedown을 통해서 커스텀한 element가 보이는데 마우스를 사용하지 못하는, 대표적으로 스크린리더의 경우 선택을 했을 때를 대비하자는 것이다.

 

 

방법은 매우 단순하다. select의 value를 바꾸어주는 onChange 이벤트를 등록해주면 된다. 어렵게 생각해서 고민을 많이 했는데, 다음부터는 커스텀 안했을 때 동작을 먼저 구현하고, 커스텀한 ui를 추가하면 헤매지 않고 코드를 작성할 수 있을 것 같다. 매우 대략적으로 작성하자면 다음과 같은 코드이다. 

 

<label onMouseDown={onOpenCustomSelectBox} width={width}>
  <select name={name} title={title} onChange={onSelectItem} value={selectedOption}>
    {options.map((option) => (
      <option key={option} value={option}>
        {option}
      </option>
    ))}
  </select>

  {custom select open Flag && (
    <ul>
      {options.map((option) => (
        <li
          key={option}
          onMouseDown={(event) => onSelectItem(event, option)}
        >
          {option}
        </li>
      ))}
    </ul>
  )}
</label>

 

코드를 자세히보면, li의 onMouseDown의 이벤트핸들러와 select의 onChange 이벤트핸들러가 동일하다. 차이점이 있다면, 항목(option)에 대한 인자를 넘겨주는지 아닌지의 차이이다. 이전의 onMouseDown 이벤트핸들러로서만 onSelectItem가 존재했을 땐, select에 보이는 값을 바뀐 option으로 보여주어야하기 때문에 직접 값을 넘겨 state를 변경하고 렌더링이 되도록 했다. 그러나 select의 onChange 이벤트에서는 option 값을 event의 target의 value로 알 수 있기 때문에 이벤트 핸들러 내부의 함수에서 option이 인자로 넘어왔을 때와 아닐 때를 구분해주었다.

 

const onSelectItem = (event, option) => {
  event.stopPropagation();

  option ? setSelectedOption(option) : setSelectedOption(event.target.value);
  setIsSelectBoxOpen(false);
};

 

(추가로 label에도 onMouseDown 이벤트가 있기 때문에 부모요소에 이벤트 전파를 막기 위해서 stopPropagation를 사용했다.)

 


이외에 더 정리하고 싶은 내용은 리팩토링 이야기보다는 스크롤바를 커스텀하는 방법과  디토가 열심히 만들어준 선택한 option 위치로 스크롤이 이동하는 방법인데 다음에 해야겠다! ㅎㅅㅎ + 글작성이 안되어서 삽질한 이야기와 state관리를 좀 더 가독성있게 boolean 타입의 상태관리로 바꾼 이야기도 쓰고싶다ㅏ..!

댓글