Frontend/HTML, CSS

[CSS] Learn Accessibility by Building a Quiz - 접근성을 고려한 홈페이지 만들기

dev6v6 2025. 2. 19. 15:45

https://www.freecodecamp.org/learn/2022/responsive-web-design/learn-accessibility-by-building-a-quiz/step-1

 

https://www.freecodecamp.org/learn/2022/responsive-web-design/learn-accessibility-by-building-a-quiz/step-1

 

www.freecodecamp.org

 

먼저 결과물

 

 

접근성(accessibility)은 모든 사람, 심지어 장애가 있는 사람들도 웹페이지를 쉽게 사용할 수 있도록 만드는 것입니다.

이 과정에서는 퀴즈 웹페이지를 구축합니다. 키보드 단축키, ARIA 속성, 디자인 모범 사례와 같은 접근성 도구를 배웁니다.

 

접근성과 시맨틱 HTML이 중요한 이유

  • 웹사이트는 모든 사용자가 쉽게 탐색할 수 있어야 한다.
  • 시각 장애인이나 화면 리더(screen reader)를 사용하는 사람들에게 페이지 구조를 올바르게 전달하는 것이 중요함.
  • 그래서 의미가 있는(시맨틱) HTML 태그를 사용하면 접근성이 향상됨.

 

header와 main의 역할

<header> - 페이지 소개 및 내비게이션 역할

<header>
  <h1>Welcome to My Website</h1>
  <nav>
    <ul>
      <li><a href="#home">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </nav>
</header>


내비게이션(<nav>)을 포함할 수 있음
✔ 화면 리더가 이 요소를 인식하면 "이 부분이 페이지의 머리글입니다."라고 안내

<main> - 페이지의 핵심 콘텐츠

<main>
  <h2>About This Site</h2>
  <p>This site is all about learning HTML and accessibility.</p>
</main>

 

<main>은 본문(content) 영역
한 페이지에 한 개만 사용해야 함 (화면 리더가 "메인 콘텐츠"로 인식)
헤더, 사이드바, 푸터 등의 보조적인 부분을 제외한 주요 내용을 포함

왜 <div> 대신 시맨틱 태그를 써야 할까?

<div class="header"> <!-- 의미가 불명확 -->
  <h1>My Website</h1>
</div>

<div class="main-content"> <!-- 의미가 불명확 -->
  <p>Welcome to my website!</p>
</div>

 

div class(위)를 이용한 걸 시맨틱 태그(아래)를 이용하면

<header>
  <h1>My Website</h1>
</header>

<main>
  <p>Welcome to my website!</p>
</main>

 

✔ 화면 리더가 시맨틱 태그를 인식하여 더 정확한 안내 가능
검색 엔진 최적화(SEO)에도 유리 (검색엔진이 페이지 구조를 이해하기 쉬움)

 

 

aria-labelledby와 접근성을 위한 section 구조

 

aria-labelledby란?

  • 웹 접근성을 위해 화면 리더(screen reader)특정 영역의 목적을 이해할 수 있도록 "라벨(이름)"을 제공하는 속성
  • 특정 요소의 id를 참조하여 그 내용을 라벨로 사용

section과 aria-labelledby를 사용하는 이유

  • <section> 태그는 페이지를 의미 있는 영역(Region)으로 나누는 역할
  • 모든 Region(role=region) 요소는 라벨(label)이 필수
  • 라벨을 추가하는 방법:
    1. <section> 내부에 <h2> 같은 제목을 넣고
    2. 해당 제목의 id를 aria-labelledby 속성에 연결 -> h2를 이 섹션의 이름으로 사용
  •  <label for=""><input id=""> 맞췄던 거처럼 <section aria-labelledby=""><h2 id=""> 를 맞춰줘야 한다.

예제 코드

<section aria-labelledby="student-info">
  <h2 id="student-info">Student Information</h2>
  <p>Name: John Doe</p>
  <p>Age: 20</p>
</section>

<section aria-labelledby="html-questions">
  <h2 id="html-questions">HTML Questions</h2>
  <p>What does the `<div>` element do?</p>
