9월 1일, Claude Code v2.1.257 릴리스 노트에 짧은 한 줄이 실렸습니다. Fable 계열의 기본 모델이 5.1로 바뀌었다는 내용이에요. Fable을 쓰면서 설정을 건드린 적 없는 사용자라면, 이날부터 코드를 만들어주는 두뇌가 조용히 교체된 겁니다.
같은 릴리스 노트에 예외도 있습니다 — Claude 앱 게이트웨이 세션의
fable별칭은 당분간 Fable 5를 유지한다고 해요. 터미널에서 쓰는 보통의 Claude Code 이야기로 읽으시면 됩니다.
보통은 “더 좋아졌겠지” 하고 넘어갈 일입니다. 그런데 저는 이런 게 궁금했습니다. AI에게 접근성을 시키지 않았을 때, AI가 알아서 지키는 선은 모델마다 다를 수 있습니다. 그 선이 팀의 기본값이 되고요. 기본 모델이 바뀌면 그 선도 움직였을까요?
궁금하면 재보면 됩니다. 처음엔 Fable 두 세대만 비교할 생각이었는데, 결과가 “거의 같다"로 나오는 바람에 질문이 하나 더 생겼습니다. 그게 Fable 얘기인지, Claude 전체 얘기인지. 그래서 Opus 5와 Sonnet 5를 같은 실험에 세웠습니다. 네 모델을 같은 잣대로 재놓으면, 다음 모델이 나올 때 열 하나만 더하면 되니까요.

사진: James William / Unsplash
실험 설계 — 접근성을 일부러 말하지 않았습니다#
비교 대상은 넷입니다.
| 모델 ID | 비교에서의 역할 |
|---|---|
claude-fable-5 | 교체 전까지의 기본 Fable 모델 |
claude-fable-5-1 | v2.1.257부터의 기본 Fable 모델 |
claude-opus-5 | Opus 급 |
claude-sonnet-5 | Sonnet 급 |
프롬프트는 접근성 문제가 잘 드러나는 UI 두 가지로 잡았습니다. 폼 접근성과 aria-live 라이브 리전을 다루면서 뭐가 빠지면 뭐가 망가지는지 알게 된 영역이라, 채점 기준을 제가 갖고 있거든요.
- 모달: “삭제 확인 모달을 순수 HTML/CSS/JS 단일 파일로 만들어줘. 페이지에 ‘항목 삭제’ 버튼이 있고, 누르면 ‘정말 삭제할까요?’ 모달이 열리고 취소/삭제 버튼이 있어.”
- 폼: “이메일과 비밀번호를 받는 회원가입 폼을 실시간 검증과 함께 순수 HTML/CSS/JS 단일 파일로 만들어줘. 형식이 틀리면 에러 메시지를 보여줘.”
눈치채셨겠지만 프롬프트 어디에도 접근성이라는 말이 없습니다. 일부러 뺐어요. “접근성 지켜서 만들어줘"라고 하면 어느 모델이든 열심히 합니다. 제가 재고 싶은 건 시키지 않았을 때의 기본값입니다. 현실의 프롬프트 대부분이 그렇게 생겼으니까요.
고백하자면 저 두 줄에서 ‘접근성’이라는 단어를 빼는 게 제일 어색했습니다. 접근성 글만 쓰다 보니 그 단어가 없는 프롬프트가 어딘가 벌거벗은 것처럼 보이더라고요. 그래도 참았습니다. 실험이니까요.
모델·프롬프트 조합마다 5회씩, 총 40개 파일을 생성했습니다. AI 생성은 같은 질문에도 매번 다른 답이 나오니, 한 번 잘 나온 걸로 “이 모델은 잘한다"고 말할 수는 없어서요. 호출은 전부 같은 버전의 Claude Code에서 claude -p --model로 했고, 런(모델 호출 1회)마다 JSON 출력의 modelUsage를 남겨 요청한 모델이 실제로 응답했는지 확인했습니다. 40건 모두 일치합니다.
검사는 두 겹으로 했습니다.
- 자동 검사: 40개 파일을 A11y Check로 일괄 스캔해 WCAG 2.2 AA·KWCAG 2.2 위반 수를 셌습니다(제가 만든 검사 도구입니다).
- 패턴 검사: 자동 도구가 못 잡는 것들 — 포커스 트랩(모달 밖으로 Tab이 못 새게 가두기), 닫을 때 포커스 복원,
aria-invalid연결(입력값이 틀렸다는 상태를 스크린 리더에 알리는 속성) 같은 동작 코드가 들어 있는지 40개 파일을 직접 확인했습니다.
자동 검사 도구는 정적인 상태만 봅니다. “모달이 열렸을 때 Tab이 배경으로 새는가” 같은 동작 문제는 코드를 읽어야 알 수 있어서 두 번째 겹이 필요합니다.
결과 1 — 자동 검사: Fable은 20개 전부 0, 나머지는 명도 대비에서 걸렸습니다#
A11y Check 스캔 결과입니다.
| 모델 | 모달 5개 위반 수 | 폼 5개 위반 수 | 걸린 것 |
|---|---|---|---|
| fable-5 | 0 | 0 | — |
| fable-5-1 | 0 | 0 | — |
| opus-5 | 1 | 0 | 12px 보조 문구, 대비 3.32:1 |
| sonnet-5 | 1 | 2 | 15px 빨간 삭제 버튼 4.37:1, 12px 회색 힌트 3.54:1 ×2 |
스캔 40개에 1분이 안 걸렸습니다. 결과를 읽는 데는 3초, “어, 이게 다야?” 하는 데 5초. 그리고 그다음 문장을 쓰는 데 한 시간이 걸렸습니다.
걸린 네 건이 전부 명도 대비(글자색과 배경색의 밝기 차이를 비율로 잰 값)입니다. 그것도 전형적인 자리예요 — 12px짜리 회색 보조 문구, 그리고 흰 글씨를 얹은 빨간 버튼. 기준은 일반 텍스트 4.5:1인데 4.37:1처럼 아슬아슬하게 못 미친 것도 있고, 3.3:1처럼 확실히 미달인 것도 있습니다. 디자이너가 손으로 만들 때도 제일 자주 걸리는 지점을 AI도 똑같이 밟습니다.
Fable 두 세대는 20개 전부 위반 0이었습니다. 그런데 이건 안심할 결과가 아니라 함정을 보여주는 결과이기도 합니다. 모달 페이지의 초기 화면을 생각해보세요. 버튼 하나 있는 페이지입니다. 모달은 아직 닫혀 있으니 검사기 눈에는 보이지도 않아요. “모달이 열렸을 때 포커스가 갇히는가, 닫으면 어디로 돌아가는가” 같은 진짜 문제는 전부 동작의 영역이라 정적 검사가 닿지 못합니다.
100%가 의미 없다는 얘기는 아닙니다. 레이블 연결이나 문서 구조 같은 정적 영역은 실제로 통과했다는 뜻이니까요. 다만 이 실험의 진짜 질문, 그러니까 열린 모달에서 포커스가 어떻게 움직이는가는 정적 검사의 관할 밖입니다. 자동 검사가 말해주는 건 딱 거기까지고, 그래서 40개 파일의 코드를 직접 읽었습니다.

