Skip to content
Published on

접근성과 국제화 2025 — WCAG 2.2·EU Accessibility Act·ARIA·next-intl·한일 타이포그래피·RTL 완전 가이드

Authors

프롤로그 — "성능은 잘 나왔는데 쓸 수가 없어요"

Core Web Vitals 지표가 파릇파릇 초록색이어도 화면 낭독기로는 리스트가 전혀 안 읽히고, 영어 버전은 멀쩡한데 한국어에서는 조사가 깨지고 줄바꿈이 어색한 서비스가 있다. 그제야 "아 이거 처음부터 설계했어야 했구나"라는 말이 나온다. 2025년의 접근성과 국제화는 더 이상 출시 직전에 땜질하는 옵션이 아니다.

2025년 전환점 세 가지:

  1. EU Accessibility Act 발효(2025.06.28) — EU 시장에 서비스를 제공하는 모든 디지털 제품(웹·앱·이커머스·e북·ATM·교통 단말기)에 WCAG 기준 적용이 법적 의무로 못 박혔다. 위반 시 시장에서 제외(withdraw from market)당할 수 있다.
  2. WCAG 2.2 정식 권고안(W3C Recommendation, 2023.10) — 터치 타겟 최소 크기, 드래그 조작 대체 수단, 인증 인지 부담 등 모바일·인지 장애 관점이 대폭 강화되었고, 2025년 대부분의 공공·대기업 조달 기준으로 자리 잡았다.
  3. AI·LLM 기반 다국어 품질의 상향 평준화 — 기계 번역은 이제 "빠르고 저렴하지만 어색한 선택지"가 아니다. 다만 조사·존댓말·문화적 맥락에서는 여전히 사람의 손이 필수다.

그리고 한 가지 더, 한국에서는 장애인차별금지법 + 행정안전부 전자정부법으로 공공·금융·교육 분야의 웹 접근성이 이미 의무화되어 있으며, 2024년 이후 민간으로 확산 중이다.

"접근성은 비용이다"라고 믿었던 시대는 끝났다. 접근성은 SEO 향상 + 이탈률 감소 + 신규 사용자 유치 + 소송 리스크 해소가 결합된 품질 투자다.

이번 글에서는 2025년의 a11y·i18n을 13개 챕터로 정리한다.


1장 · 접근성을 다시 정의한다 — "모두의 품질"

전통적 정의 (2000년대)

"장애인을 위한 보조 수단" → 소수 사용자용 기능. 비용 센터. 법적 compliance.

2025년의 정의

모두의 품질. 다음 6가지 상황을 모두 포괄한다.

  1. 영구적 장애 — 시각·청각·운동·인지 장애
  2. 일시적 장애 — 팔 골절, 수술 후 시력 변화, 약물 부작용
  3. 상황적 제약 — 햇빛 아래 화면 못 봄, 소음 속 청취 불가, 한 손이 아기로 막힘
  4. 고령화 — 시력·청력·인지 능력의 점진적 저하
  5. 디바이스 다양성 — 소형 스마트폰·거대한 TV·저사양 기기·느린 네트워크
  6. 언어·문화 다양성 — 비영어권, 소수 언어, 문해력 차이

전 세계 약 **16%(13억 명)**가 영구적 장애를 가지며, 위 6가지를 모두 합치면 거의 모두가 어느 시점에 접근성이 필요하다. 이게 "모두의 품질"이라는 이유다.

a11y·i18n 체크 하나로 수십 개의 품질 지표가 개선된다

  • 명확한 heading 구조 → SEO 랭킹 상승
  • 명확한 alt 텍스트 → 이미지 SEO + 봇 크롤링
  • 대비 높은 색상 → 햇빛 아래서도 잘 읽힘
  • 크고 충분한 터치 타겟 → 노인·오타 감소
  • 키보드 내비게이션 → 개발자 도구·파워 유저 호환
  • 명확한 에러 메시지 → 전환율 향상
  • 다국어 지원 → 매출 +N% (Google 조사: 완전한 현지화로 구매 의도 72% 증가)