</section>

<section aria-labelledby="css-questions">
  <h2 id="css-questions">CSS Questions</h2>
  <p>What is the difference between `relative` and `absolute` positioning?</p>
</section>

 

        <section role="region" aria-labelledby="student-info">
          <h2 id="student-info">Student Info</h2>
        </section>

 

role="region" 은 언제 써줘야 하나?

  • <section>은 시맨틱 태그라서 특별한 role 없이도 자동으로 "region" 역할을 가짐. 
  • <section role="region">은 불필요!
  • <div> 같은 비시맨틱 요소를 쓸 때만 role="region"을 사용

aria-labelledby는 접근성을 위한 필수 속성

  • 시각 장애인이 화면 리더를 사용하면 이 영역이 무엇인지 설명을 들을 수 있음
  • <h2>만 있어도 시맨틱 HTML을 활용하는 거지만,
    aria-labelledby를 추가하면 접근성을 더욱 강화
  • 예를 들어, 화면 리더는 이렇게 읽음:"Student Information, Section 시작. Name: John Doe, Age: 20"

 

Placeholder 대신 Label을 사용하는 것이 접근성에 더 좋다.

 

Placeholder 의 문제점

  1. 사용자가 값을 입력한 후 placeholder가 사라짐
    • 무엇을 입력해야 하는지 잊을 수 있음
  2. 일부 사용자는 placeholder를 입력된 값으로 착각
    • 특히, 색 대비가 낮거나 흐리게 표시되면 입력된 값처럼 보일 위험이 있음.
  3. 스크린 리더(screen reader)가 올바르게 읽지 않을 수도 있음
    • Label은 확실히 읽어주지만, placeholder는 잘못 읽거나 무시될 가능성이 있음.

Best Practice: Label 사용

<form>
  <label for="name">Name:</label>
  <input type="text" id="name">
</form>

 

명확한 label을 제공하면 시각 장애가 있는 사용자도 쉽게 이해할 수 있음.
✔ for="id값"을 사용해 label과 input을 연결하면 클릭 시 input이 활성화됨

 

✅ label을 사용하여 명확한 입력 필드를 제공하는 것이 접근성(Best Practice)에 좋음
✅ placeholder는 부가적인 힌트로만 사용하고, 주요 설명은 label로 제공

 

 

시각 장애인을 위한 숨겨진 텍스트 추가하기 (.sr-only 클래스 활용)

 

문제점: 질문 번호만으로는 충분한 정보가 제공되지 않음

<h3>1.</h3>
<p>What is HTML?</p>

 

  • "1."이라는 번호만으로는 질문의 내용을 알기 어려움.
  • 특히, 스크린 리더를 사용하는 사용자는 이 숫자만 들으면 질문과의 연결을 이해하기 어려움.

 

해결: span.sr-only 추가

  • sr-only 클래스를 사용하면 화면에는 보이지 않지만, 스크린 리더가 읽을 수 있도록 숨겨진 텍스트를 추가할 수 있음.
<h3>1. <span class="sr-only">Question:</span></h3>
<p>What is HTML?</p>

 

이렇게 하면 스크린 리더는 "1. Question"이라고 읽어줌!
시각적으로는 여전히 "1."만 보이지만, 접근성이 개선됨

 

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

 

 CSS sr-only 적용 필수: 단순히 display: none; 하면 스크린 리더도 읽지 못하기 때문에 올바른 숨김 스타일 적용 필요

 

 

label에 중첩된 input은 for="", id="" 로 연결해주는 게 필수는 아니지만 명시적으로 연결하는 것이 좋다.

 

마지막 Semantic HTML 요소: <footer>와 <address>

  1. <footer> 요소
    • 페이지와 관련된 콘텐츠를 담는 컨테이너 역할
    • 일반적으로 저작권 정보, 사이트 맵, 관련 링크 등을 포함
  2. <address> 요소
    • 페이지 작성자의 연락처 정보를 담는 컨테이너
    • 이메일, 전화번호, 물리적 주소 등의 정보 포함

