일기 프로젝트를 하며 웹접근성을 개선해보다!

신은수·2023년 10월 26일
0

기타

목록 보기
4/5

1. 웹표준을 준수

  • 모든 페이지마다 markup validator를 돌려 웹표준을 준수하였습니다. 웹 표준은 웹사이트 및 웹 애플리케이션을 개발할 때 사용하는 규칙과 권고사항을 나타내며, 이러한 규칙을 준수하면 웹 접근성에 대한 기본적인 요구 사항을 충족시킬 수 있습니다.
    Untitled
  • 그러나 웹 접근성을 개선하려면 웹 표준 준수만으로는 충분하지 않습니다. 웹 접근성을 위해 WAI-ARIA(Accessible Rich Internet Applications)와 같은 추가 접근성 기술과 웹 디자인 및 개발의 최상의 실천 방법을 이해하고 준수해야 합니다.

2. Lighthouse 웹접근성 점수 향상

  • Lighthouse tool을 통해 웹접근성 평가를 한 후, 접근성 점수 향상시켰습니다.
  • 기존 점수도 93점으로 나쁜 편은 아니었으나 ‘**Background and foreground colors do not have a sufficient contrast ratio**’를 해결하니 100점까지 향상시킬 수 있었습니다 (이 항목은 웹 페이지의 색상 대비 비율이 충분하지 않아 시각적으로 장애가 있는 사용자나 낮은 해상도를 사용하는 사용자가 콘텐츠를 충분히 읽고 이해하기 어려울 수 있다는 것을 나타냅니다. 텍스트와 배경 색상 간의 대비비율은 최소한 4.5:1 이상이어야 합니다. 이를 해결하기 위해 text의 font-color를 변경하였습니다. )Untitled (1)

3. 입력폼

  • 사용자가 잘못된 입력을 제출했을 때 오류 메시지를 제공해야합니다. 이 오류 메시지는 시각적으로 나타내고 <div> 또는 <span> 요소를 사용하여 스크린 리더 사용자에게 오류를 알릴 수 있어야합니다.
  • 잘못된 입력을 제출한 후 입력 필드로 즉시 이동하여 수정할 수 있도록 해야 사용자가 키보드를 통해 쉽게 이동하고 수정할 수 있습니다.

1) 개선후

  • 소리가 안나오는 관계로, 소리까지 듣고싶다면 해당링크로 이동해주세요 :)

2) 개선코드

  • 마크업
    // ./src/components/signup/SignupForm.tsx
    <form className={'form-wrap'} onSubmit={handleFormSubmit}>
      <label className={'label'} htmlFor="email">
        이메일
      </label>
      <Input
        ref={emailRef}
        id="email"
        type="email"
        error={error === errorDesc[0] || error === errorDesc[1]}
        onChange={onChange}
        value={inputs.email}
      />
    
      <label className={'label'} htmlFor="password">
        비밀번호
      </label>
      <Input
        ref={passwordRef}
        id="password"
        type="password"
        error={error === errorDesc[2]}
        onChange={onChange}
        value={inputs.password}
      />
    
      <label className={'label'} htmlFor="nickname">
        닉네임
      </label>
      <Input type="text" id="nickname" onChange={onChange} value={inputs.nickname} />
    
      {!isPending && <Button>회원가입</Button>}
      {isPending && <strong className={'pending'}>회원가입이 진행중입니다...</strong>}
      <div id="error-message" role="alert" aria-live="assertive">
        {error && <strong className={'error'}>* {error}</strong>}
      </div>
    </form>
    • role="alert": 이 속성은 해당 요소의 역할을 정의합니다. "alert" 역할은 중요한 메시지를 나타내며, 이 메시지가 화면에 표시되면 사용자의 주의를 끌어야 함을 나타냅니다.
    • aria-live="assertive": 이 속성은 해당 엘리먼트의 내용이 언제 스크린 리더에게 읽혀야 하는지를 나타냅니다. "assertive" 값은 스크린 리더가 가능한 빨리 읽어야 함을 의미합니다.
  • 입력창 포커스 코드
    const useSignup = () => {
      const [error, setError] = useState<string | null>(null);
      const [isPending, setIsPending] = useState(false);
      const emailRef = useRef<HTMLInputElement>(null);
      const passwordRef = useRef<HTMLInputElement>(null);
      const { dispatch } = useAuthContext();
    
      const signup = (email: string, password: string, nickname: string) => {
        setError(null);
        setIsPending(true);
    
        createUserWithEmailAndPassword(appAuth, email, password)
          .then((userCredential) => {
            const user = userCredential.user;
    
            updateProfile(user, { displayName: nickname })
              .then(() => {
                setError(null);
                setIsPending(false);
                dispatch({ type: 'LOGIN', payload: user });
              })
              .catch((err) => {
                setError(err.message);
                setIsPending(false);
              });
          })
          .catch((err) => {
            setError(signupError(err.code));
            setIsPending(false);
    
            if (err.code === errorCode[2]) {
              passwordRef.current?.focus();
            } else {
              emailRef.current?.focus();
            }
          });
      };
      return { error, isPending, signup, emailRef, passwordRef };
    };
    
    export default useSignup;
    • 회원가입 시 오류가 났을 경우 ref를 사용하여 focus를 시킴.

