자바스크립트 없이 HTML만으로 되는 것들: 아직도 div로 다 만들고 있다면

아코디언, 팝오버, 모달, 자동완성, 폼 검증, 이미지 지연 로딩까지. 라이브러리 없이 HTML 태그와 속성만으로 해결되는 기능을 예제와 함께 정리했다. 코드는 줄고 접근성은 덤으로 따라온다.

아코디언 하나 만드는 데 몇 줄이 필요할까?

"클릭하면 펼쳐지는 FAQ"를 만든다고 해보자. 흔히 이렇게 시작한다.

  • div로 제목과 내용을 만들고
  • 클릭 이벤트(Click Event)를 달고
  • isOpen 상태를 관리하고
  • 키보드로도 열리게 tabindex와 keydown을 붙이고
  • 스크린 리더(Screen Reader)를 위해 aria-expanded를 맞춘다

그런데 HTML에는 이걸 태그 두 개로 끝내는 방법이 이미 있다. 바로 아래가 실제로 동작하는 예시다. 눌러보자.

HTML만으로 만든 아코디언인가요?

네. 자바스크립트 한 줄 없이 details와 summary 태그만 썼어요. 키보드(Tab → Enter)로도 열리고, 스크린 리더도 "펼쳐짐/접힘" 상태를 읽어줍니다.

브라우저는 생각보다 많은 것을 기본으로 제공한다. 직접 만들기 전에 "HTML에 이미 있나?"부터 확인하면 코드도 줄고, 접근성(Accessibility)과 성능은 공짜로 따라온다.

이 글에서는 그렇게 HTML만으로 해결되는 기능들을 모아봤다.


1. 아코디언: <details> + <summary>

<details>
  <summary>배송은 얼마나 걸리나요?</summary>
  <p>평일 기준 1~3일 소요됩니다.</p>
</details>
  • 처음부터 펼쳐두고 싶으면 open 속성을 붙인다.
  • 같은 name 속성을 준 details끼리는 하나만 열리는 배타적 아코디언(Exclusive Accordion)이 된다. 하나를 열면 나머지가 자동으로 닫힌다.
<details name="faq"><summary>질문 1</summary>답변 1</details>
<details name="faq"><summary>질문 2</summary>답변 2</details>
<details name="faq"><summary>질문 3</summary>답변 3</details>

name 속성은 2024년부터 주요 브라우저(Chrome, Safari, Firefox)에서 모두 지원된다. 지원하지 않는 브라우저에서도 그냥 "여러 개 열리는 아코디언"으로 동작하니 깨질 걱정은 없다.


2. 팝오버: popover 속성

드롭다운 메뉴, 툴팁, 알림 패널처럼 "버튼 누르면 위에 뜨는 것"은 popover 속성 하나로 된다.

<button popovertarget="menu">메뉴 열기</button>

<div id="menu" popover>
  <a href="/profile">내 정보</a>
  <a href="/logout">로그아웃</a>
</div>

이것만으로 브라우저가 알아서 처리해주는 것들이 꽤 많다.

직접 만들 때 해야 했던 일popover가 기본으로 해주는 것
열기/닫기 상태 관리popovertarget 버튼이 토글
바깥 클릭하면 닫기기본 동작 (Light Dismiss)
Esc 키로 닫기기본 동작
z-index 싸움최상위 레이어(Top Layer)에 그려져서 무조건 맨 위
다른 팝오버 열면 기존 것 닫기기본 동작

z-index: 99999를 붙여본 적이 있다면 마지막에서 두 번째 줄이 특히 반가울 것이다.


3. 모달: <dialog>

확인창, 로그인 모달처럼 뒤 화면을 막는 창은 dialog를 쓴다. 여기는 여는 데만 자바스크립트 한 줄이 필요하다.

<button onclick="confirmDialog.showModal()">삭제</button>

<dialog id="confirmDialog">
  <p>정말 삭제할까요?</p>
  <form method="dialog">
    <button value="cancel">취소</button>
    <button value="ok">삭제</button>
  </form>
