๐งฉ 04. ๋ค์ดํฐ๋ธ HTML ์ฌํ ์์: JavaScript ์์ด ๋ชจ๋ฌ์ด ๋๋ค๊ณ ?
๐ ๊ฐ์
dialog, details/summary, popover API, template/slot โ ํ๋ ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ HTML ๊ธฐ๋ฅ์ผ๋ก JS ์์ด ๊ตฌํํ ์ ์๋ ๊ฒ๋ค์ ํํค์นฉ๋๋ค.
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 18๋ถ / ํต์ฌ ํํธ๋ง: 10๋ถ
๐ฏ ์ด ๋ฌธ์์ ์์น
HTML guide 01~04๋ฒ์ ๋จผ์ ์ฝ์ด๋ณด์ธ์. ๋ค์ดํฐ๋ธ dialog API์ React ๋ชจ๋ฌ ํจํด์ ์ฐ๊ฒฐํฉ๋๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[<dialog> โ ๋ค์ดํฐ๋ธ ๋ชจ๋ฌ] โ [<details>/<summary> โ ์์ฝ๋์ธ] โ [Popover API] โ [React์์ ํตํฉ]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
<dialog>๋ก ๋ชจ๋ฌ์ ๊ตฌํํ๊ณ.showModal()/.close()API๋ฅผ ์ฌ์ฉํ ์ ์๋ค. -
<details>+<summary>๋ก JS ์๋ ์์ฝ๋์ธ/FAQ๋ฅผ ๋ง๋ค ์ ์๋ค. - Popover API๋ก ํดํ/๋๋กญ๋ค์ด์ ๊ตฌํํ ์ ์๋ค.
- React์์
<dialog>๋ฅผ ํ์ฉํ๋ ํจํด์ ์ดํดํ๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ๐ฃ ์์ฒ ( ์ ์ ): "์ํธ ๋, ์ React Portal์ด๋ useRef, useEffect ์กฐํฉ์ผ๋ก ๋ชจ๋ฌ ๋ง๋ค์๋๋ฐ ์ฝ๋๊ฐ ๋ง์ด ๋ณต์กํด์ก์ด์. ์ ๊ทผ์ฑ๋ ์ง์ ๊ตฌํํ๋ ค๋ ํฌ์ปค์ค ํธ๋ฉ์ด๋ ESC ํค ์ฒ๋ฆฌ๊น์ง ํด์ผ ํ๋๋ผ๊ณ ์. ๋ ๊ฐ๋จํ ๋ฐฉ๋ฒ ์๋์?"
- ๐ฆ ์ํธ ( ๋ฆฌ๋ ): "๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ
<dialog>์์๊ฐ ํฌ์ปค์ค ํธ๋ฉ, ESC ํค, backdrop ๋ชจ๋ ๊ธฐ๋ณธ ์ ๊ณตํด์. 2024๋ ๊ธฐ์ค ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์ ๋ถ ์ง์ํ๊ณ ์. ์ง์ ๊ตฌํํ ๋ชจ๋ฌ๋ณด๋ค ํจ์ฌ ์ ์ ์ฝ๋๋ก, ์ ๊ทผ์ฑ๋ ๋ ์ข์์."
๐ช 1. <dialog> โ ๋ค์ดํฐ๋ธ ๋ชจ๋ฌ
<!-- HTML ๊ตฌ์กฐ -->
<dialog id="confirm-modal">
<h2>์ ๋ง ์ญ์ ํ์๊ฒ ์ด์?</h2>
<p>์ญ์ ํ ๊ฒ์๊ธ์ ๋ณต๊ตฌํ ์ ์์ต๋๋ค.</p>
<div>
<!-- form method="dialog": dialog ์๋ ๋ซํ + returnValue ์ค์ -->
<form method="dialog">
<button value="cancel">์ทจ์</button>
<button value="confirm">์ญ์ </button>
</form>
</div>
</dialog>
<button onclick="document.getElementById('confirm-modal').showModal()">
๊ฒ์๊ธ ์ญ์
</button>const modal = document.getElementById('confirm-modal');
// ๋ชจ๋ฌ ์ด๊ธฐ: .showModal() โ ์ ์ฒดํ๋ฉด ์ค๋ฒ๋ ์ด + ํฌ์ปค์ค ํธ๋ฉ ์๋ ์ ์ฉ
modal.showModal();
// ๋ชจ๋ฌ ๋ซ๊ธฐ ์ด๋ฒคํธ: form method="dialog" ์ ์ถ ์ ์๋ ๋ฐ์
modal.addEventListener('close', () => {
if (modal.returnValue === 'confirm') {
// ์ญ์ ๋ก์ง ์คํ
}
});<dialog> ๊ฐ ๊ธฐ๋ณธ ์ ๊ณตํ๋ ๊ฒ๋ค:
- ํฌ์ปค์ค ํธ๋ฉ: ๋ชจ๋ฌ์ด ์ด๋ฆฌ๋ฉด ํฌ์ปค์ค๊ฐ ๋ชจ๋ฌ ์์๋ง ๊ฐํ (Tab ํค ํฌํจ)
- ESC ํค: ์๋์ผ๋ก ๋ชจ๋ฌ ๋ซํ
- backdrop:
::backdropCSS ๊ฐ์ ์์๋ก ๋ฐฐ๊ฒฝ dimming ๊ฐ๋ฅ - ARIA:
role="dialog",aria-modal="true"์๋ ์ ์ฉ
/* dialog ๊ธฐ๋ณธ ์คํ์ผ๋ง */
dialog {
border: none;
border-radius: 12px;
padding: 2rem;
max-width: min(90vw, 480px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
/* ๋ฐฐ๊ฒฝ ์ค๋ฒ๋ ์ด */
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}โ๏ธ 2. React์์ <dialog> ํ์ฉ
import { useRef, useEffect } from "react";
interface ConfirmDialogProps {
isOpen: boolean;
onClose: (confirmed: boolean) => void;
title: string;
message: string;
}
function ConfirmDialog({ isOpen, onClose, title, message }: ConfirmDialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
// ์ด๋ฏธ ์ด๋ ค์์ง ์์ผ๋ฉด showModal
if (!dialog.open) dialog.showModal();
} else {
dialog.close();
}
}, [isOpen]);
// close ์ด๋ฒคํธ: ESC ํค๋ก ๋ซํ ๋๋ ์บ์น
const handleClose = () => {
onClose(dialogRef.current?.returnValue === "confirm");
};
return (
<dialog ref={dialogRef} onClose={handleClose}>
<h2>{title}</h2>
<p>{message}</p>
<form method="dialog">
<button value="cancel" type="submit">์ทจ์</button>
<button value="confirm" type="submit">ํ์ธ</button>
</form>
</dialog>
);
}๐ 3. <details> + <summary> โ JS ์๋ ์์ฝ๋์ธ
<!-- FAQ ์น์
: JS ์์ด ์์ ํ ์์ฝ๋์ธ ๊ตฌํ -->
<section>
<h2>์์ฃผ ๋ฌป๋ ์ง๋ฌธ</h2>
<details>
<summary>์์๋ค ์ปค๋ฎค๋ํฐ๋ ๋ฌด๋ฃ์ธ๊ฐ์?</summary>
<p>๋ค, ์์ ๋ฌด๋ฃ ์๋น์ค์
๋๋ค. ์คํฐ๋ ๋งค์นญ๋ถํฐ ์งํ๊น์ง ๋ชจ๋ ๋ฌด๋ฃ์์.</p>
</details>
<details>
<summary>์คํฐ๋ ์ต์ ์ธ์์ด ์๋์?</summary>
<p>์ต์ 2๋ช
๋ถํฐ ์ต๋ 8๋ช
๊น์ง ๊ตฌ์ฑํ ์ ์์ด์.</p>
</details>
<!-- open ์์ฑ: ์ด๊ธฐ ์ด๋ฆผ ์ํ -->
<details open>
<summary>์ด๋ค ๊ฐ๋ฐ ๋ถ์ผ๋ฅผ ๊ณต๋ถํ ์ ์๋์?</summary>
<p>ํ๋ก ํธ์๋, ๋ฐฑ์๋, ์๊ณ ๋ฆฌ์ฆ, ๋์์ธ ๋ฑ ๋ค์ํ ๋ถ์ผ์ ์คํฐ๋๊ฐ ์์ด์.</p>
</details>
</section>/* details/summary ์คํ์ผ๋ง */
details {
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 0.5rem;
}
summary {
padding: 1rem;
cursor: pointer;
font-weight: 600;
list-style: none; /* ๊ธฐ๋ณธ ์ผ๊ฐํ ์ ๊ฑฐ */
display: flex;
justify-content: space-between;
align-items: center;
}
summary::after {
content: "+";
font-size: 1.5rem;
transition: transform 0.2s;
}
details[open] summary::after {
transform: rotate(45deg); /* ์ด๋ฆฌ๋ฉด X ๋ชจ์์ผ๋ก */
}
details > *:not(summary) {
padding: 0 1rem 1rem;
}๐ฌ 4. Popover API โ ํดํ/๋๋กญ๋ค์ด ๋ค์ดํฐ๋ธ ๊ตฌํ
<!-- Popover API: ๋ฒํผ ํด๋ฆญ์ผ๋ก ํ์ค๋ฒ ํ ๊ธ -->
<button popovertarget="user-menu">๋ด ๊ณ์ </button>
<div id="user-menu" popover>
<ul>
<li><a href="/profile">ํ๋กํ ์์ </a></li>
<li><a href="/settings">์ค์ </a></li>
<li><button type="button">๋ก๊ทธ์์</button></li>
</ul>
</div>/* Popover ์คํ์ผ๋ง */
[popover] {
border: none;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 0.5rem;
min-width: 200px;
/* ํธ๋์ง์
: Discrete Animation (Chrome 117+) */
}
[popover]:popover-open {
display: block;
}Popover API๊ฐ ๊ธฐ๋ณธ ์ ๊ณตํ๋ ๊ฒ๋ค:
popovertarget๋ฒํผ ํด๋ฆญ์ผ๋ก ์๋ ํ ๊ธ- Light dismiss: ํ์ค๋ฒ ์ธ๋ถ ํด๋ฆญ ์ ์๋ ๋ซํ
- Top Layer: z-index ๊ณ์ธต ์๊ด์์ด ํ๋ฉด ์ต์๋จ์ ํ์
- ESC ํค ๋ซํ
๐ ํต์ฌ ํจํด ์ด์ ๋ฆฌ
| ํ์ํ UI | ๋ค์ดํฐ๋ธ HTML | ํน์ง |
|---|---|---|
| ๋ชจ๋ฌ/๋ค์ด์ผ๋ก๊ทธ | <dialog> + .showModal() | ํฌ์ปค์ค ํธ๋ฉ, ESC, backdrop ์๋ |
| ์์ฝ๋์ธ/FAQ | <details> + <summary> | JS ์์ด ์์ ๋์ |
| ๋๋กญ๋ค์ด ๋ฉ๋ด | Popover API | Light dismiss, Top Layer |
| ์งํ๋ฅ ํ์ | <progress value="70" max="100"> | ๋ค์ดํฐ๋ธ UI |
| ๋ฒ์ ์ ํ | <input type="range"> | ์ฌ๋ผ์ด๋ UI ์๋ |
| ์์ ์ ํ | <input type="color"> | ์ปฌ๋ฌ ํผ์ปค UI ์๋ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. <dialog> ๋ฅผ showModal() ๋ก ์ด์์ ๋ ์๋์ผ๋ก ์ ์ฉ๋๋ ๊ธฐ๋ฅ์ด ์๋ ๊ฒ์?
- A) ESC ํค๋ก ๋ชจ๋ฌ ๋ซ๊ธฐ
- B) ๋ชจ๋ฌ ์ธ๋ถ ํฌ์ปค์ค ์ฐจ๋จ (ํฌ์ปค์ค ํธ๋ฉ)
- ๊ฐ) ๋ฐฐ๊ฒฝ dimming ์คํ์ผ (
::backdrop) - ๋ผ) ๋ชจ๋ฌ ๋ด ์ ๋ ฅ๊ฐ ์๋ ์ ์ฅ
โ ์ ๋ต: ๋ผ
๐ก ์์ธ ํด์ค:
<dialog>.showModal()์ ํฌ์ปค์ค ํธ๋ฉ, ESC ํค ์ฒ๋ฆฌ,::backdrop๊ฐ์ ์์ ์๋ ์์ฑ์ ์ ๊ณตํด์. ๋ค๋ง::backdrop๋ CSS๋ก ์ง์ ์คํ์ผ๋งํด์ผ ์์์ด ๋ณด์ฌ์. ์ ๋ ฅ๊ฐ ์ ์ฅ์ ๋ณ๋ ๋ก์ง์ด ํ์ํด์.- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "
<dialog>.showModal()= ํฌ์ปค์ค ํธ๋ฉ + ESC + backdrop ์๋, ์คํ์ผ์ CSS๋ก"
Q2. Popover API์ "Light Dismiss" ๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ํ์ค๋ฒ ์์ญ ๋ฐ๊นฅ์ ํด๋ฆญํ๊ฑฐ๋ ESC ํค๋ฅผ ๋๋ฅด๋ฉด ํ์ค๋ฒ๊ฐ ์๋์ผ๋ก ๋ซํ๋ ๋์
๐ก ์์ธ ํด์ค:
- Light Dismiss๋ ๋๋กญ๋ค์ด ๋ฉ๋ด, ํดํ ๋ฑ์ ํต์ฌ UX ํจํด์ด์์. ์ง์ ๊ตฌํํ๋ฉด
document.addEventListener('click')์ด๋ฒคํธ ๋ฒ๋ธ๋ง ์ฒ๋ฆฌ๊ฐ ๋ณต์กํ์ง๋ง,popover์์ฑ์ ์ฐ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค์.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์
์์๋ค ์ปค๋ฎค๋ํฐ์ ๊ฐ ๊ธฐ์ ์คํ์ ๋ํ FAQ ์น์ ์ ์ถ๊ฐํด์ผ ํ๋ค.
๋์์ด๋๊ฐ "๊ฐ ์ง๋ฌธ ํด๋ฆญํ๋ฉด ๋ต๋ณ์ด ํผ์ณ์ง๋ ์์ฝ๋์ธ UI๋ก ํด์ฃผ์ธ์"๋ผ๊ณ ์์ฒญํ๋ค.
JavaScript ์์ด ๊ตฌํํ ์ ์๋๊ฐ?
โ
์ ๋ต: <details> + <summary> ์กฐํฉ์ผ๋ก JS ์์ด ์์ ํ ๊ตฌํ ๊ฐ๋ฅ
๐ก ์์ธ ํด์ค:
<details>๋open์์ฑ์ ์กด์ฌ ์ฌ๋ถ๋ก ์ด๋ฆผ/๋ซํ ์ํ๋ฅผ ๊ด๋ฆฌํด์. ๋ธ๋ผ์ฐ์ ๊ฐ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์๋์ผ๋ก ํ ๊ธํด์ฃผ๋ฏ๋ก JS๊ฐ ํ์์์ด์. CSSdetails[open] summary::after์ ๋ ํฐ๋ก ์ด๋ฆผ ์ํ ์์ด์ฝ ๋ณ๊ฒฝ๋ ๊ฐ๋ฅํด์.- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์์ฝ๋์ธ/FAQ =
<details>+<summary>โ CSS๋ง์ผ๋ก ์์ฑ"
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
<dialog> ํ๋๋ก ๋ด๊ฐ useRef, useEffect, Portal, ํฌ์ปค์ค ํธ๋ฉ, ESC ํค ํธ๋ค๋ฌ๊น์ง ์ง์ ์งฐ๋ 150์ค์ง๋ฆฌ ๋ชจ๋ฌ์ด 30์ค๋ก ์ค์๋ค. ์ง์ง ๋ง์ด ์ ๋๋ค๊ณ ์๊ฐํ๋๋ฐ, ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฏธ ์ด ๋ชจ๋ ๊ฑธ ํด์ฃผ๊ณ ์์๋ ๊ฑฐ๋ค.
๐ก "'์ง์ ๋ง๋ค์ด์ผ ํ๋ค'๋ ์๊ฐ์ด ์คํ๋ ค ํ์ง์ ๋ฎ์ถ ์ ์๋ค. ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ์ ๋จผ์ ํ์ ํ๋ ๊ฒ 5๋ ์ฐจ์ ์์ธ๋ค. dialog, details, popover โ ์ด๊ฒ๋ค์ด ์ด๋ฏธ ์์ฑ๋ ํด๋ต์ด๋ค."
<details> + <summary> ๋ก FAQ ๋ง๋ค์๋๋ ์์ ๋์์ด๋ ๋์ด "์ด๊ฒ CSS ์ ๋๋ฉ์ด์
๊น์ง ๋๋ค์?" ํ๊ณ ์ ๊ธฐํดํ์
จ๋ค. ๋ธ๋ผ์ฐ์ API๋ฅผ ๋ ๊ณต๋ถํด์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. HTML ์์ฒด๊ฐ ์ด๋ ๊ฒ ๊ฐ๋ ฅํ๋ฐ ์ ์ฌํ๊น์ง div๋ก ๋ง๋ค๊ณ ์์์ง.
๐ ๋ ์์๋ณด๊ธฐ