4. 모달

  • 키보드를 사용하여 웹사이트를 탐색하는 장애인들을 위해 키보드 포커스를 모달창 내의 첫 번째 포커스 가능한 요소로 이동해야 합니다. 또한 모달창을 닫을 때, 모달창 외부의 원래 요소에 포커스를 돌려줘야 합니다.
  • 키보드 사용자가 모달창을 닫기 위해 ESC 키를 누를 수 있도록 지원해야 합니다.
  • 모달창 내에서 포커스가 마지막 요소에 도달하면, 다시 첫 번째 요소로 포커스가 순환되어야 합니다.
  • 모달창과 같이 동적이며 상호 작용하는 요소는 스크린 리더에게는 보이지 않을 수 있습니다. WAI-ARIA 속성을 사용하면 모달창이 무엇인지, 열려 있거나 닫혔는지, 어떤 역할을 하는지에 대한 정보를 제공할 수 있어, 스크린 리더 사용자에게 모달창의 존재와 상태를 명확히 전달할 수 있습니다. 따라서 WAI-ARIA(Accessible Rich Internet Applications) 속성을 사용하여 모달창의 역할과 상태를 명시적으로 정의해야합니다.

1) 개선 전

  • esc키를 눌러도 모달창이 닫히지 않습니다.
  • 모달창 내에서 포커스가 마지막 요소에 도달했을 때, 한번더 tab키를 누를 경우 모달창 외부의 요소로 포커스가 갑니다. 모달창 첫번째 요소에서 shift + tab키를 누를경우 모달창 외부의 요소로 포커스가 갑니다.

ezgif-3-83a7446792 ezgif-3-344cb47047

  • 모달창을 닫을 때, 모달창 외부의 원래 요소에 포커스를 돌려주지 않아 처음부터 다시 웹사이트를 탐색해야합니다.

ezgif-3-b19a18e641

2) 개선 후

  • esc를 누르면 모달창이 닫힙니다. 또한 esc를 눌렀을 대 모달창 외부의 원래 요소에 포커스를 돌려주어, 탐색하던 요소 다음 요소를 계속하여 탐색할 수 있습니다.
    ezgif-3-b7c5ece040

  • 모달창 내에서 포커스가 마지막 요소에 도달했을 때, 한 번 더 tab키를 누를 경우 첫 번째 요소로 포커스가 갑니다. 모달창 첫 번째 요소에서 shift + tab키를 누를경우 마지막 요소로 포커스가 갑니다. 모달창 내부에서 포커스가 순환하고 있습니다. ezgif-3-36a54c0f68

