리팩토링-웹 접근성-요소에 대해 더 자세히 알려주기-aria 속성 설정

김철준·5일 전
0

리팩토링

목록 보기
7/9

이번에는 웹 접근성 관련한 리팩토링을 진행해볼까합니다.
웹 접근성 향상을 위해서는 aria 속성을 태그에 적절히 활용할 수 있는데요.

ARIA(Accessible Rich Internet Applications)

ARIA(Accessible Rich Internet Applications) 속성은 웹 접근성을 개선하기 위해 HTML 요소에 추가하는 속성으로, 주로 화면 읽기 도구(screen reader)와 같은 보조 기술이 웹 콘텐츠를 올바르게 해석하고 사용자에게 전달하도록 돕습니다.

import "prismjs/themes/prism.css";

import sanitize from "@/app/_utils/function/sanitize";
import React from "react";

// 퀴즈 해설 컨텐츠
function QuizExplanationContent({
    content,
}: {
    content: string;
}) {
    return (
        <div
            className={"prose"}
            aria-live={"polite"} // <== 요기
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        />
    );
}

export default QuizExplanationContent;

ARIA 속성은 웹 접근성에 있어 주요한 역할을 담당하고 있습니다. Lighthouse 접근성 항목을 측정만 해보더라도 많은 측정항목이 aria 속성에 관련되있는 것을 확인할 수 있죠.

ARIA 속성은 사용자에게 해당 요소가 어떠한 역할을 하는지 알려주는 것이 제일 중요한 역할인데요.

주요 역할은 다음과 같습니다.

ARIA 속성의 주요 역할

  • 역할(Role): 요소가 어떤 역할을 하는지 정의합니다.
    예: 버튼, 대화 상자, 메뉴 등.
    <div role="button">Click me</div>

  • 상태(State): 현재 상태 정보를 제공합니다.
    예: 활성화 여부, 선택 상태.
    <button aria-pressed="true">Toggle</button> (활성화된 토글 버튼)

  • 속성(Properties):요소와 관련된 추가 정보를 제공합니다.
    예: 이름, 설명, 관계 등.
    <input aria-label="Search" /> (검색 상자에 라벨 추가)

    주요 속성

    주로 사용되는 속성은 다음과 같습니다.

role

요소의 기능을 명시적으로 정의합니다.
예: <div role="alert">Error!</div>

aria-label

요소의 대체 라벨을 제공합니다.
예: <button aria-label="Submit Form"></button>

aria-labelledby

특정 ID를 참조해 요소의 라벨을 제공합니다.
예:

<h1 id="title">Form Title</h1>
<form aria-labelledby="title">

aria-live

동적인 콘텐츠 업데이트 시 스크린 리더 같은 보조기기가 사용자에게 적절한 정보를 전달할 수 있도록 돕는 역할을 합니다.

aria-live는 요소가 동적으로 업데이트될 때 스크린 리더가 이를 감지하고 사용자에게 읽어주는 방식을 지정합니다.

<div aria-live="polite">
  <p id="message"></p>
</div>

aria-describedby

특정 ID를 참조해 요소의 추가 설명을 제공합니다.
예:

<button aria-describedby="help-text">Submit</button>
<span id="help-text">Click to submit the form.</span>

aria-hidden

요소를 화면 읽기 도구에서 숨깁니다.
예: <div aria-hidden="true">Hidden Content</div>

aria-live

동적으로 변경되는 콘텐츠를 알립니다.
예: <div aria-live="polite">New message received.</div>

aria-expanded

요소가 확장되었는지 여부를 나타냅니다.
예: <button aria-expanded="false">Toggle Menu</button>

aria-pressed

요소가 눌린 상태인지 나타냅니다.
예: <button aria-pressed="true">Bold</button>

위 속성외에도 여러가지가 있으니 MDN에서 살펴보면 될 것 같습니다.

aria 속성 언제 필요할까?

aria 속성을 프로젝트 점검 및 적용하기에 앞서, aria 속성을 어떠한 상황에 적용해야할까요?

