스크린 리더

Screen Reader

화면을 보지 않고 음성으로 웹을 탐색하는 보조 기술. 접근성 트리를 DOM 순서대로 훑으며 요소의 역할과 이름을 읽어줍니다.

···
html
<div class="stage">
  <div class="pane page">
    <div class="tag">page</div>
    <div class="el h1" id="e0">Dashboard</div>
    <div class="el img" id="e1"></div>
    <div class="el p" id="e2"></div>
    <div class="el btn" id="e3">Export</div>
    <div class="cursor" id="cursor"></div>
  </div>
  <div class="pane sr">
    <div class="tag">SR</div>
    <div class="log" id="log"></div>
  </div>
</div>
css
.stage{width:94%;height:88%;display:flex;gap:4%}
.pane{position:relative;flex:1;border-radius:14px;border:1px solid var(--line);background:var(--surface);overflow:hidden}
.tag{position:absolute;top:8px;left:10px;font:700 10px/1 ui-monospace,monospace;color:var(--muted);z-index:2}
.page{padding:20% 9% 9%;display:flex;flex-direction:column;gap:7%}
.h1{font:800 clamp(11px,3vmin,15px)/1 ui-monospace,monospace;color:var(--fg)}
.img{height:24%;border-radius:6px;background:linear-gradient(135deg,var(--accent),var(--accent-3));opacity:.55}
.p{height:16%;border-radius:6px;background:var(--muted);opacity:.25}
.btn{align-self:flex-start;padding:6px 14px;border-radius:6px;background:var(--accent);color:#fff;font:700 10px/1 ui-monospace,monospace}
.cursor{position:absolute;border:2px dashed var(--accent-2);border-radius:6px;transition:all .4s ease;pointer-events:none;opacity:0}
.log{position:absolute;inset:26px 10px 10px;display:flex;flex-direction:column-reverse;gap:6px;font:10px/1.3 ui-monospace,monospace;color:var(--accent-3);overflow:hidden}
.log div{opacity:0;transform:translateY(4px);transition:opacity .3s ease,transform .3s ease}
.log div.show{opacity:1;transform:translateY(0)}
js
const page = document.querySelector('.page');
const cursor = document.getElementById('cursor');
const log = document.getElementById('log');
const items = [
  { el: document.getElementById('e0'), text: 'heading level 1, Dashboard' },
  { el: document.getElementById('e1'), text: 'image, revenue chart' },
  { el: document.getElementById('e2'), text: 'text, monthly summary paragraph' },
  { el: document.getElementById('e3'), text: 'button, Export' },
];
function addLine(text) {
  const line = document.createElement('div');
  line.textContent = text;
  log.prepend(line);
  requestAnimationFrame(function () { line.classList.add('show'); });
  while (log.children.length > 4) log.removeChild(log.lastChild);
}
function moveTo(el) {
  const pageBox = page.getBoundingClientRect();
  const box = el.getBoundingClientRect();
  cursor.style.opacity = '1';
  cursor.style.left = (box.left - pageBox.left - 4) + 'px';
  cursor.style.top = (box.top - pageBox.top - 4) + 'px';
  cursor.style.width = (box.width + 8) + 'px';
  cursor.style.height = (box.height + 8) + 'px';
}
let i = 0;
function tick() {
  const cur = items[i % items.length];
  moveTo(cur.el);
  addLine(cur.text);
  i++;
}
tick();
setInterval(tick, 1400);

VoiceOver, NVDA, JAWS, TalkBack 같은 스크린 리더는 화면의 픽셀이 아니라 브라우저가 만든 접근성 트리(accessibility tree)를 읽습니다. 가상 커서(virtual cursor)가 한 번에 요소 하나씩 이동하며 그 요소의 역할(heading, button, image...), 이름(accessible name), 상태(선택됨, 펼쳐짐...)를 순서대로 읽어줍니다.

이 순서는 화면에 보이는 위치가 아니라 **DOM 순서**를 따릅니다. CSS로 시각적 순서를 바꿔도(`order`, `position: absolute`) 스크린 리더가 읽는 순서는 그대로라서, 시각 순서와 DOM 순서가 크게 어긋나면 혼란스러운 경험이 됩니다.

데모는 미니 페이지 위에서 가상 커서가 제목 → 이미지 → 본문 → 버튼 순서로 이동하며, 오른쪽 트랜스크립트에 실제로 읽히는 문장이 하나씩 쌓이는 과정을 보여줍니다.

언제 쓰나

레이아웃을 짤 때 시각 순서와 DOM 순서를 크게 다르게 만들지 마세요. 확인하려면 실제 스크린 리더(맥 VoiceOver는 Cmd+F5)를 한 번 켜서 Tab만으로 페이지를 읽어보는 게 가장 정확합니다.