웹 표준에 맞는 시맨틱한 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 속성은 아래와 같이 마우스를 갖다대면 해당 값의 내용이 뜨는 추가적 장점이 있다.
- 비디오 등 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 |