스크린 리더가 HTML 요소를 읽었을 때, 요소가 표현하려는 컨텐츠 관련하여 정보가 부족한 경우나 알 수 없을 때 사용하면 좋을 것 같습니다.

이러한 이유 때문에 시맨틱 태그 즉, 각 컨텐츠에 맞는 적절한 HTML 태그를 설정하는 것이 중요합니다. MDN에서도 aria 속성을 적용하기에 앞서 적절한 HTML 태그를 설정 확인 여부를 권장하고 있어요.

지난번에는 프로젝트의 모든 컴포넌트를 점검하여 시맨틱 태그로 변환해봤는데요. 그렇기 때문에 aria 속성을 적용할 일은 많이 없다고 예측됩니다.

그렇다면 한번 적용해보도록 하겠습니다.

프로젝트 aria 속성 적용

aria 속성을 적용하기 전과 후의 코드를 보여드리도록 하겠습니다.
그리고 어떠한 HTML 태그를 설정되어있을 때에는 aria 속성을 굳이 설정할 필요없는지에 대한 코드와 설명을 첨부하겠습니다.

우선 굳이 aria 속성을 적용하지 않아도 되는 경우를 살펴보겠습니다.
위에서부터 아래로 컴포넌트를 확인해보겠습니다.

굳이 aria 속성을 적용할 필요가 없는 경우

import HomeInnerContainer from "@/app/_home_components/homeInnerContainer";
import HomeLink from "@/app/_home_components/homeLink";
import HomeOuterContainer from "@/app/_home_components/homeOuterContainer";
import HomeSubTitle from "@/app/_home_components/homeSubTitle";
import HomeTitle from "@/app/_home_components/homeTitle";

/**
 * 메인 페이지
 * SSG
 */
export const dynamic = "force-static";

export default function Home() {
    return (
        <HomeOuterContainer>
            {/* 내부 카피 컨텐츠  */}
            <HomeInnerContainer>
                {/* 메인 타이틀 */}
                <HomeTitle
                    title={"개발자들의 아지트, 코아"}
                />
                {/* 부제목 */}
                <HomeSubTitle
                    subTitle={
                        "퀴즈로 실력을 키우고, 함께 성장하세요."
                    }
                />
                {/* 메인 링크  */}
                <HomeLink />
            </HomeInnerContainer>
        </HomeOuterContainer>
    );
}

HomeOuterContainer 컴포넌트는 레이아웃 용도니 넘어가도록 하겠습니다.

import React from "react";

// 메인 내부 컨테이너
function HomeInnerContainer({ children }: { children: React.ReactNode }) {
    return (
        <section
            className={"flex justify-center items-center flex-col gap-[40px]"}>
            {children}
        </section>
    );
}

export default HomeInnerContainer;

HomeInnerContainer 컴포넌트는 내부 카피들을 컨텐츠를 감싸는 역할입니다. 내부 카피와 메인 서비스로 이동할 수 있는 링크를 감싸고 있는 하나의 섹션으로 구분할 수 있기에 section태그로 설정하였습니다.

이는 적절한 HTML 태그로 설정했기에 굳이 aria 속성을 적용할 필요가 없다고 판단하였습니다.

import React from "react";

// 메인 타이틀
function HomeTitle({ title }: { title: string }) {
    return (
        <h1
            className={
                "lg:text-headline2 md:text-headline3 sm:text-headline3 text-center"
            }>
            {title}
        </h1>
    );
}

export default HomeTitle;

대문 타이틀 카피입니다. 이 또한 heading1 태그로 표현하였고 내부 텍스트의 내용이 서비스의 내용을 잘 나타내주고 있기에 aria 속성으로 부가적인 정보를 줄 필요가 없어보이네요.

import React from 'react';

// 메인 부제
function HomeSubTitle({
    subTitle
                         }:{
    subTitle: string
}) {
    return (
        <h2
            className={"lg:text-headline3 md:text-title2Bold sm:text-title2Bold"}
        >
            {subTitle}
        </h2>
    );
}

export default HomeSubTitle;