➡️ <footer>는 페이지 전체 또는 섹션과 관련된 정보를 제공하며, <address>는 페이지 작성자의 연락처 정보를 담는 데 사용됨.

 

Header 상단에 고정하기

header {
  position: fixed;
  top: 0;
}

 

prefers-reduced-motion과 @media at-rule 사용법

 

1️⃣ 모션 기반 애니메이션과 사용자 접근성

일부 사용자는 어지럼증등의 이유로 화면에서 움직이는 요소가 불편할 수 있다.
-> 특히 자동 스크롤, 애니메이션 효과 같은 모션 기반 UI

이를 고려해, CSS에서는 사용자의 설정(reduce , no-preference )에 따라 애니메이션을 조정하는 방법을 제공함.
이때 사용하는 것이 @media (prefers-reduced-motion) 미디어 쿼리.

 

2️⃣ prefers-reduced-motion 미디어 쿼리

이 미디어 쿼리는 사용자가 운영체제(OS)에서 "모션 줄이기" 설정을 활성화했는지 여부를 확인하는 기능

  • reduce → 사용자가 모션을 줄이도록 설정했음 (애니메이션 최소화)
  • no-preference → 사용자가 특별한 설정을 하지 않음 (기본 애니메이션 적용 가능)

 

👉 scroll-behavior: smooth; 스타일을 적용할 때, 사용자가 모션 줄이기(reduce)를 설정한 경우에는 적용하지 않아야 함.
👉 대신 모션 줄이기 설정이 없는(no-preference) 경우에만 scroll-behavior: smooth;를 적용해야 함.

 

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}
  • @media (prefers-reduced-motion: no-preference) → 사용자가 모션 줄이기 설정을 하지 않은 경우에만 아래 스타일 적용
  • html { scroll-behavior: smooth; } → 부드러운 스크롤 적용

 

prefers-reduced-motion 미디어 쿼리를 사용하면, 모든 사용자에게 더 나은 접근성을 제공할 수 있다

멀미 나는 사람은 편리하지만 멀미나는 모션이 안들어갈 거고 

멀미 안나는 사람은 모션이 들어가서 훨씬 편리한 기능을 쓸 수 있다.

 

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="freeCodeCamp Accessibility Quiz practice project" />
    <title>Accessibility Quiz</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header>
      <img id="logo" alt="freeCodeCamp" src="https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg">
      <h1>HTML/CSS Quiz</h1>
      <nav>
        <ul>
          <li><a href="#student-info">INFO</a></li>
          <li><a href="#html-questions">HTML</a></li>
          <li><a href="#css-questions">CSS</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <form method="post" action="https://freecodecamp.org/practice-project/accessibility-quiz">
        <section role="region" aria-labelledby="student-info">
          <h2 id="student-info">Student Info</h2>
          <div class="info">
            <label for="student-name">Name:</label>
            <input type="text" name="student-name" id="student-name" />
          </div>
          <div class="info">
            <label for="student-email">Email:</label>
            <input type="email" name="student-email" id="student-email" />
          </div>
          <div class="info">
            <label for="birth-date">Date of Birth:</label>
            <input type="date" name="birth-date" id="birth-date" />
          </div>
        </section>
        <section role="region" aria-labelledby="html-questions">
          <h2 id="html-questions">HTML</h2>
          <div class="question-block">
            <h3><span class="sr-only">Question</span>1</h3>
            <fieldset class="question" name="html-question-one">
              <legend>
                The legend element represents a caption for the content of its
                parent fieldset element
              </legend>
              <ul class="answers-list">
                <li>
                  <label for="q1-a1">
                    <input type="radio" id="q1-a1" name="q1" value="true" />
                    True
                  </label>
                </li>
                <li>
                  <label for="q1-a2">
                    <input type="radio" id="q1-a2" name="q1" value="false" />
                    False
                  </label>
                </li>
              </ul>
            </fieldset>
          </div>
          <div class="question-block">
            <h3><span class="sr-only">Question</span>2</h3>
            <fieldset class="question" name="html-question-two">
              <legend>
                A label element nesting an input element is required to have a
                for attribute with the same value as the input's id
              </legend>
              <ul class="answers-list">
                <li>
                  <label for="q2-a1">
                    <input type="radio" id="q2-a1" name="q2" value="true" />
                    True
                  </label>
                </li>
                <li>
                  <label for="q2-a2">
                    <input type="radio" id="q2-a2" name="q2" value="false" />
                    False
                  </label>
                </li>
              </ul>
            </fieldset>
          </div>
        </section>
        <section role="region" aria-labelledby="css-questions">
          <h2 id="css-questions">CSS</h2>
          <div class="formrow">
            <div class="question-block">
              <label for="selector">Can the CSS margin property accept negative values?</label>
            </div>
            <div class="answer">
              <select name="selector" id="selector" required>
                <option value="">Select an option</option>
                <option value="yes">Yes</option>
                <option value="no">No</option>
              </select>
            </div>
            <div class="question-block">
              <label for="css-textarea">Do you have any questions:</label>
            </div>
            <div class="answer">
              <textarea id="css-textarea" name="css-questions" rows="5" cols="24"></textarea>
            </div>
          </div>
        </section>
        <button type="submit">Send</button>
      </form>
    </main>
    <footer>
      <address>
        <a href="https://freecodecamp.org">freeCodeCamp</a><br />
        San Francisco<br />
        California<br />
        USA
      </address>
    </footer>
  </body>