3) 개선 코드

  • 마크업

    <div className={styles.overlay}>
      <div className={styles.dim} onClick={handleClose}></div>
      <article role="dialog" aria-modal="true" aria-labelledby={id} className={styles['modal-container']}>
        {children}
        <div className={styles['button-group']}>
          <Button ref={firstEl} type="button" onClick={handleClose} onKeyDown={handleFirstElKeyDown}>
            취소
          </Button>
          <Button type="button" onClick={handleConfirmClick}>
            확인
          </Button>
        </div>
        <button
          ref={lastEl}
          onKeyDown={handleLastElKeyDown}
          className={styles['btn-modal-close']}
          type={'button'}
          onClick={handleClose}
        >
          <span className="a11y-hidden">모달 창 닫기 버튼</span>
        </button>
      </article>
    </div>
    • role="dialog": 이 속성은 해당 요소가 모달 다이얼로그임을 나타내는 데 사용됩니다.
    • aria-modal="true": 이 속성은 모달 외부로 포커스가 제한되는 것을 명시적으로 전달합니다.
    • aria-labelledby: 모달의 제목을 스크린 리더 사용자에게 알립니다.
  • 키보드 사용자를 위한 코드

    // ./src/hooks/useModalKeyEvent.tsx
    import { RefObject, useEffect, useRef } from 'react';
    import { ButtonKeyboardEvent } from '../typings/eventTypes';
    
    const useModalKeyEvent = (isOpen: boolean, externalBtnRef: RefObject<HTMLButtonElement>, handleClose: () => void) => {
      const firstEl = useRef<HTMLButtonElement>(null);
      const lastEl = useRef<HTMLButtonElement>(null);
    
      const handleFirstElKeyDown = (e: ButtonKeyboardEvent) => {
        if (e.shiftKey && e.key === 'Tab') {
          e.preventDefault();
          if (lastEl.current) {
            lastEl.current.focus();
          }
        }
      };
    
      const handleLastElKeyDown = (e: ButtonKeyboardEvent) => {
        if (!e.shiftKey && e.key === 'Tab') {
          e.preventDefault();
          if (firstEl.current) {
            firstEl.current.focus();
          }
        }
      };
    
      useEffect(() => {
        const handleEscKeyDown = (e: KeyboardEvent): void => {
          if (e.key === 'Escape') {
            handleClose();
          }
        };
    
        if (isOpen) {
          document.addEventListener('keydown', handleEscKeyDown);
          if (firstEl.current) {
            firstEl.current.focus();
          }
        }
    
        return () => {
          document.removeEventListener('keydown', handleEscKeyDown);
          if (externalBtnRef.current) {
            externalBtnRef.current.focus();
          }
        };
      }, [isOpen]);
    
      return { handleFirstElKeyDown, handleLastElKeyDown, firstEl, lastEl };
    };
    
    export default useModalKeyEvent;
    • useModalKeyEvent hook을 통해 키보드 사용자를 위한 접근성 로직 코드를 컴포넌트와 분리하였습니다.
    • handleFirstElKeyDown: 모달 첫 번째 요소에서 shift + tab키를 눌렀을 때 모달 마지막 요소로 포커스가 가도록 하는 함수입니다.
    • handleLastElKeyDown: 모달 마지막 요소에서 tab키를 눌렀을 때 모달 첫 번째 요소로 포커스가 가도록 하는 함수입니다.
    • useEffect 내부
      • handleEscKeyDown: 모달창이 켜진 상태에서 ‘esc’버튼을 눌렀을 때 모달이 꺼지도록 구현하였습니다.
      • if절 내부: 모달이 켜지면 자동으로 첫번째 요소에 포커스가 가도록 구현하였습니다.
    • 훅 안에서 만든 함수들과 ref들을 return 하여 컴포넌트 내부에서 해당 함수와 ref를 사용할 수 있도록 구현하였습니다.

프로젝트 깃허브링크

profile
🙌꿈꾸는 프론트엔드 개발자 신은수입니당🙌

0개의 댓글