모달 하나 붙이려고 라이브러리를 깔던 시절이 있었다. 그런데 모달이라는 게 생각보다 만만치 않다. 뒤 배경 클릭하면 닫히고, ESC 눌러도 닫히고, 열렸을 때 포커스가 모달 안에 갇혀야 하고(포커스 트랩), 스크린 리더가 "여기 다이얼로그 떴다"고 알려줘야 한다. 이걸 직접 다 짜다 보면 어느새 100줄이 넘는다.
<dialog> 요소를 쓰면 이 대부분이 브라우저에 내장돼 있다. 지금은 모든 주요 브라우저가 지원해서, 굳이 라이브러리를 쓸 이유가 없어졌다.
기본 사용법
핵심은 showModal() 메서드다. open 속성을 직접 붙이거나 show()를 쓰는 방법도 있는데, 모달로 쓸 거라면 반드시 showModal()을 써야 한다. 이걸 써야 백드롭, 포커스 트랩, ESC 닫기가 전부 활성화된다.
import { useRef } from 'react';
function Example() {
const dialogRef = useRef(null);
return (
<>
<button onClick={() => dialogRef.current.showModal()}>
모달 열기
</button>
<dialog ref={dialogRef}>
<h2>정말 삭제할까요?</h2>
<p>이 작업은 되돌릴 수 없습니다.</p>
<form method="dialog">
<button value="cancel">취소</button>
<button value="confirm">삭제</button>
</form>
</dialog>
</>
);
}
form method="dialog" 안의 버튼을 누르면 별도 핸들러 없이 다이얼로그가 닫힌다. 그리고 dialog.returnValue에 눌린 버튼의 value가 담긴다. close 이벤트에서 이 값을 읽으면 "확인을 눌렀는지 취소를 눌렀는지"를 알 수 있다.
dialogRef.current.addEventListener('close', (e) => {
console.log(e.target.returnValue); // "confirm" 또는 "cancel"
});
백드롭 클릭으로 닫기
이건 기본 제공이 아니라서 살짝 처리가 필요하다. <dialog>는 자기 자신이 클릭 대상이 될 때(= 백드롭 영역)를 감지할 수 있다.
<dialog
ref={dialogRef}
onClick={(e) => {
if (e.target === dialogRef.current) dialogRef.current.close();
}}
>
내부 콘텐츠를 클릭하면 e.target이 자식 요소라 조건에 안 걸리고, 바깥 백드롭을 누르면 e.target이 dialog 본체라서 닫힌다.
스타일링
백드롭은 ::backdrop 가상 요소로 꾸민다.
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
}
showModal()로 열면 dialog가 최상위 레이어(top layer)에 올라가서 z-index 싸움을 할 필요가 없다. 이게 은근히 크다. 기존에 모달 z-index 때문에 골치 아팠던 경험이 있다면 특히.
정리하면, 모달의 어려운 부분(포커스 관리, ESC, 레이어링, 접근성 role)은 <dialog>가 알아서 해준다. 우리가 신경 쓸 건 백드롭 클릭 닫기 정도. 다음에 모달 필요하면 라이브러리 깔기 전에 이걸 먼저 떠올려도 좋겠다.