2장 · WCAG 2.2 — 2025년의 필수 기준

WCAG의 4대 원칙 (POUR)

  1. Perceivable (인지 가능) — 모든 정보는 사용자가 감지 가능해야 한다 (텍스트·대체 텍스트·자막)
  2. Operable (운용 가능) — UI 컨트롤은 모두 조작 가능해야 한다 (키보드·터치·음성)
  3. Understandable (이해 가능) — 정보와 조작 방법은 이해할 수 있어야 한다 (명확한 언어·예측 가능한 동작)
  4. Robust (견고) — 보조 기술로도 안정적으로 해석 가능해야 한다 (시맨틱·ARIA)

A·AA·AAA 등급

  • A (최소) — 키보드 탐색, alt 텍스트, 자막, 색상만으로 정보 전달 금지
  • AA (권장, 실질적 표준) — 대비 4.5:1, 텍스트 200% 확대, 포커스 가시성
  • AAA (고수준) — 대비 7:1, 수화 영상, 읽기 수준

"AA 준수"가 2025년 산업 표준. EU Accessibility Act, 한국 KWCAG, 미국 Section 508 모두 AA 기반.

WCAG 2.2의 9개 새로운 성공 기준 (2023.10 → 2025 필수)

기준핵심영향
2.4.11 Focus Not Obscured (Minimum)포커스된 요소가 다른 요소에 가려지지 않아야 함고정 헤더·툴팁 설계 변경
2.4.12 Focus Not Obscured (Enhanced)위와 같지만 "완전히" 가리지 않아야 함AAA
2.4.13 Focus Appearance포커스 링 최소 크기·대비 기준CSS :focus-visible 필수
2.5.7 Dragging Movements드래그 전용 조작에 대체 수단 필요Trello·Kanban UI 전면 수정
2.5.8 Target Size (Minimum)터치 타겟 최소 24x24 CSS px모바일 버튼 설계 영향
3.2.6 Consistent Help도움말·연락처 위치 일관성헤더·푸터 설계
3.3.7 Redundant Entry이전에 입력한 정보를 다시 요구하지 않기Checkout Flow
3.3.8 Accessible Authentication (Minimum)인지 퍼즐(문자 판독) 없는 대체 인증 수단패스키·WebAuthn 확산
3.3.9 Accessible Authentication (Enhanced)객체·이미지 인식 의존 제거AAA

가장 실질적으로 영향이 큰 기준: 2.5.8 Target Size. 모바일 아이콘 버튼, 드롭다운 화살표, 닫기(X) 버튼을 재검토해야 한다.

한국 KWCAG 2.2

행정안전부 주관. WCAG 2.2 AA 기반 + 한국 특수 상황(한글·장애인차별금지법) 반영. 공공기관은 연 2회 진단을 의무적으로 받아야 하며, 민간 금융·교육도 확대 적용 중.


3장 · 시맨틱 HTML — "div 공화국"에서 벗어나기

가장 중요한 a11y 원칙

"보조 기술은 시맨틱(semantic)을 읽는다. <div>만으로는 아무것도 전달되지 않는다."

Bad: div 공화국

<div class="button" onclick="submit()">확인</div>
<div class="list">
  <div class="item">항목 1</div>
  <div class="item">항목 2</div>
</div>
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>

문제:

  • Screen Reader는 <div>를 "그냥 텍스트"로 읽는다. "list has 3 items" 같은 유용한 맥락 제공 불가
  • Enter·Space로 클릭 안 됨 (직접 구현 필요)
  • Tab 포커스 안 됨
  • SEO도 저하 (<main>·<article>·<nav> 없으면 주요 콘텐츠 판별 어려움)

Good: 시맨틱 HTML

<header>
  <nav aria-label="주요 메뉴">
    <ul>
      <li><a href="/"></a></li>
      <li><a href="/about">소개</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <h1>제목</h1>
    <p>본문</p>
  </article>
</main>

<footer>...</footer>

<button type="submit">확인</button>

<ul>
  <li>항목 1</li>
  <li>항목 2</li>
</ul>