위는 부제목 카피이며 heading2태그로 표현하고 내부 텍스트가 어떠한 내용인지 잘 설명해주고 있기에 이 또한 aria 속성을 덧붙힐 필요가 없어보입니다.

import PrimaryLink from "@/app/_components/link/primaryLink";
import PATHS from "@/app/_constants/paths";
import React from 'react';

// 메인 링크
function HomeLink() {
    return (
        <PrimaryLink
            className={"!w-[130px] !h-[42px]"}
            href={`/${PATHS.QUIZ}`}
        >
            퀴즈 풀어보기
        </PrimaryLink>
    );
}

export default HomeLink;

퀴즈 서비스로 넘어가는 링크입니다.

스크린 리더가 a 태그를 읽을 때, 링크라는 것을 들려주며 내부 텍스트 또한 퀴즈 풀어보기로 표현함으로써 버튼을 누르면 퀴즈가 나올 것을 기대할 수 있으므로 굳이 aria 속성을 설정할 필요가 없었습니다.

위와 같이 적절한 태그와 내부 텍스트가 내용을 잘 설명해주고 있다면 굳이 aria 속성을 지정할 필요가 없습니다.

그럼 다음으로 aria 속성을 적용하면 좋을 요소들을 살펴보고 적용해보도록 하겠습니다

aria 속성 적용

"use server"

import QuizAnswerForm from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/quizAnswerForm";
import QuizDetailsManager from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizDetailsManager";
import QuizContent from "@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizContent";
import QuizQuestion from "@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizQuestion";
import QuizTitle from "@/app/(page)/quiz/(page)/[detailUrl]/_components/server/quizTitle";

import {QuizItem} from "@/app/services/quiz/types";
import React from 'react';

// 퀴즈 상세 컴포넌트
const QuizDetails = ({
                         quizData
                     }:{quizData:QuizItem}) => {

    return (
        <QuizDetailsManager>
            {/*퀴즈 제목*/}
            <QuizTitle
                title={quizData.metaTitle}
            />
            {/*퀴즈 문제*/}
           <QuizQuestion
               question={quizData.title}
           />
            {/*퀴즈내용*/}
            <QuizContent
                content={quizData.content}
            />
            {/*퀴즈 답안 폼*/}
            <QuizAnswerForm
                quizId={quizData.quizId}
                quizType={quizData.type}
                quizMultipleChoiceContents={quizData.multipleChoiceContents}
                />
        </QuizDetailsManager>
    );
};

export default QuizDetails;

위는 퀴즈 상세 컴포넌트이며 퀴즈 관련 세부 컨텐츠를 담고 있습니다. 퀴즈를 풀 때마다 다른 퀴즈 페이지로 이동합니다. 즉, 페이지가 동적으로 계속해서 변경된다는 건데요.

때문에 퀴즈를 채점하고 다음 문제로 이동할 때마다, 스크린리더가 페이지의 변경 내용을 감지하여 퀴즈 컨텐츠들을 감지할 필요가 있습니다.

이를 위해 퀴즈 컨텐츠에 aria-live 속성을 적용해보려고 합니다.

aria-live 속성 적용

다음은 퀴즈 제목,문제 설명,내용 컴포넌트입니다.

"use server";

import React from "react";

// 퀴즈 제목
function QuizTitle({ title }: { title: string }) {
    return (
        <h1
            className={
                "text-center lg:text-title1 md:text-title2Bold sm:text-title2Bold"
            }
        >
            {title}
        </h1>
    );
}

export default QuizTitle;

"use server";

import React from "react";

// 퀴즈 문제
function QuizQuestion({ question }: { question: string }) {
    return (
        <h2
            className={"text-menu"}
        >
            {question}
        </h2>
    );
}

export default QuizQuestion;

"use server";

import sanitize from "@/app/_utils/function/sanitize";
import React from "react";
import "prismjs/themes/prism.css";

// 퀴즈 내용
function QuizContent({ content }: { content: string }) {
    return (
        <div
            className={"w-full"}
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        ></div>
    );
}

export default QuizContent;