</dialog>
  • showModal()로 열면 뒤 배경을 클릭할 수 없게 막히고, 포커스가 모달 안에 갇힌다 (Focus Trap).
  • form method="dialog" 안의 버튼을 누르면 자동으로 닫히고, 누른 버튼의 value가 dialog.returnValue에 담긴다.
  • Esc로 닫기도 기본으로 된다.
  • 뒤 배경은 CSS의 ::backdrop으로 꾸민다.
dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
}

popover와 dialog 중 무엇을 쓸지 헷갈린다면: 뒤 화면을 막아야 하면 dialog(모달), 막지 않고 위에 살짝 띄우기만 하면 popover 라고 기억하면 된다.


4. 자동완성: <datalist>

검색창에 추천 목록을 띄우는 데도 라이브러리가 필요 없다.

<label for="framework">프레임워크</label>
<input id="framework" list="frameworks" />

<datalist id="frameworks">
  <option value="Nuxt"></option>
  <option value="Next.js"></option>
  <option value="SvelteKit"></option>
  <option value="Astro"></option>
</datalist>

입력하면 목록이 필터링되고, 목록에 없는 값도 자유롭게 입력할 수 있다. 디자인을 세밀하게 바꿀 수는 없어서 서비스의 메인 검색창보다는 관리자 페이지나 사내 도구에 특히 잘 맞는다.


5. 모바일 키보드까지 챙기는 <input> 속성

type="text" 하나로 모든 입력을 받고 있다면 모바일 사용자는 매번 키보드를 바꾸고 있는 셈이다.

속성효과쓰는 곳
type="email"@가 있는 키보드 + 형식 검사이메일
type="tel"전화번호 키패드전화번호
inputmode="numeric"숫자 키패드 (값은 문자열로 유지)인증번호, 카드번호
enterkeyhint="search"키보드의 엔터 키가 "검색"으로 바뀜검색창
autocomplete="one-time-code"문자로 온 인증번호를 키보드 위에 추천휴대폰 인증
type="date" / type="color"브라우저 기본 날짜/색상 선택기간단한 날짜, 색상 입력
<!-- 문자로 받은 6자리 인증번호 입력칸 -->
<input
  inputmode="numeric"
  autocomplete="one-time-code"
  maxlength="6"
  enterkeyhint="done"
/>

카드번호나 인증번호에 type="number"를 쓰면 안 된다. 앞자리 0이 사라지거나, 마우스 휠에 값이 바뀌는 문제가 생긴다. "숫자 키패드만 필요한 문자열"이면 inputmode="numeric"이 정답이다.


6. 폼 검증: 속성 + CSS 한 줄

입력값 검사도 상당 부분 HTML 속성으로 된다.

<form>
  <input type="email" required placeholder="이메일" />
  <input
    type="password"
    required
    minlength="8"
    pattern="(?=.*\d)(?=.*[a-zA-Z]).*"
    title="영문과 숫자를 모두 포함해주세요"
  />
  <button>가입하기</button>
</form>
  • required, minlength, maxlength, min, max, pattern만으로 기본 검사가 되고, 제출 시 브라우저가 알아서 막고 안내해준다.
  • 에러 스타일은 CSS의 :user-invalid를 쓰면 좋다. :invalid와 달리 사용자가 입력을 건드린 뒤에만 빨갛게 표시돼서, 페이지에 들어오자마자 온통 빨간 화면이 되는 문제가 없다.
input:user-invalid {
  border-color: crimson;
}

HTML 검증은 사용자 편의를 위한 것이지 보안이 아니다. 개발자 도구로 속성을 지우면 그만이니, 서버에서도 반드시 다시 검사해야 한다.


7. 이미지: 속성 몇 개로 성능 챙기기

<img
  src="/images/hero-800.jpg"
  srcset="/images/hero-400.jpg 400w, /images/hero-800.jpg 800w, /images/hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="800"
  height="450"
  loading="lazy"
  decoding="async"
  alt="노트북 앞에서 코드를 작성하는 개발자"