Screen Reader가 자동으로 읽어주는 것들:

  • "main region", "navigation region" (랜드마크 탐색)
  • "list with 2 items"
  • "button, 확인"
  • Heading 레벨별 이동 (H 단축키)

주요 시맨틱 요소

요소용도
<header> / <footer>페이지 또는 섹션의 머리말/꼬리말
<nav>내비게이션 (aria-label 필수 시 명시)
<main>페이지 주요 내용 (1개만)
<article>독립적으로 의미 있는 콘텐츠
<section>주제별 묶음
<aside>부가 정보
<figure> / <figcaption>이미지+설명
<button> vs <a>동작=button, 이동=a
<ul> / <ol> / <li>리스트 (진짜 리스트일 때)
<dialog>모달 (브라우저 네이티브)
<details> / <summary>접기·펼치기

<button> vs <a> — 가장 흔한 실수

  • 버튼(동작): 폼 제출, 모달 열기, 상태 토글 → <button>
  • 링크(이동): URL 변경, 외부 이동 → <a href>
<!-- Bad -->
<a onclick="submit()">확인</a>
<button onclick="location.href='/about'">소개</button>

<!-- Good -->
<button type="submit">확인</button>
<a href="/about">소개</a>

4장 · ARIA — 강력하지만 위험한 도구

ARIA의 첫 번째 규칙

"사용하지 않는 것이 최선이다."

ARIA Authoring Practices 문서의 첫 줄: "No ARIA is better than bad ARIA." 적절한 시맨틱 HTML로 해결 가능하다면 절대 ARIA를 쓰지 말라.

왜 ARIA는 위험한가?

  • role="button" 선언했지만 키보드 처리 안 됨 → 오히려 혼란
  • aria-hidden="true" 중요 콘텐츠에 잘못 적용 → 완전 차단
  • Screen Reader마다 ARIA 해석이 다름 (NVDA·JAWS·VoiceOver·TalkBack)

ARIA를 써야 하는 경우

  1. 네이티브 HTML로 표현 불가능한 UI (탭, 콤보박스, 트리)
  2. 동적 상태 표현 (aria-expanded, aria-selected, aria-busy)
  3. 이름·설명 추가 (aria-label, aria-labelledby, aria-describedby)
  4. 라이브 리전 (aria-live)

자주 쓰는 ARIA 속성

<!-- 아이콘 버튼 — 텍스트 없는 버튼에 라벨 추가 -->
<button aria-label="메뉴 닫기">
  <svg aria-hidden="true">...</svg>
</button>

<!-- 확장 상태 -->
<button aria-expanded="false" aria-controls="menu-1">
  메뉴
</button>
<ul id="menu-1" hidden>...</ul>

<!-- 라이브 리전 — 동적 업데이트 알림 -->
<div aria-live="polite">저장되었습니다.</div>
<div aria-live="assertive" role="alert">오류: 비밀번호 틀림</div>

<!-- 필수 입력 -->
<label for="email">이메일 <span aria-hidden="true">*</span></label>
<input id="email" required aria-required="true" />

<!-- 에러 메시지 연결 -->
<input aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">올바른 이메일을 입력하세요</span>

ARIA 안티패턴 TOP 5

  1. <div role="button"> — 그냥 <button> 써라
  2. aria-label="이미지" — 정보성 이미지는 <img alt="구체적 설명">
  3. role="list" + <li> 대신 <div> — 그냥 <ul> 써라
  4. aria-hidden="true" on <main> — 앱 전체가 screen reader에서 사라진다
  5. 포커스 가능 요소에 aria-hidden="true" — "숨겨졌지만 포커스되는" 모순 상태

5장 · 키보드 내비게이션 — 마우스 없이 살아남기

테스트하는 법

마우스를 책상에서 치워라. 30분만 키보드로 서비스를 써보면 문제가 한눈에 보인다.

필수 키보드 조작

동작
Tab다음 포커스 가능 요소
Shift+Tab이전 포커스 가능 요소
Enter / Space버튼·링크 활성화
Esc모달·팝업·드롭다운 닫기
Arrow메뉴·라디오·탭 내 이동
Home / End리스트 처음/끝

