Engineering note ยท HTML / Javascript

HTML dialog ํƒœ๊ทธ์™€ popover API

2026๋…„ 07์›” 26์ผ์ดํ˜„์ˆ˜
ํƒœ๊ทธhtmldialogpopoverjavascriptcss

๋ธŒ๋ผ์šฐ์ € Top Layer์— ๋„์šฐ๋Š” <dialog>์™€ Popover API์˜ ์ฐจ์ด์ , modal/non-modal ๊ตฌ๋ถ„ ๋ฐ CSS Anchor Positioning์„ ํ™œ์šฉํ•œ ํŒ์—… UI ๊ตฌํ˜„ ๊ฐ€์ด๋“œ.

modal ๊ณผ popover ๋ฅผ API ๋กœ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์ตœ๊ทผ์— ์•Œ์•˜๋‹ค. ๊ทธ ์ „๊นŒ์ง€๋Š” ์ฝ”๋“œ๋กœ ๊ตฌํ˜„์„ ํ–ˆ์—ˆ๋Š”๋ฐ..

Dialog์™€ Popover ๊ตฌ์กฐ ๋น„๊ต

Dialog

html
<button id="open-btn">์„ค์ • ์—ด๊ธฐ</button>

<dialog id="settings-dialog">
  <form method="dialog">
    <h3>์„ค์ •</h3>
    <p>๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</p>
    <button value="cancel">์ทจ์†Œ</button>
    <button value="confirm">ํ™•์ธ</button>
  </form>
</dialog>

<script>
  const dialog = document.querySelector('#settings-dialog');
  document.querySelector('#open-btn').addEventListener('click', () => {
    dialog.showModal(); // Top Layer ๋ชจ๋‹ฌ ๋„์šฐ๊ธฐ
  });
</script>

<dialog> ๋Š” showModal()๊ณผ show() ๋ฅผ ํ†ตํ•ด ๋ชจ๋‹ฌ๊ณผ ๋น„๋ชจ๋‹ฌ ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•œ๋‹ค. ๋ชจ๋‹ฌ๋กœ ๋„์šฐ๋ฉด ์ƒํ˜ธ์ž‘์šฉ์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ์™ธ๋ถ€ ์ž…๋ ฅ์ด ์ฐจ๋‹จ๋œ๋‹ค.

๊ธฐ์ค€showModal()show()
๋ชจ๋‹ฌ ์—ฌ๋ถ€๋ชจ๋‹ฌ (์™ธ๋ถ€ ํด๋ฆญ/์ž…๋ ฅ ์ฐจ๋‹จ)๋น„๋ชจ๋‹ฌ (์™ธ๋ถ€ ํด๋ฆญ/์ž…๋ ฅ ํ—ˆ์šฉ)
๋ ˆ์ด์–ด ์œ„์น˜๋ธŒ๋ผ์šฐ์ € ์ตœ์ƒ์œ„ Top Layer์ผ๋ฐ˜ DOM ๋ ˆ์ด์–ด
๋ฐฐ๊ฒฝ ์•”์ „::backdrop ์˜์‚ฌ ์š”์†Œ ์ œ๊ณต๋ฐฑ๋“œ๋กญ ์—†์Œ
ํฌ์ปค์Šค ์ด๋™๋‚ด๋ถ€๋กœ ํฌ์ปค์Šค ์ž๋™ ์ด๋™ ๋ฐ ๊ฐ€๋‘ ํฌ์ปค์Šค ์ด๋™ ์—†์Œ

<form method="dialog">๋ฅผ ๋‚ด๋ถ€์— ์‚ฌ์šฉํ•˜๋ฉด, ํผ ๋‚ด๋ถ€์˜ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ ์ฝ”๋“œ ์ž‘์„ฑ ์—†์ด๋„ ๋‹ค์ด์–ผ๋กœ๊ทธ๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ซํžŒ๋‹ค. ๋ฒ„ํŠผ์˜ value๊ฐ€ dialog.returnValue๋กœ ์ „๋‹ฌ๋˜๋Š” ๊ตฌ์กฐ.

Dialog ๋ฐฐ๊ฒฝ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ

๋ชจ๋‹ฌ ๋‹ค์ด์–ผ๋กœ๊ทธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Esc ํ‚ค๋กœ ๋‹ซํžˆ์ง€๋งŒ, ๋ฐฐ๊ฒฝ ์ชฝ ::backdrop ์„ ๋ˆŒ๋ €์„ ๋•Œ ์ž๋™์œผ๋กœ ๋‹ซํžˆ์ง€๋Š” ์•Š๋Š”๋‹ค. ๋ฐฐ๊ฒฝ ํด๋ฆญ ๋‹ซ๊ธฐ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋‹ค์ด์–ผ๋กœ๊ทธ์— ํด๋ฆญ ์ด๋ฒคํŠธ์—์„œ ๋ฐฐ๊ฒฝ ํด๋ฆญํ•œ ๊ฑธ ์˜์—ญ์„ ๊ณ„์‚ฐํ•ด ํŒ๋ณ„ํ•ด์•ผ ํ•œ๋‹ค.

