왜 아직도 모달 라이브러리를 쓰고 있나요?
<dialog> 요소가 나온 지도 벌써 10년이 다 되어갑니다. 그런데도 실무에서는 여전히 react-modal, sweetalert2 같은 라이브러리를 습관처럼 붙이는 경우가 많아요. 이유는 단순합니다. 네이티브 dialog의 동작 방식이 미묘하게 헷갈리기 때문이죠. show()와 showModal()의 차이, ::backdrop 스타일링, 닫힘 상태의 display: none, 애니메이션 트리거 등 문서가 흩어져 있어서 매번 검색하게 됩니다.
이 글은 그 삽질을 한 번에 정리해드리는 실무 가이드예요. 기본 마크업부터 접근성 있는 닫기 버튼, 스크롤 락, 등장/퇴장 애니메이션, 그리고 Popover API와의 선택 기준까지 코드와 함께 짚어봅니다. 근거자료는 CSS-Tricks의 dialog 스타일링 정리글을 참고했어요.
참고로 다이얼로그를 열었을 때 배경 페이지는
inert상태가 됩니다. 이건 모달일 때만 그렇고,show()로 열면 팝오버에 가까운 동작이 된다는 점, 뒤에서 다시 설명할게요.

기본 마크업과 열기/닫기
가장 단순한 형태는 이렇습니다.
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">
<p>안녕하세요, 다이얼로그입니다.</p>
<button id="dialog-close">닫기</button>
</dialog>
open 속성을 직접 넣으면 초기부터 열린 상태가 되지만, 실무에서는 거의 쓸 일이 없어요. 보통은 JS로 엽니다.
const dialogButton = document.querySelector('#dialog-button');
const dialog = document.querySelector('#dialog');
// show()는 팝오버처럼 동작 (백드롭/중앙정렬/Esc 닫기 없음)
dialogButton.addEventListener('click', () => {
dialog.show();
});
// showModal()이 진짜 모달 (백드롭 + 중앙정렬 + Esc 닫기)
dialogButton.addEventListener('click', () => {
dialog.showModal();
});
실무에서는 거의 항상 showModal()을 쓰세요. show()는 백드롭도 없고, 중앙 정렬도 안 되고, Esc로 닫히지도 않습니다. 툴팁 같은 가벼운 팝업이 아니라면 showModal()이 정답이에요.
닫기 버튼
close() 메서드로 닫습니다. closeModal()은 없어요. 이상하지만 그냥 close()가 모달도 닫습니다.
const dialogClose = document.querySelector('#dialog-close');
dialogClose.addEventListener('click', () => {
dialog.close();
});
JS 없이도 가능합니다. form method="dialog" 트릭이에요.
<form method="dialog">
<button>닫기</button>
</form>
접근성 있는 X 버튼
X 아이콘만 넣으면 스크린리더가 "엑스"라고 읽어버립니다. 반드시 텍스트를 시각적으로 숨기고 아이콘은 aria-hidden 처리하세요.
<button id="dialog-close">
<span class="sr-only">다이얼로그 닫기</span>
<svg aria-hidden="true" width="16" height="16">...</svg>
</button>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
초기 포커스 제어
모달이 열리면 첫 포커스가 닫기 버튼에 갑니다. 그 상태에서 Space를 누르면 의도치 않게 닫힐 수 있죠. 폼 필드나 링크가 있다면 tabindex나 autofocus로 초기 포커스를 옮기는 걸 고려하세요.
![]()
스타일링: ::backdrop, 위치, 스크롤 락
백드롭
기본 백드롭은 너무 옅어서 거의 안 보입니다. ::backdrop 의사 요소로 직접 스타일링하세요.
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
다이얼로그 본체
주의할 점: dialog { ... }에 직접 스타일을 넣으면 안 먹는 것처럼 보입니다. 닫힌 상태의 기본값이 display: none이기 때문이에요. [open] 또는 :open 상태에서 스타일링하세요.
dialog {
border: 0;
border-radius: 12px;
padding: 24px;
width: min(90vw, 480px);
&[open] {
background-color: #fff;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}
}
:modal 의사 클래스가 :open보다 명시도가 높아서, 오버라이드가 필요할 때 유용합니다. 다만 :open은 비교적 최근 지원이라 레거시 브라우저를 고려한다면 [open] 속성 선택자를 쓰는 게 안전해요.
스크롤 락 (배경 스크롤 방지)
모달이 열렸을 때 배경이 스크롤되면 사용자 맥락이 흐트러집니다. 가장 널리 호환되는 방법은 :has()입니다.
body:has(dialog[open]) {
overflow: hidden;
}
최신 Chrome(144+)에서는 overscroll-behavior: contain을 다이얼로그 자체에 걸어서 더 선언적으로 처리할 수 있습니다.
dialog {
overflow: hidden;
overscroll-behavior: contain;
&::backdrop {
overscroll-behavior: contain;
}
}
등장/퇴장 애니메이션
transition만으로는 안 됩니다. display: none → block 전환 시점에 초기 스타일이 없어서 트랜지션이 트리거되지 않거든요. 여기서 @starting-style이 필요합니다.
@starting-style {
dialog:open {
opacity: 0;
transform: translateY(-8px);
}
}
dialog {
transition: opacity 0.3s ease, transform 0.3s ease;
&:open {
opacity: 1;
transform: translateY(0);
}
}
View Transitions API는 다이얼로그에 잘 안 맞습니다. 모달이 top layer에 있어서 close 시 old/new 스냅샷 쌍이 안정적으로 안 만들어지거든요. 등장만 View Transition, 퇴장은 CSS 애니메이션으로 섞는 하이브리드도 가능하지만, 그냥 양쪽 다 CSS transition/animation으로 통일하는 게 관리하기 편합니다.
Dialog vs Popover: 무엇을 써야 하나
Zell Liew의 정리를 실무 관점에서 요약하면 이렇습니다.
| 항목 | Dialog (showModal) | Popover API |
|---|---|---|
| 백드롭 | 자동 | 수동 |
| 포커스 트랩 | 자동 | 수동 (JS 필요) |
배경 inert | 자동 | 없음 |
| Esc 닫기 | 자동 | 자동 |
| ARIA 연결 | 자동 | 수동 역할 지정 필요 |
| 적합한 용도 | 진짜 모달 | 툴팁, 드롭다운, 메뉴 |
정리하면 모달이면 Dialog, 그 외 팝업이면 Popover입니다. Popover는 접근성 기능(포커스 트랩, inert, ARIA 연결)을 직접 구현해야 하니, 진짜 모달이 필요한 상황에서 Popover를 쓰면 접근성 부채가 쌓입니다.

정리: 오늘부터 적용할 3가지
- 모달은 무조건
showModal().show()는 팝오버 유사 동작이라 백드롭/포커스 트랩/배경 inert가 없습니다. - 스타일은
[open]또는:modal상태에서. 닫힌 상태의display: none때문에 본체 선택자만으로는 안 먹습니다. - 애니메이션은
@starting-style필수. 이거 없으면 등장 트랜지션이 그냥 스냅으로 끝납니다.
주의사항 (비판적 시각)
:open의사 클래스는 Safari 26.5에서 막 지원된 수준이라, 레거시 지원이 필요하면[open]을 쓰세요.overscroll-behavior: contain방식은 Chrome 144+ 의존이라, 실무에서는body:has(dialog[open])폴백을 같이 두는 게 안전합니다.- Invoker Commands (
command/commandfor속성)는 아직 실험 단계입니다. 지금 프로덕션에 넣으면 안 되고, 폴리필/점진적 향상 관점에서만 접근하세요. - X 아이콘에
aria-hidden안 붙이면 스크린리더가 "엑스"라고 읽습니다.sr-only텍스트와 세트로 쓰세요.
다음 단계 학습 방향
- View Transitions API로 페이지 전환까지 확장해보고 싶다면, 다이얼로그가 아니라 페이지 레벨 전환에서 먼저 감을 잡으세요.
- Popover API를 실무에 도입할 때는 MDN의 Popover 접근성 가이드를 반드시 같이 읽어보시길 권합니다.
- 다이얼로그를 활용한 커스텀 애니메이션 패턴(예:
shape()경로를 따라가는 모달)은 CSS-Tricks의 관련 아티클을 참고하세요.
함께 보면 좋은 글
- React, 드디어 독립했다 – 리눅스 재단 산하 React Foundation 공식 출범
- CSS 상대 색상(Relative Color)으로 애니메이션 테마링 끝내기 OKLCH 실전 가이드
한국 개발 생태계에서의 적용 맥락
국내 SI/스타트업 환경에서는 여전히 react-modal, sweetalert2 의존이 강한데, <dialog> 네이티브로 옮기면 번들 사이즈와 접근성 부채를 동시에 줄일 수 있습니다. 다만 사내 디자인 시스템이 커스텀 애니메이션에 의존하고 있다면, @starting-style 지원 범위(최신 브라우저)를 먼저 확인하고, 레거시 대상이면 JS 기반 애니메이션 폴백을 병행하는 전략이 안전합니다.