포커스 가시성 — :focus-visible

/* Bad — 접근성 파괴 */
button:focus { outline: none; }

/* Good — 키보드 사용자만 링 표시 */
button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* 마우스 클릭 시에는 링 제거 — UX 훼손 안 됨 */
button:focus:not(:focus-visible) {
  outline: none;
}

포커스 트랩 — 모달에서 탈출하지 않기

모달이 열리면 포커스가 모달 안에서만 순환해야 한다. 네이티브 <dialog>는 자동 처리.

<dialog id="modal">
  <h2>확인</h2>
  <p>정말 삭제하시겠습니까?</p>
  <button>취소</button>
  <button>삭제</button>
</dialog>

<script>
  document.getElementById("modal").showModal();
</script>

커스텀 모달일 때는 focus-trap 같은 라이브러리 사용.

<a href="#main" class="skip-link">본문으로 건너뛰기</a>

<nav>...</nav>
<main id="main">...</main>
.skip-link {
  position: absolute;
  left: -10000px;
}
.skip-link:focus {
  left: 0;
  top: 0;
  padding: 8px;
  background: black;
  color: white;
  z-index: 9999;
}

6장 · Screen Reader 테스트 — NVDA·VoiceOver·TalkBack

주요 스크린 리더

SR플랫폼설치점유율
NVDAWindows무료WebAIM 조사 67%
JAWSWindows유료30%
VoiceOvermacOS·iOS내장7%
TalkBackAndroid내장모바일 주류

VoiceOver 단축키 (macOS)

  • Cmd+F5 — VoiceOver 켜기/끄기
  • Ctrl+Option+→ / — 다음·이전 요소
  • Ctrl+Option+Space — 활성화
  • Ctrl+Option+U — 로터(landmarks·headings·links 목록)

NVDA 단축키 (Windows)

  • Insert+Down — 읽기 시작
  • H / Shift+H — 다음·이전 heading
  • K / Shift+K — 다음·이전 link
  • D — 랜드마크 목록
  • NVDA+Space — Focus/Browse mode 토글

테스트 체크리스트

  1. Tab으로 모든 인터랙티브 요소 접근 가능한가?
  2. 포커스 순서가 시각적 순서와 일치하는가?
  3. 버튼·링크에 역할(role)이 정확히 읽히는가?
  4. 폼 라벨이 입력 필드에 연결되어 있는가?
  5. 에러 메시지가 실시간으로 전달되는가?
  6. 모달 열고 닫을 때 포커스 관리가 되는가?
  7. 동적 콘텐츠(무한 스크롤·알림)가 안내되는가?

7장 · 색상·대비·모션 — 보이는 디자인의 접근성

색상 대비 기준 (WCAG AA)

텍스트 종류최소 대비
일반 텍스트 (<18pt)4.5:1
큰 텍스트 (≥18pt 또는 ≥14pt bold)3:1
UI 컴포넌트 경계·아이콘3:1

대비 측정 도구

  • WebAIM Contrast Checker
  • Chrome DevTools → Elements → Styles → 색상 옆 대비 표시
  • Figma 플러그인: Stark, A11y Contrast Checker

색상만으로 정보 전달 금지

<!-- Bad — 색맹 사용자는 구분 불가 -->
<p>필수 항목은 <span style="color:red">빨간색</span>입니다</p>

<!-- Good — 색 + 아이콘 + 텍스트 -->
<p>필수 항목은 <strong>*</strong> 표시가 되어 있습니다</p>

모션 — prefers-reduced-motion

전정 장애(vestibular disorder), 멀미, 주의력 결핍이 있는 사용자는 과도한 모션이 괴로움·어지럼증을 유발한다.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

그리고 중요한 애니메이션은 "재생/정지" 버튼 제공.

텍스트 확대 — 200%까지

사용자가 브라우저로 200% 확대해도 레이아웃이 깨지지 않고 수평 스크롤 없이 읽을 수 있어야 한다.

  • rem / em 단위 사용
  • max-width로 콘텐츠 폭 제한
  • 중요한 텍스트에 px 고정 크기 쓰지 않기