실제 점검 출력을 그대로 옮긴 캡처입니다. 아래 표의 숫자는 전부 여기서 나왔습니다.
결과 2 — 코드를 직접 읽으니: 모달은 교과서가 달랐고, 폼은 급이 갈렸습니다#
솔직히 빨간펜을 준비하고 읽기 시작했는데, 대부분은 모범답안이었습니다. 한 모델은 모범답안이되 다른 교과서를 본 것 같았고요.

사진: Kelly Sikkema / Unsplash
넷 다 5/5로 통과한 것부터 적어둡니다 — 모달의 대화 상자 시맨틱과 제목 연결(aria-labelledby), 폼의 레이블 연결·에러 라이브 리전(화면을 못 보는 사용자에게도 에러 발생이 소리로 전달되게 하는 알림 영역)·autocomplete="new-password". 이건 바닥선이라 표에서 뺐고, 표엔 갈린 것만 남겼습니다. 숫자는 5회 생성 중 그 패턴이 들어간 횟수이고, 행 이름 끝에 “결함"이나 “권고 역행"이 붙은 줄만 거꾸로 읽으시면 됩니다 — 거기는 0/5가 좋은 거예요.
모달 (모델당 5회 생성)
| 항목 | fable-5 | fable-5-1 | opus-5 | sonnet-5 |
|---|---|---|---|---|
<dialog> + showModal() 사용 | 5/5 | 5/5 | 5/5 | 0/5 |
| 배경 비활성화 (showModal·inert·aria-hidden) | 5/5 | 5/5 | 5/5 | 0/5 |
| 초기 포커스를 취소 버튼에 (파괴적 동작 배려) | 5/5 | 5/5 | 4/5 | 5/5 |
| 닫을 때 포커스 복원 (명시 코드) | 5/5 | 5/5 | 4/5 | 5/5 |
비활성화한 버튼에 focus() — 결함 | 2/5 | 0/5 | 0/5 | 0/5 |
Fable 두 세대와 Opus는 같은 길을 갔습니다. <dialog>를 showModal()로 열고, aria-modal(모달 밖은 지금 무시하라고 보조기기에 알리는 속성)·ESC·포커스 트랩 코드는 쓰지 않았어요. 그게 잘한 겁니다 — showModal()이면 aria-modal과 ESC는 브라우저가 해주고, 배경이 통째로 비활성(inert — 클릭·포커스·보조기기 탐색까지 한꺼번에 막는 상태)이 되니 Tab이 배경으로 샐 일도 없거든요. 열 줄로 끝나는 네이티브를 15회 모두 골랐습니다. fable-5의 한 런은 이런 주석까지 달아뒀습니다. 시키지 않았는데요.
openBtn.addEventListener("click", () => {
dialog.showModal();
// 파괴적 동작이므로 안전한 쪽(취소)에 먼저 포커스
cancelBtn.focus();
});Sonnet은 다른 교과서였습니다. <dialog>를 한 번도 안 쓰고 div로 직접 만들었어요. 그런데 못 만든 게 아닙니다. 5회 모두 role="alertdialog"(응답이 꼭 필요한 대화 상자라는 뜻의 역할. 한 런은 dialog)에 aria-modal, ESC 닫기, Tab 순환 트랩, 열기 전 포커스를 저장했다가 닫을 때 되돌리는 코드까지 손으로 다 넣었습니다. WAI-ARIA APG(공식 패턴 가이드)가 지금도 가르치는 정석 그대로예요. 네이티브 <dialog>가 자리 잡기 전까지 모두가 이렇게 만들었죠. 포커스 트랩을 손으로 짜본 분은 아실 겁니다. Tab은 잘 도는데 Shift+Tab을 빼먹어서 QA에게 잡히는 그 순간을요. Sonnet은 다섯 번 다 Shift+Tab까지 챙겼습니다.
빠진 건 하나 — 배경을 비활성화하지 않았습니다. Tab은 JS 트랩이 막아주지만, 스크린 리더의 탐색 커서(Tab과 별개로, 화면 내용을 줄 단위로 읽어 내려가는 커서)까지 가두는 건 aria-modal만으로는 스크린 리더 재량이에요. JAWS·NVDA는 대체로 가둬 주지만 iOS VoiceOver처럼 모달 뒤 콘텐츠로 넘어가는 경우가 있어서, ARIA 스펙도 배경을 inert로 만들라고 적어 두고 있습니다(SHOULD). 네이티브 <dialog>가 공짜로 주던 걸 수제로 만들면서 하나 흘린 셈입니다.
모달 표의 결함 행은 이렇습니다. 이전 Fable의 2개 런은 삭제가 확정되면 “항목 삭제” 버튼을 disabled로 만든 다음 그 버튼에 focus()를 불렀습니다. 비활성 버튼은 포커스를 못 받으니 호출은 조용히 실패하고, 포커스는 문서 처음으로 떨어집니다. 새 Fable과 Opus의 대응 런은 같은 상황에서 상태 메시지나 빈 상태 영역에 tabindex="-1"을 주고 그쪽으로 포커스를 옮겼어요. 권장하는 처리예요. Opus의 4/5 둘 중 하나는 반대 방향 실수였습니다 — 초기 포커스를 삭제 버튼에 뒀어요. Enter 한 번이면 삭제되는 모달입니다. “정말 삭제할까요?” 하고 물어놓고 커서를 삭제 버튼 위에 올려두는 건, 회의 끝에 “반대 없으시죠?” 하고 3초 만에 닫는 것과 비슷해요. 다른 하나는 닫을 때 포커스를 돌려보내는 코드를 따로 쓰지 않은 런인데, showModal()로 연 <dialog>는 닫힐 때 브라우저가 이전 포커스로 돌려보내니 실사용엔 문제가 없고 명시 코드 기준으로만 빈 칸입니다.
폼 (모델당 5회 생성)
표의 “blur 이후"는 사용자가 필드를 떠나기 전까진 잔소리하지 않는다는 뜻입니다. 처음 보는 사람에게 첫 글자부터 “형식이 틀렸습니다"라고 따지는 폼, 쓰고 싶지 않잖아요.
| 항목 | fable-5 | fable-5-1 | opus-5 | sonnet-5 |
|---|---|---|---|---|
aria-invalid로 에러 상태 표시 | 5/5 | 5/5 | 5/5 | 2/5 |
| 검증을 시작하는 시점 | blur 이후 | blur 이후 | blur 이후 | 첫 타이핑부터 |
제출 버튼을 disabled로 잠금 — 권고 역행 | 0/5 | 0/5 | 0/5 | 5/5 |
여기서 급이 확실히 갈렸습니다. Fable과 Opus는 15회 모두 touched 같은 플래그를 두고 필드를 떠난 뒤부터 검증했고, 제출 버튼은 살려둔 채 제출 시점에 검증했습니다. Sonnet은 5회 모두 첫 타이핑부터 즉시 검증했고, 5회 모두 필드가 다 찰 때까지 제출 버튼을 disabled로 잠갔어요. disabled 버튼은 포커스도 못 받고 왜 안 되는지 알려주지도 않아서, 접근성에선 피하라는 쪽입니다. 표에서 하나 더 갈린 곳은 aria-invalid(2/5)입니다.
오해는 마세요. Sonnet의 폼도 레이블은 전부 연결돼 있고 에러는 라이브 리전으로 읽힙니다. 망가진 폼이 아니라 다른 기본값을 가진 폼이에요. 즉시 검증과 잠긴 제출 버튼은 웹에 아주 흔한 패턴이고, Sonnet은 그 흔한 쪽을 골랐습니다. Fable과 Opus는 덜 흔하지만 접근성 쪽이 권하는 쪽을 골랐고요.
결론 — 세대 교체는 접근성 기본값 그대로, 급이 다르면 기본값도 다릅니다#
재보기 전 제 가설은 “5.1이 더 똑똑하니 접근성도 더 잘하겠지"였습니다. 데이터는 똑똑함과 손버릇이 다른 축이라고 하네요. 두 질문에 답이 따로 나왔습니다.
Fable 5에서 5.1로의 교체는 기준선을 움직이지 않았습니다. 결함 행 하나만 빼면 전 항목 동률이고, 갈린 그 하나는 삭제 후 비활성 버튼에 포커스를 돌려보내는 가장자리 실수(2/5 → 0/5)입니다. 5회 표본으로 “5.1이 더 낫다"고 단정할 순 없지만, 나빠진 곳은 없습니다.
급이 다르면 기준선이 다릅니다. Fable과 Opus는 거의 같은 프로필이고, Sonnet은 뚜렷이 다른 프로필입니다. 네이티브 대신 수제, 관대한 타이밍 대신 즉시 검증, 살려둔 버튼 대신 잠긴 버튼. 어느 쪽도 “접근성을 모른다"고 할 순 없어요. 다만 시키지 않았을 때 손이 가는 방향이 다릅니다.
이게 실무에서 뜻하는 건 하나입니다. 팀이 쓰는 모델의 급이 바뀌면 접근성 리뷰 체크리스트도 바뀌어야 합니다. 같은 급 안의 세대 교체라면, 이번엔, 그대로 둬도 됐고요. Fable·Opus를 쓰는 팀이라면 모달·폼의 기본기는 대체로 나오니 리뷰 시간을 가장자리(삭제 후 포커스, 초기 포커스 위치)에 쓰면 되고, Sonnet을 쓰는 팀이라면 모달의 배경 비활성화와 폼의 검증 타이밍·제출 버튼을 리뷰 항목 맨 위에 올려야 해요. 같은 회사 모델인데 리뷰 포인트가 다릅니다.
직접 재보려면 — 스크립트와 순서#
여기까지 읽고 “그래서 우리 팀 모델은?” 싶으셨다면, 그러라고 스크립트를 남겼습니다. 실험에 쓴 파일 그대로 GitHub 데모 폴더에 있어요. 준비물은 셋입니다 — 로그인된 Claude Code CLI(claude -p "OK"가 답하면 준비 끝), Python 3(표준 라이브러리만 씀), 자동 검사기 하나.
1. 생성. 핵심은 이 한 줄입니다. --output-format json으로 받아야 실제 응답 모델이 modelUsage에 찍힙니다.
printf '%s' "$PROMPT" | claude -p --model claude-sonnet-5 --output-format json > run1.json데모 폴더의 gen.sh는 이걸 모델 × 프롬프트 2종 × 5회로 돌리고, 응답 모델을 .meta.json에 따로 남깁니다.
./gen.sh claude-fable-5-1,claude-opus-5,claude-sonnet-5Claude Code 세션 안에서 다른
claude를 부르면 부모 세션의 인증 변수가 섞여 401이 날 수 있습니다.gen.sh가ANTHROPIC_*·CLAUDE*변수를 걷어내고 호출하는 이유예요.
2. 자동 검사. 저는 A11y Check의 Claude Code 플러그인으로 로컬 폴더를 통째로 검사했습니다(scan_dir). 웹에서 쓰려면 생성물을 어딘가 올려 A11y Check에 URL을 넣으면 되고, 오픈소스로만 가려면 로컬 서버를 띄우고 axe CLI를 돌리면 됩니다.
npx serve gen
npx @axe-core/cli http://localhost:3000/claude-sonnet-5/P_MODAL/run1.html3. 패턴 집계. 자동 검사가 못 보는 동작 코드는 check.py가 셉니다.
python3 check.py claude-fable-5-1,claude-opus-5,claude-sonnet-5한 가지만 당부드리면 — 이 스크립트의 정규식은 이번 생성물에 맞춰 조정한 것입니다. 다른 프롬프트에선 오탐이 납니다. 이 글을 쓰는 동안에도 두 번 틀렸고(변수명이 openButton이라 openBtn을 못 잡았고, <dialog> 없는 모달의 포커스 복원을 0으로 셌습니다), 둘 다 파일을 직접 읽어서 잡았어요. 숫자를 믿기 전에 파일을 여세요.
이 실험이 말해주지 않는 것#
- 단순한 UI 2종, 짧은 프롬프트, 모델당 5회입니다. “우리 디자인 시스템의 Modal 컴포넌트로 만들어줘"처럼 네이티브
<dialog>를 못 쓰는 조건이 붙으면, 그러니까 브라우저가 해주던 걸 전부 직접 해야 하는 순간이 오면 Fable·Opus의 결과도 Sonnet 쪽으로 내려올 수 있습니다. - 같은 버전의 Claude Code(시스템 프롬프트 동일)에서 모델만 바꿔 쟀습니다. 모델 차이는 통제됐지만, CLI 업데이트가 함께 실어 오는 변화까지 대변하진 않습니다.
- Sonnet의 “즉시 검증·잠긴 버튼"이 나쁜 UX라고 단정하는 글이 아닙니다. 접근성 쪽 권고와 어긋난다는 것까지만 말할 수 있어요.
- 그리고 기본값이 높다는 게 검토를 건너뛰어도 된다는 뜻은 아닙니다. 자동 검사는 Fable 20개 전부에 100%를 줬지만, 포커스가 증발하는 런은 그 안에 있었습니다. 동작을 읽는 눈은 여전히 사람 몫이에요.
한 장 요약#
- Claude Code v2.1.257부터 기본 Fable 모델이
claude-fable-5-1로 교체됐습니다 - 접근성을 언급하지 않은 프롬프트 2종 × 네 모델 × 5회 = 40개 생성물을 실측했고, 런마다 실제 응답 모델을 기록해 대조했습니다
- 자동 검사(WCAG 2.2 AA·KWCAG 2.2): Fable 20개 위반 0, Opus 1건, Sonnet 3건 — 전부 12px 보조 문구·빨간 버튼의 명도 대비
- Fable 5 → 5.1은 결함 행 하나만 빼면 전 항목 동률. 갈린 그 하나는 삭제 후 비활성 버튼에 포커스를 돌려보내는 가장자리 실수(2/5 → 0/5)
- Fable·Opus는 네이티브
<dialog>·blur 이후 검증·살려둔 제출 버튼, Sonnet은 수제 모달(트랩·복원은 갖췄지만 배경 비활성화 누락)·즉시 검증·잠긴 제출 버튼 — 모달·검증 타이밍·제출 버튼 세 군데서 기본값이 갈립니다
실험에 쓴 프롬프트와 집계 기준은 본문에 그대로 있으니, 다음 모델이 나올 때 같은 실험에 열 하나를 더하면 이 글이 비교 기준점이 됩니다. 그러라고 재놓은 숫자들이기도 합니다.
