Tips & Tricks (업데이트: 2026. 6. 2.)

Claude Code 접근성 워크플로: 시맨틱 HTML, ARIA, axe, 수동 점검

Claude Code로 접근성을 개선하는 실무 절차: HTML, 키보드, 폼, 포커스, axe, 스크린리더.

Claude Code 접근성 워크플로: 시맨틱 HTML, ARIA, axe, 수동 점검

접근성은 배포 직전에 도구를 한 번 돌리는 일이 아닙니다. 처음 HTML을 작성할 때부터 의미 있는 태그를 쓰고, 키보드 조작과 포커스 이동을 확인하고, 폼 오류와 색상 대비를 점검한 뒤 자동화 테스트와 스크린리더 확인까지 이어지는 개발 습관입니다.

Claude Code는 이 과정을 빠르게 만들 수 있습니다. 하지만 “접근성 좋게 고쳐줘”라고만 요청하면 위험합니다. div에 ARIA만 붙이고 키보드 동작을 빠뜨리거나, 모달은 보이지만 포커스가 뒤쪽 페이지로 빠지는 코드를 만들 수 있습니다.

이 글의 기준은 공식 자료입니다. 실무 목표는 W3C WCAG 2.2, 위젯 패턴은 WAI-ARIA Authoring Practices Guide, ARIA 원칙은 MDN ARIA 문서, 자동 점검은 Deque axe-core 문서를 참고합니다. Claude Code 기본 사용법은 공식 문서를 확인하세요.

먼저 합격 기준을 고정하기

“accessible하게”라는 말은 너무 넓습니다. Claude Code에는 구체적인 합격 기준을 줘야 합니다. 추천 기준은 WCAG 2.2 AA를 실무 목표로 삼고, ARIA보다 시맨틱 HTML을 우선하며, 주요 플로우가 키보드로 동작하고, 자동 점검과 수동 점검 결과를 모두 남기는 것입니다.

영역합격 기준흔한 실패
시맨틱 HTMLbutton, a, form, label, main, nav를 의미대로 사용클릭 가능한 div가 버튼 역할을 함
키보드Tab, Shift+Tab, Enter, Space, Escape로 주요 동작 가능모달을 마우스로만 닫을 수 있음
포커스새 UI가 열리면 내부로 이동하고 닫히면 원래 버튼으로 복귀포커스가 배경으로 빠짐
ARIA원래 HTML로 표현할 수 없는 상태에만 사용aria-label로 보이는 라벨 부족을 덮음
색상텍스트, 버튼, 오류, 포커스가 충분히 대비됨색만으로 오류를 표현
라벨, 설명, 필수 여부, 오류가 input과 연결됨오류가 보이지만 읽히지 않음
테스트axe 자동 점검과 키보드, 스크린리더 확인자동 위반 0개만 보고 완료 처리

MDN은 필요한 의미와 동작을 제공하는 기본 HTML 요소가 있다면 ARIA보다 그것을 쓰라고 설명합니다. Claude Code도 먼저 HTML 구조를 고치게 하고, 부족한 상태 표현에만 ARIA를 추가하게 해야 합니다.

안전한 Claude Code 프롬프트

접근성 작업의 프롬프트는 작업 지시가 아니라 리뷰 계약에 가깝습니다. 범위, 금지 사항, 기준, 검증 명령을 함께 적습니다.

claude <<'PROMPT'
Scope:
- Review only src/components/CheckoutForm.tsx and its tests.
- Do not change pricing copy, analytics events, or unrelated styles.

Accessibility target:
- Use WCAG 2.2 AA as the practical target.
- Prefer semantic HTML before ARIA.
- Add ARIA only when native HTML cannot express the state.

Check these items:
- Labels, descriptions, required state, and validation errors.
- Keyboard operation with Tab, Shift+Tab, Enter, Space, and Escape.
- Focus order, visible focus, and focus return after closing UI.
- Color contrast and non-color error indicators.
- Automated axe check plus manual screen-reader notes.

Output:
- Findings first, with file and line references.
- Minimal patch.
- Commands to verify.
- Any remaining risk.
PROMPT

핵심은 Findings first입니다. Claude Code가 먼저 결함을 설명하면, 이후 패치가 왜 필요한지 검토하기 쉽습니다. CTA, 가격 문구, 분석 이벤트가 있는 페이지에서는 특히 중요합니다. 범위를 작게 유지하는 습관은 Claude Code 생산성 팁과도 연결됩니다.

유스케이스 1: 제품 CTA와 글 하단 CTA