</html>

 

styles.css

@media (prefers-reduced-motion: no-preference) {
  * {
  scroll-behavior: smooth;
  }
}

body {
  background: #f5f6f7;
  color: #1b1b32;
  font-family: Helvetica;
  margin: 0;
}

header {
  width: 100%;
  height: 50px;
  background-color: #1b1b32;
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: fixed;
  top: 0;
}

#logo {
  width: max(10rem, 18vw);
  background-color: #0a0a23;
  aspect-ratio: 35 / 4;
  padding: 0.4rem;
}

h1 {
  color: #f1be32;
  font-size: min(5vw, 1.2em);
  text-align: center;
}

nav {
  width: 50%;
  max-width: 300px;
  height: 50px;
}

nav > ul {
  display: flex;
  justify-content: space-evenly;
  flex-wrap: wrap;
  align-items: center;
  padding-inline-start: 0;
  margin-block: 0;
  height: 100%;
}

nav > ul > li {
  color: #dfdfe2;
  margin: 0 0.2rem;
  padding: 0.2rem;
  display: block;
}

nav > ul > li:hover {
  background-color: #dfdfe2;
  color: #1b1b32;
  cursor: pointer;
}

li > a {
  color: inherit;
  text-decoration: none;
}

main {
  padding-top: 50px;
}

section {
  width: 80%;
  margin: 0 auto 10px auto;
  max-width: 600px;
}

h1,
h2 {
  font-family: Verdana, Tahoma;
}

h2 {
  border-bottom: 4px solid #dfdfe2;
  margin-top: 0px;
  padding-top: 60px;
}

.info {
  padding: 10px 0 0 5px;
}

.formrow {
  margin-top: 30px;
  padding: 0px 15px;
}

input {
  font-size: 1rem;
}

.info label, .info input {
  display: inline-block;
}

.info input {
  width: 50%;
  text-align: left;
}

.info label {
  width: 10%;
  min-width: 55px;
  text-align: right;
}

.question-block {
  text-align: left;
  display: block;
  width: 100%;
  margin-top: 20px;
  padding-top: 5px;
}

h3 {
  margin-top: 5px;
  padding-left: 15px;
  font-size: 1.375rem;
}

h3::before {
  content: "Question #";
}

.question {
  border: none;
  padding-bottom: 0;
}

.answers-list {
  list-style: none;
  padding: 0;
}

button {
  display: block;
  margin: 40px auto;
  width: 40%;
  padding: 15px;
  font-size: 1.438rem;
  background: #d0d0d5;
  border: 3px solid #3b3b4f;
}

footer {
  background-color: #2a2a40;
  display: flex;
  justify-content: center;
}

footer,
footer a {
  color: #dfdfe2;
}

address {
  text-align: center;
  padding: 0.3em;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
}