본문 바로가기

웹 접근성 개선해보기

웹 접근성 개선해보기

 

웹 표준에 맞는 시맨틱한 HTML 코드 작성하기
그리고 웹 콘텐츠 접근성 지침 중 일부를 충족해보기 (WAI-ARIA 등)
스크린 리더를 활용하여 웹 접근성 개선해보기

 

스크린 리더 설치

 

시작하기 전에, 화면의 구성 요소를 읽어주는 스크린 리더를 설치해야 한다.

  • 스크린 리더를 사용해서 시각적 요소를 제대로 인지할 수 없는 환경에서 화면 구성을 어떻게 파악하게 되는지 직접 확인해볼 수 있다.
  • Chrome 확장 프로그램인 ChromeVox Screen Reader를 설치하여 사용한다.
  • 설치 후 확인하고자 하는 요소를 클릭하면 스크린 리더가 요소를 인식하여 읽어주게 된다.

 

 

  • Chrome창 우측 상단에 있는 더보기 버튼 → 도구 더보기 → 확장 프로그램 페이지에서 스크린 리더 활성화 및 비활성화를 할 수 있다.
  • 필요할 때에만 활성화하여 스크린 리더를 사용하는 것이 좋겠다.

 

 

 

1. Tab

 

 

  • 우선 데이터 파일을 따로 만들어 준다.
  • 필요한 곳에서 import해서 사용할 예정
statics/staticData.js
 const data = {
 
   tab: {
          tab1 : {
            title: "공지사항",
            content: ["휴가 기간 공지", "이벤트 당첨자 안내", "1주년 기념 이벤트"]
          },
          tab2 : {
            title: "상품페이지",
            content: ["2020년도 달력", "개구리 안대", "잉어 슈즈"]
          },
          tab3 : {
            title: "문의페이지",
            content: ["언제 출발해요", "결제가 안 돼요", "2020년도 달력 재입고 문의"]
          }
        }
        
  }

 

잘못된 예시
import { useState } from "react"
import data from "../static/staticData"

