[React][웹 접근성] react router와 Loading 컴포넌트

Joowon Jang·2024년 10월 26일

React

목록 보기
12/19

Loading (Spinner) 컴포넌트

Loading Spinner: 앱에서 콘텐츠나 데이터가 로드되고 있을 때 사용자에게 그 상태를 시각적으로 알려주는 애니메이션

서버에서 데이터를 불러온다거나, JavaScript 파일을 다운로드 받는 시간동안 아무 것도 보이지 않으면, 사용자는 페이지가 로드되고 있는지 아무 것도 없는 페이지인지 알 수가 없다.
그래서 Loading 컴포넌트를 사용해서 콘텐츠가 로드되는 중이라는 것을 알려주는 것이 좋은데, 최근 진행한 프로젝트에서 어떤 방법을 사용했고, 어떤 부분을 신경써야 하는지 정리하려 한다.

라우터 구성

App.jsx에서 아래와 같이 AppRouter를 렌더링한다.

// App.jsx

import AppRouter from './router';

function App() {
  return <AppRouter />;
}

export default App;

아래는 프로젝트에 사용한 router.jsx의 일부이다.

// router.jsx

import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Loading from './components/Loading/Loading';

const RootLayout = lazy(() => import('./layouts/RootLayout/RootLayout'));
const HomePage = lazy(() => import('./pages/homePage'));
const MusicPage = lazy(() => import('./pages/musicPage'));
const MyPage = lazy(() => import('./pages/myPage'));

const routes = [
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: 'music',
        element: (
          <Suspense fallback={<Loading musicPage />}>
            <MusicPage />
          </Suspense>
        ),
      },
      {
        path: 'my',
        element: <MyPage />,
      },
      // ...
    ],
  },
];

// 라우터
const router = createBrowserRouter(routes);

const AppRouter = () => (
  <Suspense fallback={<Loading />}>
    <RouterProvider router={router} />
  </Suspense>
);

// 라우터 내보내기
export default AppRouter;

React에서 제공하는 lazy api를 사용해서 각각의 페이지(컴포넌트)를 불러온다.
이렇게 컴포넌트를 lazy를 사용해서 로딩하면, 해당 컴포넌트가 필요하지 않을 때에는 로드하지 않는 지연 로딩을 통해 초기 로딩 시간을 줄일 수 있다.

그 다음 routes 객체를 통해 만들어진 RouterProviderSuspense로 감싸주고, fallback속성으로 Loading 컴포넌트를 넣어주면, 콘텐츠를 로드하는 동안 Loading 컴포넌트를 보여주어 콘텐츠가 로드 중임을 시각적으로 알려주게 된다.

그런데, 위의 라우터에서 MusicPage의 element는 이미 Suspense로 감싸져있는 상태인데, 이런 경우에는 개별적으로 다른 컴포넌트를 보여줄 수 있다.

왼쪽은 전체, 오른쪽은 MusicPage의 개별적으로 Suspense로 처리해준 Loading 컴포넌트가 보이는 것을 확인할 수 있다.

참고: https://ko.react.dev/reference/react/lazy

접근성

위처럼 Loading 컴포넌트를 보여주는 방식 자체는 간단하다.
하지만, 콘텐츠가 로드 중이라는 정보를 시각적으로만 제공하기 때문에 시각적 제약이 있는 상황의 사용자는 여전히 무슨 상황인지 알 수 없다는 문제가 있다.

이 경우에, 스크린리더가 읽을 수 있는 정보를 제공해야 한다.

다음의 2가지는 반드시 스크린리더가 읽어야 한다.

  1. 로딩이 시작되었다는 것을 읽기
  2. 로딩이 종료되었다는 것을 읽기

페이지가 로드될 때부터, aria-live 속성을 삽입한 비어있는 로딩의 시작과 종료 요소가 같이 존재한다.
리액트 앱 외부에서 렌더링이 필요하기 때문에 아래와 같이 페이지 로딩의 시작과 종료를 알리기 위한 요소를 HTML에 작성한다.

<!-- index.html -->

