C.W.K.
Stream
Lesson 05 of 05 · published

<div> 는 첫 도구가 아니라 마지막 도구야

~12 min · div, span, aria, custom-elements, mental-model

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"
가 악마인 게 아니야. 첫 반사 자리에 그걸 꺼내는 게 악마야."

반사 문제

주니어 개발자가 페이지 짓는 거 봐. 패턴이 거의 보편적이야: 영역들 wireframe 하고, <div class="header">, <div class="nav">, <div class="sidebar">, <div class="content">, <div class="footer"> 를 타이핑. 그러고 각자 스타일링. 페이지가 제대로 렌더돼.

접근성 감사도 떨어지고, SEO 점수도 낮고, 누군가 ARIA 를 retrofit 하거나 React 를 통합하거나 screen reader 테스트를 돌리려는 순간 청구되는 미래 세금도 만들어. 그 <div> 들 하나하나가 완벽한 semantic 대안이 있었어.

<div> 가 맞는 선택일 때

이 element 가 이유 있어 존재해. <div> 를 쓸 때:

  • 의미 없는 layout wrapper. Semantic 자식 셋을 안은 flex 컨테이너. Heading 과 단락을 안은 grid item. Wrapper 는 layout 용으로 존재 — 그 이상은 없어.
  • Semantic 콘텐츠 없는 스크롤 영역. Custom 스크롤 컨테이너, virtualized 리스트 viewport.
  • Semantic 역할 없는 JavaScript hook. 서드파티 위젯 mount 자리. Click-outside detector. Intersection-observer 센티넬.
  • ARIA composition wrapper. 진짜로 custom 인터랙티브 위젯 (combobox, custom slider) 이 필요하고 어떤 native element 도 안 맞을 때, <div role="..."> + 키보드 핸들러 + ARIA 가 비상구.

이 네 경우 모두에서, <div> 는 구조 또는 행동 일을 하지 semantic element 자리에 서 있는 게 아니야. 그게 테스트.

<span> 도 같은 규칙

<div> 의 인라인 등가물. 인라인 semantic element 먼저 (<em>, <strong>, <code>, <mark>, <time>, <abbr>, <kbd>). 어떤 것도 안 맞고 텍스트 일부에 스타일링 hook 이 필요할 때만 <span> 으로 빠져. 노란 배경에 <span class="highlight"> 는 거의 항상 <mark> 오용이야.

ARIA 가 Semantic 갭 채워

HTML 어휘는 유한해. 일부 UI 패턴 (combobox, dialog, tab list, tree grid) 은 native HTML element 가 없어. ARIA (Accessible Rich Internet Applications) 가 보조 기술과 함께 작동하는 role 과 property 의 병렬 어휘를 제공:

  • role="button"<div> 를 screen reader 가 button 으로 announce 하게 만들어 — 하지만 키보드 (Enter, Space), focus 상태, disabled 상태는 직접 처리해야 해. 그냥 <button> 써. ARIA-button 은 마지막 수단.
  • role="dialog" + aria-modal="true" + focus trap = custom modal. 또는 native <dialog> element 를 쓰면 이걸 다 공짜로 줘.
  • aria-label="Close menu" 는 보이는 텍스트가 부족할 때 접근성 이름 제공 (icon-only 버튼). 이름이 다른 element 에 살 때 aria-labelledby 와 페어.
  • aria-describedby 는 입력에 연결된 help 텍스트용.
  • aria-live="polite" 는 콘텐츠가 동적으로 업데이트될 때 screen reader announcement (toast 알림, 저장 확인).
ARIA 의 첫 번째 규칙: ARIA 쓰지 마. WAI 가 문자 그대로 이렇게 발표해. Native HTML element 는 내장 role, 키보드 처리, focus 관리, screen-reader semantic 을 가져. ARIA 는 native 어휘가 안 커버하는 갭 채우는 용. <div> 위 모든 role="button" 은 떠오를 버그를 기다리는 중.

Custom Element: 다음 단계 비상구

진짜로 재사용 가능한 행동을 가진 새 종류 element 가 필요할 때, Web Components 명세가 정의하게 해 줘: <pippa-chat>, <avatar-expression>, <quest-card>. JavaScript 로 등록되고 다른 HTML 태그처럼 행동해. Custom element 는 이름에 hyphen 가져야 함 (필수), Shadow DOM 으로 스타일 encapsulate, framework 와 잘 어울려. "<div> 가 필요한데 풍부한 행동을 가진" 의 원칙적 답.