/>
속성하는 일
loading="lazy"화면 근처에 올 때까지 이미지 다운로드를 미룸 (지연 로딩, Lazy Loading)
width + height이미지가 뜨기 전에 자리를 미리 잡아서 화면이 덜컹거리지 않게 함 (CLS 감소)
srcset + sizes화면 크기에 맞는 해상도만 골라서 다운로드
alt스크린 리더와 검색 엔진이 읽는 대체 텍스트

첫 화면에 바로 보이는 큰 이미지(히어로 이미지)에는 loading="lazy"를 붙이지 않는다. 오히려 늦게 떠서 LCP(가장 큰 콘텐츠가 그려지는 시간) 점수가 나빠진다. 이런 이미지에는 fetchpriority="high"가 어울린다.


8. 의미를 담는 작은 태그들

모양만 보면 span에 CSS를 입힌 것과 다를 게 없지만, 의미를 담고 있어서 검색 엔진과 보조기기가 더 잘 이해한다.

태그의미예시
<kbd>키보드 입력Ctrl + C
<mark>강조 표시(형광펜)검색 결과에서 일치한 단어
<abbr>약어 (마우스를 올리면 풀네임)CLS
<time>기계가 읽을 수 있는 날짜<time datetime="2026-10-08">10월 8일</time>
<progress> / <meter>진행률 / 측정값

위 표의 키보드 키, 형광펜, CLS, 막대 두 개는 모두 실제 태그로 렌더링한 것이다. CLS에 마우스를 올려보자. meter는 low/high 기준을 넘으면 브라우저가 알아서 색을 바꿔준다 (위 예시는 90%라서 경고색).


9. div 대신 써야 할 시맨틱 태그 한눈에 보기

시맨틱 태그(Semantic Tag)는 "이 영역이 무슨 역할인지"를 태그 이름으로 알려준다. 스크린 리더 사용자는 이걸로 "본문으로 바로 이동" 같은 탐색을 하고, 검색 엔진은 핵심 콘텐츠를 더 정확히 골라낸다.

이렇게 쓰고 있다면이렇게 바꾸자
<div class="header"><header>
<div class="nav"><nav>
<div class="main"><main> (페이지에 하나만)
<div class="post"><article>
<div class="sidebar"><aside>
<div class="footer"><footer>
<div onclick="..."><button>

마지막 줄이 가장 중요하다. div에 클릭 이벤트를 달면 키보드로는 누를 수 없고, 스크린 리더는 그게 버튼인지 모른다. 클릭되는 건 button, 페이지를 이동하는 건 a. 이것만 지켜도 접근성 문제의 상당수가 사라진다.


정리: 왜 알아두면 좋은가

만들고 싶은 것HTML 해결책
FAQ 아코디언<details> + <summary> (+ name)
드롭다운 / 툴팁popover + popovertarget
확인 모달<dialog> + showModal()
검색어 추천<datalist>
모바일 숫자 키패드inputmode="numeric"
입력값 검사required, pattern + :user-invalid
이미지 성능loading="lazy", width/height, srcset

브라우저 기본 기능을 쓰면 세 가지를 동시에 얻는다.

  1. 코드가 준다. 상태 관리, 이벤트 처리, 바깥 클릭 감지 같은 코드를 지워도 된다.
  2. 접근성이 따라온다. 키보드 조작, 포커스 관리, 스크린 리더 안내를 브라우저가 이미 해준다.
  3. 번들이 가벼워진다. UI 라이브러리를 하나 덜 설치해도 된다.

물론 디자인을 아주 세밀하게 바꿔야 하거나 복잡한 동작이 필요하면 직접 만들거나 라이브러리를 쓰는 게 맞다. 다만 그 결정을 내리기 전에 "HTML에 이미 있나?"를 먼저 확인하는 습관만 들여도 프로젝트가 훨씬 가벼워진다.