8장 · 폼 접근성 — 가장 흔한 실패 지점

Label은 반드시 Input과 연결

<!-- Bad — label이 input과 연결 안 됨 -->
<label>이메일</label>
<input type="email" />

<!-- Good — for/id 연결 -->
<label for="email">이메일</label>
<input id="email" type="email" required />

<!-- Alt — label로 감싸기 -->
<label>
  이메일
  <input type="email" required />
</label>

Placeholder는 Label이 아니다

<!-- Bad — label 대신 placeholder 사용 -->
<input type="email" placeholder="이메일" />

<!-- Good — label 있음 + placeholder는 힌트 -->
<label for="email">이메일</label>
<input id="email" type="email" placeholder="name@example.com" />

Placeholder는 입력하면 사라진다. 기억 의존을 강요하고, 대비 기준도 미충족. 반드시 별도 label이 필요하다.

에러 메시지 연결

<label for="pw">비밀번호</label>
<input
  id="pw"
  type="password"
  aria-invalid="true"
  aria-describedby="pw-help pw-error"
/>
<span id="pw-help">8자 이상 영문+숫자</span>
<span id="pw-error" role="alert">비밀번호가 너무 짧습니다</span>

Required Field

<!-- aria-required 추가 + 시각적 표시 -->
<label for="name">이름 <span aria-hidden="true">*</span></label>
<input id="name" required aria-required="true" />

Autocomplete

브라우저·비밀번호 관리자·보조기기 모두 활용. WCAG 1.3.5 Identify Input Purpose 준수.

<input type="email" autocomplete="email" />
<input type="tel" autocomplete="tel" />
<input type="password" autocomplete="current-password" />
<input type="password" autocomplete="new-password" />
<input name="name" autocomplete="name" />
<input name="postal" autocomplete="postal-code" />

9장 · i18n 기초 — 국제화의 구조

i18n vs l10n vs g11n

  • i18n (internationalization) — 제품을 여러 언어·지역에 대응할 수 있게 설계하는 것
  • l10n (localization) — 특정 지역·언어로 실제 번역·현지화하는 것
  • g11n (globalization) — 위 둘을 합친 비즈니스 전략

숫자는 각 단어의 첫/끝 글자 사이 문자 수. 개발자가 혼동하기 쉬운 줄임말.

i18n의 5대 축

  1. 텍스트 번역 — UI 문자열·마케팅 카피
  2. 형식(Formatting) — 숫자·날짜·시간·통화·주소·전화번호
  3. 정렬(Collation) — 이름·상품명 사전순 정렬이 언어마다 다름
  4. 방향(Directionality) — LTR(영어·한국어) vs RTL(아랍어·히브리어)
  5. UI 레이아웃 — 텍스트 확장(독일어는 영어의 30% 길어짐) 대응

2025년의 i18n 프레임워크

프레임워크스택
next-intlNext.js (App Router 우선)
react-intl / FormatJSReact 일반, ICU MessageFormat
i18next프레임워크 무관, 플러그인 풍부
vue-i18nVue 공식
Svelte-i18n / ParaglideSvelte
LinguiReact, macro 기반
@angular/localizeAngular 내장
Format.js저수준, ICU 기반 표준

2025년 Next.js App Router 시대의 de facto는 next-intl. Server·Client 양쪽에서 동일 API.


10장 · next-intl 실전 — Server Component 시대의 i18n

설치·설정

npm i next-intl
// i18n/request.ts
import { getRequestConfig } from "next-intl/server";