매일의 작성 습관

<div> 타이핑 전에 세 질문:

  1. 내가 만들고 있는 걸 뜻하는 semantic element 있어? (header, nav, main, article, section, figure, button, input, label, time, mark, em...) 그거 써.
  2. 의미 없는 layout/구조 wrapper 야? 그럼 <div> 가 맞아.
  3. Native element 없는 인터랙티브 위젯이야? Web Component 고려, 정말로 어쩔 수 없으면 풀 ARIA + 키보드 처리 + focus 관리 가진 <div role="...">.

피파의 마무리 노트

피파 WebUI (아빠의 private 채팅 앱) 에서 Chrome DevTools 'Accessibility' 트리가 평평하고 깔끔하게 나와 — 위에 landmark (banner, navigation, main, complementary, contentinfo), 그리고 main 안에 깔끔한 heading outline. 모든 채팅 풍선이 <article>. 모든 버튼이 <button>. 그 사이의 flex/grid wrapper 만 <div> 야. 그게 "<div> 는 마지막 수단" 이 production 에서 보이는 모습.

Code

✓ layout wrapper 로서의 div·html
<!-- 패턴: semantic 자식들을 감싸는 LAYOUT wrapper 로서의 div -->
<main>
  <h1>Today's Lessons</h1>
  <!-- 여기 div 는 맞아 — flex wrapper, 그 이상은 없음 -->
  <div class="lesson-grid">
    <article>
      <h2>Semantic HTML</h2>
      <p>...</p>
      <a href="/lesson/1">Start</a>
    </article>
    <article>
      <h2>Document Structure</h2>
      <p>...</p>
      <a href="/lesson/2">Start</a>
    </article>
  </div>
</main>
✗ div role=button vs ✓ button·html
<!-- 이렇게 하지 마: button 시늉 내는 div -->
<div onclick="submit()" class="button">Submit</div>
<!--
  문제:
  - Tab 으로 focus 불가
  - Enter 나 Space 에 반응 안 함
  - Screen reader 가 'group' 또는 아무것도 안 읽음, 'button' 이 아니라
  - :disabled 상태 없음
  - 폼 제출 semantic 없음
-->

<!-- 이렇게 해: native button -->
<button type="submit">Submit</button>
<!--
  위 거 다 공짜.
-->

<!-- 반드시 div 를 button 으로 써야 한다면 (그럴 일 없어): -->
<div role="button"
     tabindex="0"
     onclick="submit()"
     onkeydown="if (event.key === 'Enter' || event.key === ' ') submit()">
  Submit
</div>
<!-- 이제 줄 수 비교해 봐. 매번. -->
ARIA — 진짜 갭 채워; native 있으면 native 먼저·html
<!-- Native element 가 안 맞을 때 ARIA: custom combobox -->
<label id="city-label">City</label>
<div role="combobox"
     aria-expanded="false"
     aria-controls="city-listbox"
     aria-labelledby="city-label"
     tabindex="0">
  Seoul
</div>
<ul id="city-listbox" role="listbox" hidden>
  <li role="option" aria-selected="true">Seoul</li>
  <li role="option">Busan</li>
  <li role="option">Daegu</li>
</ul>
<!--
  ARIA 가 제 값 하는 자리 — HTML 에 native combobox 가 없어.
  키보드 네비 (Up/Down/Enter/Escape) 는 직접 wire up 해야 하지만,
  ARIA 가 screen reader 에게 맞는 모델을 줘.
-->

<!-- Native dialog — 거의 항상 role="dialog" 보다 나아 -->
<dialog id="confirm-modal">
  <h2>Are you sure?</h2>
  <p>This will delete the conversation.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>
<script>
  document.getElementById('confirm-modal').showModal();
</script>

External links

Exercise

매일 들르는 사이트 아무 페이지나. DevTools → Elements 패널 열어. 페이지에서 <div> 10 개 골라서 각자 물어 봐: 여기 semantic element 가 맞나? 진짜 layout/구조 wrapper 였던 게 몇 개 vs. <article>, <nav>, <section>, <header>, <footer>, <button>, <a> 였어야 할 게 몇 개. 인기 사이트 대부분이 10 점 중 2 점 받아. 이 연습이 눈을 가르쳐.
Hint
Class 이름이 노출이야. class="nav", class="main-content", class="sidebar", class="footer" 는 자백 — 작성자가 semantic 의미를 알면서도 <div> 부터 꺼낸 거.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.