제품 카드나 글 하단 CTA는 수익 경로와 직접 연결됩니다. 화면에는 카드처럼 보여도 실제 동작이 페이지 이동이라면 원칙적으로 링크를 사용해야 합니다.

<div class="hero-card" onclick="location.href='/en/products'">
  <div class="title">Claude Code Templates</div>
  <div class="button">Buy now</div>
</div>

더 나은 구조는 제목, 설명, 링크를 분리하는 것입니다.

<section aria-labelledby="templates-heading" class="product-cta">
  <h2 id="templates-heading">Claude Code 템플릿으로 리뷰 시간을 줄이기</h2>
  <p>
    구현, 리뷰, 디버깅, 문서화에 반복해서 쓰는 프롬프트를
    바로 복사할 수 있는 형태로 정리합니다.
  </p>
  <a class="primary-link" href="/en/products">
    제품 리소스 보기
  </a>
</section>

Claude Code에는 링크 목적지, Gumroad URL, 분석 속성을 보존하라고 명시하세요. 접근성 개선이 전환 경로를 깨면 실무에서는 좋은 패치가 아닙니다.

유스케이스 2: 상담 폼과 오류 메시지

폼은 접근성 문제가 매출 손실로 이어지는 대표적인 위치입니다. 사용자가 필드 이름을 알 수 없거나, 오류를 읽을 수 없거나, 어떤 값을 고쳐야 하는지 모르면 제출을 포기합니다.

import { FormEvent, useState } from "react";

type Errors = {
  name?: string;
  email?: string;
};

export function ConsultationForm() {
  const [errors, setErrors] = useState<Errors>({});

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const nextErrors: Errors = {};

    if (!String(data.get("name") || "").trim()) {
      nextErrors.name = "이름을 입력하세요.";
    }

    if (!String(data.get("email") || "").includes("@")) {
      nextErrors.email = "올바른 이메일 주소를 입력하세요.";
    }

    setErrors(nextErrors);
  }

  return (
    <form aria-labelledby="consultation-title" onSubmit={handleSubmit} noValidate>
      <h2 id="consultation-title">도입 상담 폼</h2>

      <div className="field">
        <label htmlFor="name">이름</label>
        <input
          id="name"
          name="name"
          autoComplete="name"
          aria-invalid={errors.name ? "true" : "false"}
          aria-describedby={errors.name ? "name-error" : undefined}
        />
        {errors.name && (
          <p id="name-error" role="alert">
            {errors.name}
          </p>
        )}
      </div>

      <div className="field">
        <label htmlFor="email">이메일</label>
        <p id="email-help">답장을 받을 수 있는 업무용 이메일을 입력하세요.</p>
        <input
          id="email"
          name="email"
          type="email"
          autoComplete="email"
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={
            errors.email ? "email-help email-error" : "email-help"
          }
        />
        {errors.email && (
          <p id="email-error" role="alert">
            {errors.email}
          </p>
        )}
      </div>

      <button type="submit">상담 내용 보내기</button>
    </form>
  );
}

흔한 실패는 오류 문구만 표시하고 input과 연결하지 않는 것입니다. 반대로 존재하지 않는 오류 ID를 항상 참조하는 것도 유지보수에 좋지 않습니다. 도움말은 항상 연결하고 오류는 발생했을 때만 연결하는 방식이 깔끔합니다.

유스케이스 3: 모달과 명령 팔레트

모달, 설정 드로어, 명령 팔레트는 포커스 관리가 핵심입니다. WAI-ARIA 모달 패턴은 열릴 때 포커스가 내부로 이동하고, Tab이 내부에서 순환하며, Escape로 닫히고, 닫힌 뒤 호출 버튼으로 돌아가야 한다고 설명합니다.

import { ReactNode, useEffect, useRef } from "react";

type ModalProps = {
  open: boolean;
  title: string;
  onClose: () => void;
  children: ReactNode;
};

const focusableSelector = [
  "a[href]",
  "button:not([disabled])",
  "input:not([disabled])",
  "select:not([disabled])",
  "textarea:not([disabled])",
  '[tabindex]:not([tabindex="-1"])',
].join(",");

export function AccessibleModal(props: ModalProps) {
  const { open, title, onClose, children } = props;
  const dialogRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (!open) return;

    previousFocusRef.current = document.activeElement as HTMLElement;
    const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
      focusableSelector
    );
    focusable?.[0]?.focus();

    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") onClose();
      if (event.key !== "Tab" || !dialogRef.current) return;

      const items = [...dialogRef.current.querySelectorAll<HTMLElement>(
        focusableSelector
      )];
      const first = items[0];
      const last = items[items.length - 1];

      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last?.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first?.focus();
      }
    }

    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("keydown", onKeyDown);
      previousFocusRef.current?.focus();
    };
  }, [open, onClose]);

  if (!open) return null;

  return (
    <div className="modal-backdrop">
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="modal-panel"
      >
        <h2 id="modal-title" tabIndex={-1}>
          {title}
        </h2>
        {children}
        <button type="button" onClick={onClose}>
          닫기
        </button>
      </div>
    </div>
  );
}

