왜 아직도 모달 라이브러리를 쓰고 있나요?

<dialog> 요소가 나온 지도 벌써 10년이 다 되어갑니다. 그런데도 실무에서는 여전히 react-modal, sweetalert2 같은 라이브러리를 습관처럼 붙이는 경우가 많아요. 이유는 단순합니다. 네이티브 dialog의 동작 방식이 미묘하게 헷갈리기 때문이죠. show()와 showModal()의 차이, ::backdrop 스타일링, 닫힘 상태의 display: none, 애니메이션 트리거 등 문서가 흩어져 있어서 매번 검색하게 됩니다.

이 글은 그 삽질을 한 번에 정리해드리는 실무 가이드예요. 기본 마크업부터 접근성 있는 닫기 버튼, 스크롤 락, 등장/퇴장 애니메이션, 그리고 Popover API와의 선택 기준까지 코드와 함께 짚어봅니다. 근거자료는 CSS-Tricks의 dialog 스타일링 정리글을 참고했어요.

참고로 다이얼로그를 열었을 때 배경 페이지는 inert 상태가 됩니다. 이건 모달일 때만 그렇고, show()로 열면 팝오버에 가까운 동작이 된다는 점, 뒤에서 다시 설명할게요.

Developer inspecting native HTML dialog element in browser DevTools with modal backdrop visible Programming Illustration

기본 마크업과 열기/닫기

가장 단순한 형태는 이렇습니다.

<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로 초기 포커스를 옮기는 걸 고려하세요.

Web designer styling dialog and ::backdrop pseudo-element with CSS transitions in code editor IT Technology Image

스타일링: ::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를 쓰면 접근성 부채가 쌓입니다.

Frontend developer comparing Dialog API and Popover API accessibility on laptop screen Coding Session Visual

정리: 오늘부터 적용할 3가지

  1. 모달은 무조건 showModal(). show()는 팝오버 유사 동작이라 백드롭/포커스 트랩/배경 inert가 없습니다.
  2. 스타일은 [open] 또는 :modal 상태에서. 닫힌 상태의 display: none 때문에 본체 선택자만으로는 안 먹습니다.
  3. 애니메이션은 @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의 관련 아티클을 참고하세요.

함께 보면 좋은 글

한국 개발 생태계에서의 적용 맥락

국내 SI/스타트업 환경에서는 여전히 react-modal, sweetalert2 의존이 강한데, <dialog> 네이티브로 옮기면 번들 사이즈와 접근성 부채를 동시에 줄일 수 있습니다. 다만 사내 디자인 시스템이 커스텀 애니메이션에 의존하고 있다면, @starting-style 지원 범위(최신 브라우저)를 먼저 확인하고, 레거시 대상이면 JS 기반 애니메이션 폴백을 병행하는 전략이 안전합니다.

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.