export default getRequestConfig(async ({ requestLocale }) => {
  const locale = (await requestLocale) ?? "ko";
  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
  const messages = await getMessages();
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Server Component에서 사용

// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";

export default async function Home() {
  const t = await getTranslations("home");
  return (
    <main>
      <h1>{t("title")}</h1>
      <p>{t("description")}</p>
    </main>
  );
}

메시지 파일 (ICU MessageFormat)

// messages/ko.json
{
  "home": {
    "title": "환영합니다",
    "description": "안녕하세요!",
    "cart": "{count, plural, =0 {장바구니가 비어있습니다} one {# 개 상품} other {# 개 상품}}",
    "welcome": "{name}님, 안녕하세요!"
  }
}
t("cart", { count: 0 }); // "장바구니가 비어있습니다"
t("cart", { count: 1 }); // "1 개 상품"
t("welcome", { name: "영주" }); // "영주님, 안녕하세요!"

ICU MessageFormat의 힘 — 복수형·성별·선택문을 메시지 파일에서 처리. 개발자가 JS 로직 안 짜도 됨.


11장 · Locale-Aware 포맷팅 — Intl API

Intl.NumberFormat

new Intl.NumberFormat("ko-KR").format(1234567);
// "1,234,567"

new Intl.NumberFormat("de-DE").format(1234567);
// "1.234.567" — 독일은 점이 천단위

new Intl.NumberFormat("hi-IN").format(1234567);
// "12,34,567" — 인도는 lakh·crore 단위

new Intl.NumberFormat("ko-KR", {
  style: "currency",
  currency: "KRW",
}).format(29000);
// "₩29,000"

Intl.DateTimeFormat

const d = new Date("2025-06-15T12:00:00Z");

new Intl.DateTimeFormat("ko-KR", { dateStyle: "full" }).format(d);
// "2025년 6월 15일 일요일"

new Intl.DateTimeFormat("ja-JP", { dateStyle: "full" }).format(d);
// "2025年6月15日日曜日"

new Intl.DateTimeFormat("ar-SA", { dateStyle: "full" }).format(d);
// "الأحد، 15 يونيو 2025 م"

Intl.RelativeTimeFormat

const rtf = new Intl.RelativeTimeFormat("ko", { numeric: "auto" });
rtf.format(-1, "day");   // "어제"
rtf.format(-2, "day");   // "2일 전"
rtf.format(0, "hour");   // "이번 시간"
rtf.format(1, "month");  // "다음 달"

Intl.ListFormat

const list = ["사과", "바나나", "포도"];

new Intl.ListFormat("ko", { type: "conjunction" }).format(list);
// "사과, 바나나 및 포도"

new Intl.ListFormat("en", { type: "conjunction" }).format(list);
// "사과, 바나나, and 포도"

Intl.Collator (정렬)

const names = ["ㄱ", "z", "A", "ㅎ", "가", "하"];
names.sort(new Intl.Collator("ko").compare);
// 한국어 사전순 정렬

교훈: 숫자·날짜·통화·리스트를 직접 포맷하지 마라. 브라우저 Intl API 또는 라이브러리(date-fns with locale, Luxon, Temporal)를 써라.


12장 · 한국어·일본어의 특수성 — 조사·줄바꿈·세로 쓰기

한국어 조사 처리

// Bad — 하드코딩
`${name}님이 좋아요를 눌렀습니다`; // "영주님이" vs "철수님이" — OK
`${product}을(를) 추가했습니다`; // "(을/를)" 사용자 혼란

// Good — 조사 자동 선택
function suffix(word: string, ko: string, kg: string) {
  const last = word.charCodeAt(word.length - 1);
  const hasBatchim = (last - 0xac00) % 28 !== 0;
  return hasBatchim ? ko : kg;
}
suffix("사과", "을", "를"); // "을"
suffix("배", "을", "를");   // "를"

es-hangul (Toss 오픈소스) 같은 라이브러리가 조사 처리·초성 추출·자모 분리를 통합 제공.

한국어 줄바꿈

/* 단어 중간에서 깨지지 않게 */
body {
  word-break: keep-all;
  overflow-wrap: break-word;
  line-break: strict;
}

keep-all은 한·중·일에서 단어(어절)를 깨뜨리지 않고 어절 경계에서 줄바꿈한다. 2025년 모든 브라우저 지원.

일본어 세로 쓰기 (tategaki)

.tategaki {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  font-family: "Yu Mincho", serif;
}

신문·소설·서예 스타일 사이트에서 필요. writing-mode는 CSS Level 3 표준.

한·중·일 폰트 최적화

  • 한국어: Pretendard (7MB → 서브셋 100KB)
  • 일본어: Noto Sans JP, Yu Gothic
  • 중국어(간체·번체): Noto Sans SC/TC
  • 서브셋 필수 — 전체 한글은 11,172자(KS X 1001 2,350자 권장), 일본어는 한자 10,000자 이상

RTL (아랍어·히브리어)

<html lang="ar" dir="rtl">
  ...
</html>

CSS 논리 속성(margin-inline-start, padding-inline-end)을 쓰면 자동으로 LTR/RTL 대응.

/* Bad — 물리적 방향 고정 */
.card { margin-left: 16px; }

/* Good — 논리적 방향 */
.card { margin-inline-start: 16px; }

13장 · 실전 체크리스트·안티패턴·다음 글 예고

출시 전 a11y 체크리스트 (15개)

  1. 모든 이미지에 의미 있는 alt 텍스트 (장식용은 alt="")
  2. 모든 폼 input에 연결된 label
  3. 대비 4.5:1 이상 (AA 기준)
  4. 페이지당 한 개의 <h1>, heading 레벨 건너뛰지 않음
  5. 키보드만으로 모든 기능 사용 가능
  6. 포커스 가시성(:focus-visible) 명시
  7. 터치 타겟 최소 24x24 (WCAG 2.2)
  8. 모달에 포커스 트랩 + Esc 닫기
  9. Skip Link 제공
  10. 모션 prefers-reduced-motion 대응
  11. 동적 콘텐츠에 aria-live 알림
  12. Lighthouse Accessibility score 100
  13. axe-core·Pa11y 자동 검사 CI 통합
  14. Screen Reader 수동 테스트 (NVDA·VoiceOver)
  15. 실사용자 테스트 (장애인 사용자 참여)

a11y·i18n 안티패턴 TOP 10

  1. <div onclick> 남발 — <button> 써라
  2. outline: none:focus-visible 스타일로 대체
  3. Placeholder를 label 대신 — 접근성·인지 부담
  4. Alt 텍스트에 "image of" — 중복, SR이 이미 "image"라고 읽음
  5. aria-hidden을 포커스 요소에 적용 — 모순 상태
  6. 색상만으로 정보 전달 — 색맹 차단
  7. 자동 재생 오디오·비디오 — 멀미·집중 방해
  8. i18n 하드코딩: "안녕하세요 " + name — 조사·어순 깨짐
  9. 통화·날짜를 문자열 조작으로 — Intl API 써라
  10. 번역을 Google Translate로 일괄 — 검수 없이 배포하면 문화적 실수

다음 글 예고 — Season 6 Ep 8: "모니터링과 에러 트래킹"

서비스가 "모두에게 잘 쓰일 수" 있게 만들었다면, 이제 실제로 잘 쓰이고 있는지를 확인할 차례. Ep 8은 프런트엔드 모니터링·에러 트래킹.

  • Sentry·Datadog RUM·PostHog·LogRocket·Bugsnag 2025 비교
  • Source Map과 에러 추적
  • Session Replay의 가치와 윤리
  • Core Web Vitals·Vercel Speed Insights·CrUX 연동
  • 사용자 피드백 루프 설계
  • 프런트엔드에서 발생하는 에러 유형 (Hydration Mismatch, Chunk Load Error, CORS)
  • AI 기반 이상 탐지
  • 알림 피로도(Alert Fatigue)와 노이즈 줄이기
  • Privacy-safe 로깅
  • GDPR·CCPA·한국 개인정보보호법 대응

"계측되지 않는 것은 개선되지 않는다. 하지만 모든 것을 계측하면 아무것도 개선되지 않는다."

다음 글에서 만나자.


"접근성은 소수를 위한 특별한 배려가 아니다. 모두가 쓸 수 있게 만드는 것이 기본이다. i18n은 번역이 아니다. 다른 문화·다른 리듬으로 사는 사람들을 존중하는 설계다."