<!doctype html>
<html lang="ko-KR">
  <head>
    <meta charset="UTF-8" />
    <title>해마디</title>
    <!-- ... -->
    <script type="module" src="src/main.jsx"></script>
  </head>

  <body>
    <noscript>이 앱을 사용하려면 JavaScript 활성화가 필요합니다.</noscript>

    <div id="root"></div>

    <div id="loading-start" aria-live="assertive"></div>
    <div id="loading-end" aria-live="assertive"></div>
  </body>
</html>

로딩이 시작되었다는 것을 알리기

로딩이 시작되면 role="alert" 속성이 삽입되고, 비어있는 loading-start 안에 로딩 내용이 삽입된다.

로딩이 종료되었다는 것을 알리기

로딩 소스는 삽입되었던 role="alert" 속성과 함께 일정 시간이 지나면 삭제되고, 하단 로딩 종료를 위한 소스에 종료되었다는 문구가 삽입된다.

로딩이 끝나면 삽입되었던 종료 소스도 삭제되고, 처음 존재했었던 소스만 남긴다.

Loading 컴포넌트

// Loading.jsx

import { bool, number } from 'prop-types';
import { useLayoutEffect } from 'react';
import styles from './Loading.module.css';
import { SyncLoader } from 'react-spinners';

// 로딩 시작/끝 엘리먼트
// 컴포넌트 초기화 과정에서 1회만 필요하므로 컴포넌트 외부에서 참조
const loadingStartElement = document.getElementById('loading-start');
const loadingEndElement = document.getElementById('loading-end');

// 로딩 시작 설정 함수
const setLoadingStart = () => {
  loadingStartElement.innerHTML =
    '<p class="sr-only">콘텐츠 로딩 시작됩니다.</p>';
  loadingStartElement.setAttribute('role', 'alert');
};

// 로딩 시작 초기화 함수
const resetLoadingStart = () => {
  loadingStartElement.innerHTML = '';
  loadingStartElement.removeAttribute('role');
};

// 로딩 끝 설정 함수
const setLoadingEnd = () => {
  loadingEndElement.innerHTML =
    '<p class="sr-only">콘텐츠 로딩이 마무리되었습니다.</p>';
};

// 로딩 끝 초기화 함수
const resetLoadingEnd = () => {
  loadingEndElement.innerHTML = '';
};

// 컴포넌트 속성 타입 검사
Loading.propTypes = { size: number, musicPage: bool };

// Loading 컴포넌트
function Loading({ size = 24, musicPage = false }) {
  // 레이아웃 이펙트
  useLayoutEffect(() => {
    // 로딩 시작 설정
    setLoadingStart();

    return () => {
      // 로딩 시작 초기화
      resetLoadingStart();
      // 로딩 끝 설정
      setLoadingEnd();
      // 1초 이후, 로딩 끝 초기화
      setTimeout(resetLoadingEnd, 1000);
    };
  }, []);

  if (musicPage)
    return (
      <div className={styles.loadingMusic}>
        <div className={styles.loadingText}></div>
        <img src="/bgImages/island.webp" alt="" />
      </div>
    );

  // 화면에 표시할 스피너(spinner) 마크업
  // CSS 모듈 스타일 클래스 이름으로 연결
  return (
    <SyncLoader
      margin={6}
      size={size}
      color="#2E7FB9"
      className={styles.spinner}
    />
  );
}

export default Loading;

이렇게 로딩이 시작되고 종료될 때, loading-startloading-end 요소에 로딩 문구를 제공함과 동시에 role 속성을 변경해준다.

로딩 시작할 때만 role="alert" 속성이 삽입된 이유

role="alert" 속성은 스크린리더가 읽고 있는 것을 중지하고 주위를 환기시키고 집중할 수 있게 하기 때문에 로딩중이라는 것을 바로 인지할 수 있다.
하지만, 종료 시 role="alert" 속성을 넣게 되면 로딩 시작 문구를 중간에 끊어서 제대로 읽지 않기 때문에 종료에는 삭제하고 스크린리더가 읽던 것을 다 읽은 후에 종료되었다는 것을 읽도록 aria-live="assertive" 속성만 넣는다.

접근성 관련 자세한 내용은 아래의 문서에서 확인할 수 있다.

참고: https://aoa.gitbook.io/skymimo/aoa-2018/2018-aria/loading

profile
깊이 공부하는 웹개발자

0개의 댓글