const Page04 = () => {
    const [currentTab1, setCurrentTab1] = useState(0)
    const [currentTab2, setCurrentTab2] = useState(0)
    const { tab } = data
    
    return (
    
            <section>
                <h2>예시 1</h2>
                <div class="tabContainer">
                
                    <div className="tabList">
                        <div className={currentTab1 === 0 ? "tab selected" : "tab"} onClick={()=>setCurrentTab1(0)}>
                        {tab.tab1.title}</div>
                        <div className={currentTab1 === 1 ? "tab selected" : "tab"} onClick={()=>setCurrentTab1(1)}>
                        {tab.tab2.title}</div>
                        <div className={currentTab1 === 2 ? "tab selected" : "tab"} onClick={()=>setCurrentTab1(2)}>
                        {tab.tab3.title}</div>
                    </div>
                    
                    <div className={currentTab1 === 0 ? "block" : "none"}>
                    {tab.tab1.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                    <div className={currentTab1 === 1 ? "block" : "none"}>
                    {tab.tab2.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                    <div className={currentTab1 === 2 ? "block" : "none"}>
                    {tab.tab3.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                
                </div>
            </section>
            
            )

 

잘못된 예시의 경우, 스크린 리더는 이렇게 읽어준다.

공지사항, 상품페이지, 문의페이지, 휴가기간 공지, 이벤트 당첨자 안내.....

 

  • 테이블 헤더만 쭉 읽어주고, 그 뒤에 바디 부분의 데이터를 순차적으로 읽어준다.
  • 표의 구조와 내용을 파악하고 이해하기는 쉽지 않다. 따라서 아래와 같이 바꿔준다.

 

좋은 예시
import { useState } from "react"
import data from "../static/staticData"

const Page04 = () => {
    const [currentTab1, setCurrentTab1] = useState(0)
    const [currentTab2, setCurrentTab2] = useState(0)
    const { tab } = data
    
    return (
    
            <section>
                <h2>예시 2</h2>
                <div class="tabContainer">
                
                    <div className="tabList">
                        <div>
                            <div role="tab" className={currentTab2 === 0 ? "tab selected" : "tab"} 
                            onClick={()=>setCurrentTab2(0)}>{tab.tab1.title}</div>
                            <div  role="tabpanel" className={`tabPanel${currentTab2 === 0 ? " block" : " none"}`}>
                            {tab.tab1.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                        </div>
                        <div>
                            <div role="tab" className={currentTab2 === 1 ? "tab selected" : "tab"}  
                            onClick={()=>setCurrentTab2(1)}>{tab.tab2.title}</div>
                            <div role="tabpanel" className={`tabPanel${currentTab2 === 1 ? " block one" : " none"}`}>
                            {tab.tab2.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                        </div>
                        <div>
                            <div role="tab" className={currentTab2 === 2 ? "tab selected" : "tab"}  
                            onClick={()=>setCurrentTab2(2)}>{tab.tab3.title}</div>
                            <div role="tabpanel"  className={`tabPanel${currentTab2 === 2 ? " block two" : " none"}`}>
                            {tab.tab3.content.map((el,idx)=> <li key={idx}>{el}</li>)}</div>
                        </div>
                    </div>
                    
                </div>
            </section>
            
            )

 

  • WAI-ARIA를 사용하여 각 컴포넌트에 맞는 역할(role)을 추가해주었다.

 

좋은 예시의 경우, 스크린 리더는 이렇게 읽어준다.

공지사항, 휴가 기간 공지, 이벤트 당첨자 안내, 1주년 기념 이벤트, 상품페이지...

 

  • 첫 번째 테이블 헤더를 읽은 후에, 그와 관련된 바디 데이터들을 다 읽는다. 그 후에, 두 번째 헤더를 읽고 끝날 때까지 반복된다.
  • 테이블이 어떤 구조와 내용을 담고 있는지 이해하기가 한결 수월해 졌다.

 

여기에 나온 예시가 정답은 아니다. 예를 들면 위의 '좋은 예시'의 경우에도 스타일링 할 때 비효율적이라는 단점이 있다.
HTML 구조의 차이가 정보 전달에 있어서 어떤 차이를 가져오는지만 확인하는 것이 목적이다.

 

2. 이미지로 만든 버튼 

 

나쁜 예시
<div className="iconButtonContainer">
  <button className="iconButton" ><img src={home} /></button>
  <button className="iconButton" ><img src={web} /></button>
  <button className="iconButton"><img src={mail} /></button>
</div>

 

1) 전체 섹션을 클릭했을 때 스크린리더: 

 

 

이미지 이미지 이미지

 

 

2) 버튼 하나만 클릭했을 때 스크린리더: 

 

 

이미지 버튼

 

좋은 예시
<div className="iconButtonContainer">
  <button className="iconButton" aria-label="홈"><img src={home} /></button>
  <button className="iconButton" aria-label="웹"><img src={web} /></button>
  <button className="iconButton" aria-label="메일"><img src={mail} /></button>
</div>

 

1) 전체 섹션을 클릭했을 때 스크린리더: 

 

 

홈 웹 메일

 

2) 버튼 하나만 클릭했을 때 스크린리더: 

 

 

메일 버튼

 

3. input 

참고로 input의 type을 password로 바꿀 경우, 스크린 리더도 인풋에 적히는 값을 읽지 않는다.

 

나쁜 예시 1
                <div className="inputContainer">
                    <input type="text" placeholder="아이디" />
                    <input type="text" placeholder="비밀번호" />
                </div>
  • <input>요소만 있으면 무엇을 입력하라는 의미인지 알 수 없다.
  • 레이블을 꼭 작성해주어야 한다.
  • 스크린 리더는 이렇게만 읽어 준다.
텍스트 수정

 

나쁜 예시 2
                <div className="inputContainer">
                    <input type="text" placeholder="아이디" />
                    <input type="text" placeholder="비밀번호" />
                </div>
  • <input>요소에 placeholder를 사용하더라도 레이블을 작성해줘야 한다. 
  • placeholder는 레이블을 대체할 수 없다.
  • placeholder는 내용을 입력하는 순간 사라지기 때문에 일부 스크린 리더는 읽지 못하게 된다.
  • 스크린 리더는 이렇게 읽어준다. (해당 스크린 리더는 placeholder가 사라져도 읽어준다.)

 

아이디 비밀번호

 

아이디 텍스트 수정

 

아이디 도유 텍스트수정

 

 

좋은 예시 (label 사용)
                <div className="inputContainer">
                        <label for="아이디">아이디</label>
                        <input id="아이디" type="text" />
                        <label for="비밀번호">비밀번호</label>
                        <input id="비밀번호" type="text" />
                </div>
  • <input>요소 밖에 무엇을 입력해야하는지 알려주는 요소가 있더라도, <label>요소로 레이블을 작성해 <input>요소와 연결해준다.
  • <input>요소에서 id를 작성하고,<label>요소의 for 속성으로 연결할 <input>요소의 id를 작성한다.
  • 스크린 리더는 이렇게 읽어준다.

 

아이디 아이디 비밀번호 비밀버번호

 

아이디 텍스트 수정

 

아이디 도유 텍스트 수정

 

좋은 예시 (title 속성 사용)
<input type="text" title="아이디" placeholder="아이디" />​
  • title 속성은 아래와 같이 마우스를 갖다대면 해당 값의 내용이 뜨는 추가적 장점이 있다.

title 속성

  • 비디오 등 alt 속성을 쓸 수 없을 때도 title을 쓰면 좋다.

 

최후의 수단 예시 (aria-label)
               <div className="inputContainer">
                        <input type="text" aria-label="아이디" placeholder="아이디를 입력하세요." />
               </div>
  • WAI-ARIA의 aria-label 속성을 사용할 수도 있다.
  • 단, WAI-ARIA의 경우 꼭 필요한 경우가 아니라면 사용하지 않는 것이 좋다.
  • 다른 HTML 속성이나 요소로 대체 가능한 경우에는 해당 속성이나 요소를 우선적으로 사용한다.
  • 스크린 리더는 이렇게 읽어준다. (두 번째에서 아이디가 입력이 되고 난 후, placeholder 값은 읽어주지 않는 모습을 볼 수 있다.)

 

아이디 with hint 아이디를 입력하세요

 

아이디 도유 텍스트수정

 

4. table

비장애인은 표를 보면 그 구조를 인식할 수 있지만, 시각 장애가 있는 경우에는 내용을 들으면서 그 구조를 파악해야만 한다. 따라서 듣기만해도 표의 구조, 내용을 이해하기 쉽게 구성해야 한다. 표의 구조가 복잡할 경우, 최대한 간소화하거나 scope, id, headers 속성을 사용하여 작성한다.

 

좋은 예시 1
  • 테이블 요소 안에 <caption> 요소를 사용해서 표에 제목을 제공해줬다.
  •  표의 셀은 제목 셀과 데이터 셀이 구분되도록 구성했다.
    • 제목 셀은 <th>
    • 데이터 셀은 <td>
           <section>
                <h2>좋은 예시 1</h2>
                <table>
                    <caption>테이블 요소의 종류</caption>
                    <thead>
                        <tr>
                            <th>요소</th>
                            <th>역할</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>{`<table>`}</td>
                            <td>표를 생성</td>
                        </tr>
                        <tr>
                            <td>{`<caption>`}</td>
                            <td>표의 제목</td>
                        </tr>
                        <tr>
                            <td>{`<thead>`}</td>
                            <td>(optional) 열의 제목을 묶음</td>
                        </tr>
                        <tr>
                            <td>{`<tbody>`}</td>
                            <td>(optional) 표의 내용을 묶음</td>
                        </tr>
                        <tr>
                            <td>{`<th>`}</td>
                            <td>열의 제목</td>
                        </tr>
                        <tr>
                            <td>{`<tr>`}</td>
                            <td>table row의 약자. 열을 생성</td>
                        </tr>
                        <tr>
                            <td>{`<td>`}</td>
                            <td>table data의 약자. 행을 생성</td>
                        </tr>
                    </tbody>
                </table>
            </section>

 

 

아래에 나오는 좋은 예시 2, 3처럼 테이블을 작성하면, 표 구성을 파악하기 더 쉬워진다.
무료 스크린리더는 속성 작성 전과 차이 없이 표를 읽지만, 일부 유료 스크린 리더는 표를 다음과 같이 읽게 된다.

속성 작성 전 : 상품명 → 판매가 → 판매량 → 판매총액 → 2020년 달력 → 12,000원 → 6개 → 72,000원 → 개구리 안대 → ...

속성 작성 후 : 상품명 → 2020년 달력 → 판매가 → 12,000원 → 판매량 → 6개 → 판매총액 → 72,000원 → 상품명 → 개구리 안대 → ...

 

좋은 예시 2
  • 비교적 복잡한 구성의 표에서 scope 속성을 사용하여 행과 열의 제목이 무엇인지 표시해주었다.
    • (row)는 행의 제목
    • (col)은 열의 제목
             <section>
                <h2>좋은 예시 2</h2>
                <table>
                    <caption>Cmarket 판매총액</caption>
                    <thead>
                        <tr>
                            <th scope="col">(col)<br/>상품명</th>
                            <th scope="col">(col)<br/>판매가</th>
                            <th scope="col">(col)<br/>판매량</th>
                            <th scope="col">(col)<br/>판매총액</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td scope="row">(row)<br/>2020년 달력</td>
                            <td>12,000원</td>
                            <td>6개</td>
                            <td>72,000원</td>
                        </tr>
                        <tr>
                            <td scope="row">(row)<br/>개구리 안대</td>
                            <td>2,900원</td>
                            <td>4개</td>
                            <td>11,600원</td>
                        </tr>
                        <tr>
                            <td scope="row">(row)<br/>잉어 슈즈</td>
                            <td>3,900원</td>
                            <td>7개</td>
                            <td>27,300원</td>
                        </tr>
                        <tr>
                            <td scope="row">(row)<br/>노른자 분리기</td>
                            <td>9,900원</td>
                            <td>5개</td>
                            <td>49,500원</td>
                        </tr>
                    </tbody>
                </table>
            </section>

 

좋은 예시 3
  • 비교적 복잡한 구성의 표에서 id와 headers를 사용해 데이터 구조를 표시해주었다.
            <section>
                <h2>좋은 예시 3</h2>
                <table>
                    <caption>Cmarket 판매총액</caption>
                    <thead>
                        <tr>
                            <th id="A">상품명<br/>(A)</th>
                            <th id="B">판매가<br/>(B)</th>
                            <th id="C">판매량<br/>(C)</th>
                            <th id="D">판매총액<br/>(D)</th>
                        </tr>
                        </thead>
                    <tbody>
                        <tr>
                            <td id="a">2020년 달력<br/>(a)</td>
                            <td headers="B a">12,000원<br/>(B a)</td>
                            <td headers="C a">6개<br/>(C a)</td>
                            <td headers="D a">72,000원<br/>(D a)</td>
                        </tr>
                        <tr>
                            <td id="b">개구리 안대<br/>(b)</td>
                            <td headers="B b">2,900원<br/>(B b)</td>
                            <td headers="C b">4개<br/>(C b)</td>
                            <td headers="D b">11,600원<br/>(D b)</td>
                        </tr>
                        <tr>
                            <td id="c">잉어 슈즈<br/>(c)</td>
                            <td headers="B c">3,900원<br/>(B c)</td>
                            <td headers="C c">7개<br/>(C c)</td>
                            <td headers="D c">27,300원<br/>(D c)</td>
                        </tr>
                        <tr>
                            <td id="d">노른자 분리기<br/>(d)</td>
                            <td headers="B d">9,900원<br/>(B d)</td>
                            <td headers="C d">5개<br/>(C d)</td>
                            <td headers="D d">49,500원<br/>(D d)</td>
                        </tr>
                    </tbody>
                </table>

 

좋은 예시 3이 필요한 경우는 아래와 같이 여러 셀에 걸쳐 데이터가 있는 경우이다.

 

   
     
     
 
     
728x90

'FE > UI & UX' 카테고리의 다른 글

디자인 시스템  (0) 2023.05.24
웹 접근성에 맞게 Cmarket 리팩토링하기  (0) 2023.04.29
웹 접근성 & 웹 콘텐츠 접근성 지침  (0) 2023.04.26
SEO  (0) 2023.04.26
웹표준  (0) 2023.04.26
⬆︎