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

- Name
- Youngju Kim
- @fjvbn20031
프롤로그 — "성능은 잘 나왔는데 쓸 수가 없어요"
Core Web Vitals 지표가 파릇파릇 초록색이어도 화면 낭독기로는 리스트가 전혀 안 읽히고, 영어 버전은 멀쩡한데 한국어에서는 조사가 깨지고 줄바꿈이 어색한 서비스가 있다. 그제야 "아 이거 처음부터 설계했어야 했구나"라는 말이 나온다. 2025년의 접근성과 국제화는 더 이상 출시 직전에 땜질하는 옵션이 아니다.
2025년 전환점 세 가지:
- EU Accessibility Act 발효(2025.06.28) — EU 시장에 서비스를 제공하는 모든 디지털 제품(웹·앱·이커머스·e북·ATM·교통 단말기)에 WCAG 기준 적용이 법적 의무로 못 박혔다. 위반 시 시장에서 제외(withdraw from market)당할 수 있다.
- WCAG 2.2 정식 권고안(W3C Recommendation, 2023.10) — 터치 타겟 최소 크기, 드래그 조작 대체 수단, 인증 인지 부담 등 모바일·인지 장애 관점이 대폭 강화되었고, 2025년 대부분의 공공·대기업 조달 기준으로 자리 잡았다.
- AI·LLM 기반 다국어 품질의 상향 평준화 — 기계 번역은 이제 "빠르고 저렴하지만 어색한 선택지"가 아니다. 다만 조사·존댓말·문화적 맥락에서는 여전히 사람의 손이 필수다.
그리고 한 가지 더, 한국에서는 장애인차별금지법 + 행정안전부 전자정부법으로 공공·금융·교육 분야의 웹 접근성이 이미 의무화되어 있으며, 2024년 이후 민간으로 확산 중이다.
"접근성은 비용이다"라고 믿었던 시대는 끝났다. 접근성은 SEO 향상 + 이탈률 감소 + 신규 사용자 유치 + 소송 리스크 해소가 결합된 품질 투자다.
이번 글에서는 2025년의 a11y·i18n을 13개 챕터로 정리한다.
1장 · 접근성을 다시 정의한다 — "모두의 품질"
전통적 정의 (2000년대)
"장애인을 위한 보조 수단" → 소수 사용자용 기능. 비용 센터. 법적 compliance.
2025년의 정의
모두의 품질. 다음 6가지 상황을 모두 포괄한다.
- 영구적 장애 — 시각·청각·운동·인지 장애
- 일시적 장애 — 팔 골절, 수술 후 시력 변화, 약물 부작용
- 상황적 제약 — 햇빛 아래 화면 못 봄, 소음 속 청취 불가, 한 손이 아기로 막힘
- 고령화 — 시력·청력·인지 능력의 점진적 저하
- 디바이스 다양성 — 소형 스마트폰·거대한 TV·저사양 기기·느린 네트워크
- 언어·문화 다양성 — 비영어권, 소수 언어, 문해력 차이
전 세계 약 **16%(13억 명)**가 영구적 장애를 가지며, 위 6가지를 모두 합치면 거의 모두가 어느 시점에 접근성이 필요하다. 이게 "모두의 품질"이라는 이유다.
a11y·i18n 체크 하나로 수십 개의 품질 지표가 개선된다
- 명확한 heading 구조 → SEO 랭킹 상승
- 명확한 alt 텍스트 → 이미지 SEO + 봇 크롤링
- 대비 높은 색상 → 햇빛 아래서도 잘 읽힘
- 크고 충분한 터치 타겟 → 노인·오타 감소
- 키보드 내비게이션 → 개발자 도구·파워 유저 호환
- 명확한 에러 메시지 → 전환율 향상
- 다국어 지원 → 매출 +N% (Google 조사: 완전한 현지화로 구매 의도 72% 증가)
2장 · WCAG 2.2 — 2025년의 필수 기준
WCAG의 4대 원칙 (POUR)
- Perceivable (인지 가능) — 모든 정보는 사용자가 감지 가능해야 한다 (텍스트·대체 텍스트·자막)
- Operable (운용 가능) — UI 컨트롤은 모두 조작 가능해야 한다 (키보드·터치·음성)
- Understandable (이해 가능) — 정보와 조작 방법은 이해할 수 있어야 한다 (명확한 언어·예측 가능한 동작)
- 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를 써야 하는 경우
- 네이티브 HTML로 표현 불가능한 UI (탭, 콤보박스, 트리)
- 동적 상태 표현 (
aria-expanded,aria-selected,aria-busy) - 이름·설명 추가 (
aria-label,aria-labelledby,aria-describedby) - 라이브 리전 (
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
<div role="button">— 그냥<button>써라aria-label="이미지"— 정보성 이미지는<img alt="구체적 설명">role="list"+<li>대신<div>— 그냥<ul>써라aria-hidden="true"on<main>— 앱 전체가 screen reader에서 사라진다- 포커스 가능 요소에
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 같은 라이브러리 사용.
Skip Links — 스크린 리더·키보드 사용자의 생명줄
<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 | 플랫폼 | 설치 | 점유율 |
|---|---|---|---|
| NVDA | Windows | 무료 | WebAIM 조사 67% |
| JAWS | Windows | 유료 | 30% |
| VoiceOver | macOS·iOS | 내장 | 7% |
| TalkBack | Android | 내장 | 모바일 주류 |
VoiceOver 단축키 (macOS)
Cmd+F5— VoiceOver 켜기/끄기Ctrl+Option+→/←— 다음·이전 요소Ctrl+Option+Space— 활성화Ctrl+Option+U— 로터(landmarks·headings·links 목록)
NVDA 단축키 (Windows)
Insert+Down— 읽기 시작H/Shift+H— 다음·이전 headingK/Shift+K— 다음·이전 linkD— 랜드마크 목록NVDA+Space— Focus/Browse mode 토글
테스트 체크리스트
- Tab으로 모든 인터랙티브 요소 접근 가능한가?
- 포커스 순서가 시각적 순서와 일치하는가?
- 버튼·링크에 역할(role)이 정확히 읽히는가?
- 폼 라벨이 입력 필드에 연결되어 있는가?
- 에러 메시지가 실시간으로 전달되는가?
- 모달 열고 닫을 때 포커스 관리가 되는가?
- 동적 콘텐츠(무한 스크롤·알림)가 안내되는가?
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대 축
- 텍스트 번역 — UI 문자열·마케팅 카피
- 형식(Formatting) — 숫자·날짜·시간·통화·주소·전화번호
- 정렬(Collation) — 이름·상품명 사전순 정렬이 언어마다 다름
- 방향(Directionality) — LTR(영어·한국어) vs RTL(아랍어·히브리어)
- UI 레이아웃 — 텍스트 확장(독일어는 영어의 30% 길어짐) 대응
2025년의 i18n 프레임워크
| 프레임워크 | 스택 |
|---|---|
| next-intl | Next.js (App Router 우선) |
| react-intl / FormatJS | React 일반, ICU MessageFormat |
| i18next | 프레임워크 무관, 플러그인 풍부 |
| vue-i18n | Vue 공식 |
| Svelte-i18n / Paraglide | Svelte |
| Lingui | React, macro 기반 |
| @angular/localize | Angular 내장 |
| 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개)
- 모든 이미지에 의미 있는 alt 텍스트 (장식용은
alt="") - 모든 폼 input에 연결된 label
- 대비 4.5:1 이상 (AA 기준)
- 페이지당 한 개의
<h1>, heading 레벨 건너뛰지 않음 - 키보드만으로 모든 기능 사용 가능
- 포커스 가시성(
:focus-visible) 명시 - 터치 타겟 최소
24x24(WCAG 2.2) - 모달에 포커스 트랩 +
Esc닫기 - Skip Link 제공
- 모션
prefers-reduced-motion대응 - 동적 콘텐츠에
aria-live알림 - Lighthouse Accessibility score 100
- axe-core·Pa11y 자동 검사 CI 통합
- Screen Reader 수동 테스트 (NVDA·VoiceOver)
- 실사용자 테스트 (장애인 사용자 참여)
a11y·i18n 안티패턴 TOP 10
<div onclick>남발 —<button>써라outline: none—:focus-visible스타일로 대체- Placeholder를 label 대신 — 접근성·인지 부담
- Alt 텍스트에 "image of" — 중복, SR이 이미 "image"라고 읽음
aria-hidden을 포커스 요소에 적용 — 모순 상태- 색상만으로 정보 전달 — 색맹 차단
- 자동 재생 오디오·비디오 — 멀미·집중 방해
- i18n 하드코딩:
"안녕하세요 " + name— 조사·어순 깨짐 - 통화·날짜를 문자열 조작으로 —
IntlAPI 써라 - 번역을 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은 번역이 아니다. 다른 문화·다른 리듬으로 사는 사람들을 존중하는 설계다."