드롭다운도 무조건 role="menu"가 답은 아닙니다. APG Menu Button Pattern은 명령 메뉴에 적합합니다. 일반 사이트 내비게이션은 nav와 링크 목록이 더 자연스러운 경우가 많습니다.

색상, 포커스, 모바일 터치 영역

디자인 수정 중 가장 자주 망가지는 부분은 포커스 표시입니다. outline: none을 넣었다면 반드시 대체 스타일이 있어야 합니다. 오류도 색만으로 표현하지 말고 선, 아이콘, 텍스트를 함께 사용합니다.

.primary-link {
  background: #0f766e;
  border-radius: 6px;
  color: #ffffff;
  display: inline-flex;
  font-weight: 700;
  min-height: 44px;
  padding: 0.75rem 1rem;
}

.primary-link:focus-visible,
button:focus-visible,
input:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

.field [role="alert"] {
  border-left: 4px solid #b91c1c;
  color: #7f1d1d;
  margin-top: 0.5rem;
  padding-left: 0.75rem;
}

모바일 폭도 확인해야 합니다. 작은 닫기 버튼, sticky header에 가려지는 포커스, 너무 좁은 CTA는 데스크톱 리뷰에서 쉽게 놓칩니다.

axe 자동 점검과 스크린리더 점검

axe는 구조적 문제를 빠르게 찾지만 문구가 실제로 이해되는지는 보장하지 않습니다. 따라서 CI 게이트로 사용하고, 변경된 핵심 플로우는 직접 키보드와 스크린리더로 확인합니다.

npm install -D @axe-core/playwright @playwright/test
npx playwright install --with-deps chromium
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

test("consultation form has no serious accessibility issues", async ({ page }) => {
  await page.goto("/contact");

  const results = await new AxeBuilder({ page })
    .include("main")
    .withTags(["wcag2a", "wcag2aa", "wcag22aa"])
    .analyze();

  expect(results.violations).toEqual([]);
});

수동 확인은 제품 CTA, 상담 폼, 결제 전 폼, 모달, 내비게이션, 오류 복구부터 시작합니다. Windows에서는 NVDA, macOS에서는 VoiceOver가 현실적인 출발점입니다.

자주 남는 실패 예시

  • role="button"만 있고 Enter와 Space 처리가 없다.
  • 아이콘 버튼에 접근 가능한 이름이 없다.
  • alt="image"처럼 의미 없는 대체 텍스트가 들어간다.
  • aria-hidden="true"가 모달이나 live region까지 숨긴다.
  • 오류 문구가 input과 aria-describedby로 연결되지 않는다.
  • 포커스 링을 제거하고 대체 표시가 없다.
  • 모달을 닫은 뒤 포커스가 호출 버튼으로 돌아오지 않는다.
  • 자동 점검이 실제 상담 폼이 아니라 마케팅 첫 화면만 검사한다.

이 목록을 Claude Code에 그대로 주고 “이번 diff에 해당 항목이 있는지 파일과 줄 번호로 찾아라”라고 요청하면, 막연한 재검토보다 훨씬 유용합니다.

CTA와 실제 확인 결과

기본 명령과 리뷰 습관은 무료 자료로 시작하고, 반복 가능한 리뷰와 디버깅 프롬프트가 필요하면 Claude Code 제품 리소스50 Claude Code 프롬프트 템플릿을 확인하세요. 팀에서 권한, hook, 검증 기록까지 정리해야 한다면 상담으로 진행하는 편이 빠릅니다.

이번 업데이트에서는 세 가지 실패를 기준으로 확인했습니다. CTA 카드가 클릭 가능한 div였던 경우, 폼 오류가 보이지만 읽히지 않았던 경우, 모달을 닫은 뒤 포커스가 사라진 경우입니다. Claude Code에는 먼저 findings를 쓰게 하고, 그다음 최소 패치를 만들게 했을 때 결과가 가장 안정적이었습니다. axe는 구조 문제를 잡는 데 유용했지만, 마지막 판단은 키보드 조작과 VoiceOver 확인이 필요했습니다.

#Claude Code #accessibility #WCAG #a11y #React
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.