iBetter Books
수정

도입 스토리

"스킵 내비게이션 추가했어요!" 김개발이 자랑스럽게 말했습니다.

박멘토가 Tab 키를 눌렀습니다. 아무것도 나타나지 않았습니다. 다시 Tab. 내비게이션 첫 번째 링크로 이동했습니다.

"스킵 링크를 추가했는데, 실제로 나타나지 않아요?"

"그냥 포커스가 올 때 보이게 해뒀는데, 포커스가 이미 다음 요소로 넘어가는 것 같아요."

박멘토가 소스를 확인했습니다. 스킵 링크가 내비게이션 메뉴 <ul> 아래 있었습니다.

"스킵 링크는 페이지의 첫 번째 요소여야 해요. 내비게이션보다 먼저 나와야 Skip Navigation이죠."

핵심 개념 설명

건너뛰기 링크 — WCAG 2.4.1

건너뛰기 링크(Skip Navigation)는 반복되는 메뉴를 건너뛰고 본문으로 바로 이동하는 링크입니다. 페이지의 가장 첫 번째 요소여야 합니다.

<!DOCTYPE html><html lang="ko"><head>...</head><body>  <!-- 반드시 <body> 직후 첫 번째 요소 -->  <a href="#main-content" class="skip-link">    본문으로 건너뛰기  </a>  <header>    <nav aria-label="주요 메뉴">      <!-- 30개의 링크 -->    </nav>  </header>  <main id="main-content" tabindex="-1">    <!-- tabindex="-1": 프로그래밍 포커스는 가능, Tab 순서에는 없음 -->    <h1>서비스 소개</h1>    ...  </main></body></html>
/* 기본은 숨기고, 포커스 시 보이게 */.skip-link {  position: absolute;  top: -100%;  left: 1rem;  padding: 0.75rem 1.5rem;  background: #005FCC;  color: white;  font-weight: bold;  border-radius: 0 0 4px 4px;  text-decoration: none;  z-index: 9999;  transition: top 0.2s;}.skip-link:focus {  top: 0;}

동적 콘텐츠의 포커스 관리

SPA(Single Page Application)에서 페이지 이동 시, 새 콘텐츠로 포커스를 이동시켜야 합니다. 그렇지 않으면 포커스가 이전 위치에 남아 혼란이 생깁니다.

// React Router 예시: 페이지 이동 후 포커스 관리function PageWrapper({ children }) {  const location = useLocation();  const mainRef = useRef(null);  useEffect(() => {    // 라우트 변경 시 main으로 포커스 이동    if (mainRef.current) {      mainRef.current.focus();    }  }, [location.pathname]);  return (    <main      ref={mainRef}      tabIndex={-1}      id="main-content"    >      {children}    </main>  );}

모달 포커스 관리

모달을 열 때와 닫을 때 포커스 이동이 중요합니다.

class Modal {  constructor(element) {    this.modal = element;    this.opener = null; // 모달을 연 버튼 기억  }  open(openerButton) {    this.opener = openerButton;    // 모달 표시    this.modal.removeAttribute('hidden');    this.modal.setAttribute('aria-modal', 'true');    // 배경 스크롤 방지    document.body.style.overflow = 'hidden';    // 포커스를 모달 안으로 이동    const firstFocusable = this.modal.querySelector(      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'    );    firstFocusable?.focus();    // 포커스 트랩 활성화    this.modal.addEventListener('keydown', this.handleKeydown.bind(this));  }  close() {    // 모달 숨기기    this.modal.setAttribute('hidden', '');    document.body.style.overflow = '';    this.modal.removeEventListener('keydown', this.handleKeydown.bind(this));    // 포커스를 모달을 열었던 버튼으로 복원 (핵심!)    this.opener?.focus();  }  handleKeydown(e) {    if (e.key === 'Escape') {      this.close();    }  }}

포커스 이동 원칙 정리

상황 포커스 이동 위치
페이지 로드 <main> 또는 <h1>
모달 열기 모달 첫 번째 포커스 가능 요소
모달 닫기 모달을 열었던 버튼
드롭다운 열기 첫 번째 메뉴 항목
드롭다운 닫기 드롭다운 버튼
알림/토스트 닫기 알림을 유발한 버튼 또는 이전 포커스
SPA 라우트 변경 새 페이지의 <main> 또는 <h1>

포커스 이동 시 사용자 안내

포커스가 갑자기 이동하면 스크린리더 사용자가 혼란스러울 수 있습니다. aria-live로 이동 이유를 알려줍니다.

<!-- 스크린리더 전용 공지 영역 --><div  id="page-announcement"  role="status"  aria-live="polite"  aria-atomic="true"  class="sr-only"></div>
function navigateTo(page) {  // 페이지 이동  loadPage(page);  // 스크린리더에 이동 알림  document.getElementById('page-announcement').textContent =    `${page.title} 페이지로 이동했습니다.`;}

단계별 실습

따라하기: 건너뛰기 링크 추가하기

  1. <body> 직후 첫 번째 자식으로 스킵 링크를 추가합니다.
  2. <main>id="main-content"tabindex="-1"을 추가합니다.
  3. Tab 키를 눌러 스킵 링크가 나타나는지 확인합니다.
  4. Enter를 눌러 <main>으로 포커스가 이동하는지 확인합니다.

변형하기: 스킵 링크 스타일링

/* 접근성 + 디자인 모두 만족하는 스킵 링크 */.skip-link {  position: fixed;  top: 1rem;  left: 50%;  transform: translateX(-50%) translateY(-200%);  padding: 1rem 2rem;  background: #1a1a1a;  color: #ffffff;  font-size: 1rem;  font-weight: 600;  border-radius: 8px;  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);  text-decoration: none;  z-index: 99999;  transition: transform 0.2s ease;}.skip-link:focus {  transform: translateX(-50%) translateY(0);}

응용하기: SPA 포커스 관리 감사

React, Vue, Angular 앱에서 라우트 변경 후 포커스 위치를 확인합니다.

// 현재 포커스 위치 추적document.addEventListener('focusin', (e) => {  console.log('포커스:', e.target.tagName, e.target.id, e.target.textContent?.substring(0, 30));});

정리와 확인

핵심 내용 요약

  • 스킵 링크(WCAG 2.4.1): <body> 직후 첫 번째 요소 — href="#main-content"
  • <main tabindex="-1">: 스킵 링크의 목적지에 필요
  • 모달 포커스: 열 때 → 모달 내부 첫 요소 / 닫을 때 → 열었던 버튼
  • SPA 라우트 변경: useEffect<main> 포커스 이동
  • 포커스 복원: 사용자 맥락을 잃지 않게 원래 위치로 돌아와야 함

확인 문제

문제 1. 스킵 링크가 작동하려면 <main>에 어떤 속성이 필요한가?

<main id="main-content" tabindex="-1"><!-- id: href="#main-content"와 연결, tabindex="-1": 프로그래밍 포커스 허용 -->

문제 2. 모달을 닫은 후 포커스가 페이지 상단으로 이동하면 왜 문제인가?

사용자가 모달을 열기 전에 어디에 있었는지 맥락을 잃게 됩니다.
페이지 상단에서 다시 Tab을 처음부터 눌러야 이전 위치로 돌아올 수 있습니다.

PART 03을 마쳤습니다. 다음 PART에서는 "이해할 수 있는(Understandable)" 원칙을 다룹니다. 예측 가능한 UI, 명확한 오류 메시지, 언어 설정까지 — 콘텐츠가 이해되어야 진짜 접근성입니다.