js
dialog.addEventListener('click', (event) => {
  const rect = dialog.getBoundingClientRect();
  const isClickOutside = (
    event.clientX < rect.left ||
    event.clientX > rect.right ||
    event.clientY < rect.top ||
    event.clientY > rect.bottom
  );
  
  if (isClickOutside) {
    dialog.close();
  }
});

Popover

html
<!-- ๋ฒ„ํŠผ์— popovertarget ์ง€์ • -->
<button popovertarget="profile-menu">ํ”„๋กœํ•„ ๋ฉ”๋‰ด</button>

<!-- ํŒ์˜ค๋ฒ„ ๋Œ€์ƒ ์š”์†Œ -->
<div id="profile-menu" popover>
  <button type="button">ํ”„๋กœํ•„ ์ˆ˜์ •</button>
  <button type="button">๋กœ๊ทธ์•„์›ƒ</button>
</div>

popover ์†์„ฑ์€ ํƒœ๊ทธ ์š”์†Œ๊ฐ€ ์•„๋‹ˆ๋ผ ์–ด๋–ค ์š”์†Œ์—๋‚˜ ๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” ์ „์—ญ ์†์„ฑ์ด๋‹ค. ๋ฒ„ํŠผ์˜ popovertarget๊ณผ ์กฐํ•ฉํ•˜๋ฉด ์ฝ”๋“œ ํ•œ ์ค„ ์—†์ด๋„ ๋“œ๋กญ๋‹ค์šด์ด๋‚˜ ํˆดํŒ์„ ๋„์šธ ์ˆ˜ ์žˆ๋‹ค.

  1. ๊ธฐ๋ณธ ๋™์ž‘์€ popover="auto" ๋กœ ์™ธ๋ถ€ ๋ฐ”๊นฅ ์˜์—ญ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ Esc ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์•Œ์•„์„œ ํŒ์˜ค๋ฒ„๋ฅผ ๋‹ซ์•„์ค€๋‹ค.
  2. ์—ฌ๋Ÿฌ ํŒ์˜ค๋ฒ„๋ฅผ ๋™์‹œ์— ๋„์šฐ๊ธฐ. ๋ฐ”๊นฅ ํด๋ฆญ์œผ๋กœ ๋‹ซํžˆ์ง€ ์•Š์•„์•ผ ํ•  ๋•Œ. popover="manual" ๋กœ ์„ค์ •
  3. ๋ถ€๋ชจ์˜ overflow: hidden์ด๋‚˜ z-index Stacking Context ์— ๊ฐ‡ํžˆ์ง€ ์•Š๊ณ  ํ•ญ์ƒ ์ตœ์ƒ์œ„ ๋ ˆ์ด์–ด์— ๋œจ๊ฒŒ ๋จ

Anchor Positioning

ํŒ์˜ค๋ฒ„๋ฅผ ์—ด ๋•Œ ์ค‘์š”ํ•œ ๊ฒƒ ํŠธ๋ฆฌ๊ฑฐ ๋ฐ”๋กœ ์•„๋ž˜์— ํŒ์˜ค๋ฒ„๋ฅผ ์œ„์น˜์‹œํ‚ค๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. Anchor Positioning์„ ์‚ฌ์šฉํ•˜๋ฉด ํŒ์˜ค๋ฒ„ ์œ„์น˜๋ฅผ ์ˆœ์ˆ˜ CSS๋กœ ๊ธฐ์ค€ ๋ฒ„ํŠผ์— ๊ณ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

css
/* ๊ธฐ์ค€ ๋ฒ„ํŠผ์— ์•ต์ปค ์ด๋ฆ„ ๋ถ€์—ฌ */
#menu-button {
  anchor-name: --menu-btn;
}

/* ํŒ์˜ค๋ฒ„ ์š”์†Œ๋ฅผ ๊ธฐ์ค€ ๋ฒ„ํŠผ ์•„๋ž˜์— ๊ณ ์ • */
#profile-menu {
  position: fixed;
  position-anchor: --menu-btn;
  top: anchor(bottom);
  left: anchor(left);
  margin-top: 4px;
}
html
<button id="menu-button" popovertarget="profile-menu">๋ฉ”๋‰ด</button>
<div id="profile-menu" popover>ํŒ์˜ค๋ฒ„ ๋‚ด์šฉ</div>

์„ธ์ƒ ์ข‹์•„์กŒ๋‹ค..