블로그 목록
프론트엔드

네이티브 <dialog>로 모달 만들기 — 라이브러리 없이 접근성까지

모달 하나 붙이려고 라이브러리를 깔던 시절이 있었다. 그런데 모달이라는 게 생각보다 만만치 않다. 뒤 배경 클릭하면 닫히고, 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>가 알아서 해준다. 우리가 신경 쓸 건 백드롭 클릭 닫기 정도. 다음에 모달 필요하면 라이브러리 깔기 전에 이걸 먼저 떠올려도 좋겠다.