자바스크립트 없이 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 |
브라우저 기본 기능을 쓰면 세 가지를 동시에 얻는다.
- 코드가 준다. 상태 관리, 이벤트 처리, 바깥 클릭 감지 같은 코드를 지워도 된다.
- 접근성이 따라온다. 키보드 조작, 포커스 관리, 스크린 리더 안내를 브라우저가 이미 해준다.
- 번들이 가벼워진다. UI 라이브러리를 하나 덜 설치해도 된다.
물론 디자인을 아주 세밀하게 바꿔야 하거나 복잡한 동작이 필요하면 직접 만들거나 라이브러리를 쓰는 게 맞다. 다만 그 결정을 내리기 전에 "HTML에 이미 있나?"를 먼저 확인하는 습관만 들여도 프로젝트가 훨씬 가벼워진다.