각 태그에 aria-live : polite을 적용해주도록 하겠습니다.

aria-live는 요소가 동적으로 업데이트될 때 스크린 리더가 이를 감지하고 사용자에게 읽어주는 방식을 지정합니다.

polite는현재 스크린 리더가 읽고 있는 내용을 방해하지 않고, 읽기가 끝난 뒤 변경 사항을 읽어줍니다. 중요한 정보이지만 즉각적인 주의가 필요하지 않은 경우 사용합니다.

다음과 같이 속성 하나만 추가해줬습니다.

"use server";

import React from "react";

// 퀴즈 제목
function QuizTitle({ title }: { title: string }) {
    return (
        <h1
            className={
                "text-center lg:text-title1 md:text-title2Bold sm:text-title2Bold"
            }
        >
            {title}
        </h1>
    );
}

export default QuizTitle;

"use server";

import React from "react";

// 퀴즈 문제
function QuizQuestion({ question }: { question: string }) {
    return (
        <h2
            className={"text-menu"}
        >
            {question}
        </h2>
    );
}

export default QuizQuestion;

"use server";

import sanitize from "@/app/_utils/function/sanitize";
import React from "react";
import "prismjs/themes/prism.css";

// 퀴즈 내용
function QuizContent({ content }: { content: string }) {
    return (
        <div
            className={"w-full"}
            dangerouslySetInnerHTML={{
                __html: sanitize(content),
            }}
        ></div>
    );
}

export default QuizContent;

aria-label 속성 적용

다음은 퀴즈를 채점한 뒤, 나타나는 해설로 이동하는 버튼과 다음 문제로 이동하는 버튼에 대한 컴포넌트입니다.

import AfterCheckButtonContainer
    from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/afterCheckButtonContainer";
import ExplanationLink
    from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/explanationLink";
import NextQuizLink from "@/app/(page)/quiz/_common_ui/client/nextQuizLink";
import React from 'react';

// 채점 후 버튼(해설, 다음문제)
function AfterCheckButtons() {
    
    return (
        <AfterCheckButtonContainer>
             {/*해설 링크*/}
             <ExplanationLink/>
             {/*다음 문제 링크*/}
             <NextQuizLink/>
        </AfterCheckButtonContainer>
    );
}

export default AfterCheckButtons;

AfterCheckButtonContainer는 하위 버튼 및 링크들을 감싸주는 역할을 합니다.

import React from 'react';

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        <nav
            className={"flex justify-center items-center gap-2 w-full"}>
            {children}
        </nav>
    );
}

export default AfterCheckButtonContainer;

이 때 이 컨테이너가 레이아웃 역할뿐만 아니라 다른 페이지로 이동할 수 있는 항목이라는 것을 nav태그를 사용함으로써 알려주고 있습니다.

여기에 더불어 이 링크와 버튼들이 어떠한 네비게이션인지 설명해주면 더 좋을 것 같다는 생각이 들어 aria-label로 추가적인 설명을 해주고 싶었습니다.

그래서 다음과 같이 변경해봤습니다.

import React from 'react';

function AfterCheckButtonContainer({
    children
                                   }:{
    children: React.ReactNode
}) {
    return (
        <nav
            aria-label={"Quiz navigation"}
            className={"flex justify-center items-center gap-2 w-full"}>
            {children}
        </nav>
    );
}

export default AfterCheckButtonContainer;

스크린 리더 버튼들을 설명할 때, 퀴즈 네비게이션이라 해석 및 설명하여 버튼들의 목적을 잘 알려줄 수 있을 것 같다 생각하여 추가해보았습니다.

정리

시맨틱 태그를 적절히 설정하여 aria 속성을 적용할 부분이 많이 없었습니다.

때문에 우선 내가 표현하는 컨텐츠에 대해서 태그와 텍스트가 잘 표현하는 것이 우선적으로 중요하고 그것만으로 설명이 부족하다면 적절한 aria 속성을 적용하는 것이 더 나은 웹 접근성을 향상시키는데에 중요할 것 같습니다.

profile
FE DEVELOPER

0개의 댓글