| ์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
- ๋คํฌ๋ชจ๋ํ ๊ธ
 - ui์ปดํฌ๋ํธ
 - ์ผ์ดํ๋ฐ๋ชฌํํฐ์ค
 - ๋คํฌ๋ชจ๋
 - ๋ฐ๋๋ผ์คํฌ๋ฆฝํธ
 - ๋ฐ๋ง์ผํ
 - js์คํฌ๋กค์ด๋ฒคํธ
 - UIUX
 - ์ผ๋ฐํ
 - ์น๊ฐ๋ฐ
 - ํ๋ก ํธ์๋
 - ๊ณจ๋
 - ๋ทํ๋ฆญ์ค์ ๋๋ฉ์ด์ 
 - JavaScript
 - ์น๋์์ธ
 - ์นUI
 - ์๋ฐ์คํฌ๋ฆฝํธui
 - ์น์ ๊ทผ์ฑ
 - ์คํฌ๋กค์ ๋๋ฉ์ด์ 
 - ํํธ๋ฆญ์ค
 - ์ ๊ทผ์ฑ
 - ๋ฐ๋๋ผjs
 - Golden
 - ์ฌ์๋ณด์ด์ฆ
 - ์๋ฐ์คํฌ๋ฆฝํธ
 - kpopdemonhunters
 - ๋น๋ณด๋์ฐจํธ
 - UX๋์์ธ
 - ๋ทํ๋ฆญ์คost
 - ๊ฐ์์์ด๋
 
- Today
 
- Total
 
UI Code Lab
์ ๊ทผ์ฑ์ ๊ณ ๋ คํ ์ค์  ๋ชจ๋ฌ ๊ตฌํ๊ธฐ (Modal) ๋ณธ๋ฌธ
์ ๊ทผ์ฑ์ ๊ณ ๋ คํ ์ค์  ๋ชจ๋ฌ ๊ตฌํ๊ธฐ (Modal)
๐ฏ๊ฟ์์ด๐ 2025. 6. 26. 11:57
๋ชจ๋ฌ์ UI์์ ์์ฃผ ์ฐ์ด๋ ์ปดํฌ๋ํธ์ง๋ง,
์ ๊ทผ์ฑ๊น์ง ์ฑ๊ธด ๊ตฌํ์ ์๊ฐ๋ณด๋ค ๋๋ญ ๋๋ค.
์ด ๊ธ์์๋ ๊ธฐ๋ณธ์ ์ธ ๋ฐ๋๋ผ ์๋ฐ์คํฌ๋ฆฝํธ ๋ชจ๋ฌ ๊ตฌํ๋ฒ๋ถํฐ,
์คํฌ๋ฆฐ๋ฆฌ๋์ ํค๋ณด๋ ์ฌ์ฉ์๋ ๊ณ ๋ คํ ์ ๊ทผ์ฑ ๊ฐ์ ํฌ์ธํธ๊น์ง ๋ค๋ค๋ด ๋๋ค.
์ค๋ฌด์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ์์  ์ฝ๋๋ ํจ๊ป ์ ๊ณตํ๋,
๋ณต์กํ ํ๋ ์์ํฌ ์์ด ์์ JS๋ก๋ ์ถฉ๋ถํ ๋ฉ์ง ์ ๊ทผ์ฑ ๋ชจ๋ฌ์ ๋ง๋ค์ด๋ณด์ธ์!
โจ ๊ธฐ๋ณธ ๋ชจ๋ฌ ๊ตฌํ
๊ฐ๋จํ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ๋ฒํผ ํด๋ฆญ ์ ๋ชจ๋ฌ์ ๋์ฐ๊ณ ๋ซ๋ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ ๋๋ค.
โ HTML ๊ตฌ์กฐ
<button id="openModal">๋ชจ๋ฌ ์ด๊ธฐ</button>
<div id="modal" class="modal" hidden>
  <div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
    <h2 id="modalTitle">๋ชจ๋ฌ ์ ๋ชฉ</h2>
    <p>๋ชจ๋ฌ ๋ด์ฉ์
๋๋ค.</p>
    <button id="closeModal">๋ซ๊ธฐ</button>
  </div>
</div>
โ JS ๊ธฐ๋ณธ ๋ก์ง
const openBtn = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');
const modal = document.getElementById('modal');
openBtn.addEventListener('click', () => {
  modal.hidden = false;
});
closeBtn.addEventListener('click', () => {
  modal.hidden = true;
});
โ ๊ธฐ๋ณธ ์คํ์ผ (์ ํ)
.modal {
  position: fixed;
  inset: 0;
  background-color: rgba(0,0,0,0.4);
  display: flex;
  justify-content: center;
  align-items: center;
}
.modal-content {
  background: white;
  padding: 2em;
  border-radius: 8px;
}
โฟ ์ ๊ทผ์ฑ์ ๋ฐ์ํ ๋ชจ๋ฌ ๊ฐ์ ํฌ์ธํธ
์ ์ฝ๋์ ์ฝ๊ฐ์ ๊ธฐ๋ฅ๋ง ์ถ๊ฐํ๋ฉด ํจ์ฌ ์ ๊ทผ์ฑ ๋์ ๋ชจ๋ฌ์ด ๋ฉ๋๋ค.
์ฌ๊ธฐ์๋ ์ด์ ์๋น์ค์ ์ค์  ์ ์ฉํ๋ ์ ๊ทผ์ฑ ์ฌ๋ก๋ค์ ๊ธฐ๋ฐ์ผ๋ก ์ค๋ช ํฉ๋๋ค.
1. ํฌ์ปค์ค ํธ๋ฉ
๋ชจ๋ฌ ์์์ Tab ํค๋ก ํฌ์ปค์ค๊ฐ ์ํํ๋๋ก ์ค์ ํฉ๋๋ค.
๋ชจ๋ฌ์ด ์ด๋ฆฌ๋ฉด ํฌ์ปค์ค๊ฐ ์ฒซ ๋ฒ์งธ ํฌ์ปค์ค ๊ฐ๋ฅํ ์์๋ก ์ด๋ํ๊ณ ,
Shift + Tab์ด๋ Tab์ผ๋ก ํฌ์ปค์ค๊ฐ ๋ฐ๊นฅ์ผ๋ก ๋๊ฐ์ง ์๊ฒ ๋ง๋ ๋ฐฉ์์ ๋๋ค.
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modalContent = modal.querySelector('.modal-content');
function trapFocus(e) {
  const focusableElements = modalContent.querySelectorAll(focusableSelectors);
  const firstEl = focusableElements[0];
  const lastEl = focusableElements[focusableElements.length - 1];
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === firstEl) {
      e.preventDefault();
      lastEl.focus();
    } else if (!e.shiftKey && document.activeElement === lastEl) {
      e.preventDefault();
      firstEl.focus();
    }
  }
}
2. ESC ํค๋ก ๋ซ๊ธฐ
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !modal.hidden) {
    modal.hidden = true;
    openBtn.focus();
  }
});
3. ARIA ์์ฑ ์ ์ฉ
- role="dialog": ์คํฌ๋ฆฐ๋ฆฌ๋๊ฐ ๋ํ์ฐฝ์์ ์ธ์งํฉ๋๋ค.
 - aria-modal="true": ๋ฐฐ๊ฒฝ๊ณผ ๋ถ๋ฆฌ๋ ๋ ๋ฆฝ ๋ ์ด์ด์์ ์๋ฆฝ๋๋ค.
 - aria-labelledby="modalTitle": ํ์ดํ์ ์ฐธ์กฐํด ์ ๋ชฉ์ ์๋ ค์ค๋๋ค.
 
4. ํฌ์ปค์ค ๋ณต๊ท
๋ชจ๋ฌ์ ๋ซ์ ๋๋ ๋ค์ ํธ๋ฆฌ๊ฑฐ ๋ ์์(์: "๋ชจ๋ฌ ์ด๊ธฐ" ๋ฒํผ)๋ก ํฌ์ปค์ค๋ฅผ ๋๋๋ ค ์ฌ์ฉ์ ํ๋ฆ์ ๋์ง ์์์ผ ํฉ๋๋ค.
โ ๊ตฌ์กฐ์ ·์๊ฐ์  ๊ฐ์ ์์ด๋์ด
๐ฏ 1. ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ ์ถ๊ฐ
๋ชจ๋ฌ์ด ์ด๋ฆฌ๊ณ ๋ซํ ๋ ๋ถ๋๋ฌ์ด ํธ๋์ง์ ์ ์ฃผ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์์ฐ์ค๋ฌ์์ง๋๋ค.
.modal[hidden] {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.modal {
  opacity: 1;
  transition: opacity 0.3s ease;
}
JS์์๋ modal.hidden = false; ๋์ modal.classList.add('visible'),
๋ซ์ ๋ remove('visible')๋ก ํ ๊ธ ํ๋ฉด์ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ๋ฉด ์ข์์.
๐ง 2. ๋ชจ๋ฌ ์ฌ๋ฌ ๊ฐ ๋์์ ์ํ ๊ตฌ์กฐ ์ผ๋ฐํ
๋ชจ๋ฌ์ด ํ๋๋ฟ์ด ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํด,
JS ๊ตฌ์กฐ๋ฅผ ํจ์ํ์ผ๋ก ๋ง๋ค์ด๋๋ฉด ์ฌ์ฌ์ฉ์ฑ์ด ์ข์์ง๋๋ค.
function openModal(modalId) {
  const modal = document.getElementById(modalId);
  modal.hidden = false;
  trapFocus(modal); // ํฌ์ปค์ค ํธ๋ฉ ํจ์ ์ฌํ์ฉ
}
function closeModal(modalId) {
  const modal = document.getElementById(modalId);
  modal.hidden = true;
}
์ถํ CAD ๋ค์ด๋ก๋๋ ๊ณต์ง์ฌํญ ํ์ ๋ฑ์๋ ๋์ผํ ๊ตฌ์กฐ๋ก ๋์ํ ์ ์์ด์.
๐งญ 3. ๋ฐฐ๊ฒฝ ํด๋ฆญ์ผ๋ก ๋ชจ๋ฌ ๋ซ๊ธฐ ๊ธฐ๋ฅ
์์ธ๋ก ์ฌ์ฉ์๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๊ธฐ๋ํ๋ ๊ธฐ๋ฅ ์ค ํ๋์์.
modal.addEventListener('click', (e) => {
  if (e.target === modal) {
    modal.hidden = true;
    openBtn.focus();
  }
});
๐ชก 4. CSS ๋ณ์ ํ์ฉํ ํ ๋ง ์คํ์ผ
๋ชจ๋ฌ ๋ฐฐ๊ฒฝ์, ๊ทธ๋ฆผ์, ์ฌ๋ฐฑ ๋ฑ์ CSS ๋ณ์๋ก ์ถ์ถํ๋ฉด
์ฌ๋ฌ ํ๊ฒฝ์ ์ฌ์ฌ์ฉํ๊ธฐ ์ฌ์์.
:root {
  --modal-bg: #fff;
  --modal-overlay: rgba(0, 0, 0, 0.4);
}
.modal {
  background-color: var(--modal-overlay);
}
.modal-content {
  background-color: var(--modal-bg);
}
๐ 5. ๋ค๊ตญ์ด ๋์์ ์ํ ํ ์คํธ ๊ด๋ฆฌ ๊ตฌ์กฐ
ํ๋ก์ ํธ์์ ๋ค๊ตญ์ด ํ์ด์ง๋ ์์๋ ๊ฑธ๋ก ๋ณด์ด๋๋ฐ์,
๋ชจ๋ฌ ํ ์คํธ๋ ์์ฑ ๊ธฐ๋ฐ์ผ๋ก ๋ถ๋ฆฌํด๋๋ฉด ๊ตญ์ ํ์ ์ ๋ฆฌํด์.
<p data-text="modal.message">๋ชจ๋ฌ ๋ด์ฉ์
๋๋ค.</p>
const i18n = {
  ko: {
    'modal.message': '๋ชจ๋ฌ ๋ด์ฉ์
๋๋ค.',
  },
  en: {
    'modal.message': 'This is modal content.',
  },
};
function applyLang(lang) {
  document.querySelectorAll('[data-text]').forEach((el) => {
    const key = el.getAttribute('data-text');
    el.textContent = i18n[lang][key] || '';
  });
}
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์ ๊ทผ์ฑ์ ํ ๋ฒ์ ์๋ฒฝํ๊ฒ ํ๊ธฐ๋ณด๋ค,
๊พธ์คํ ๊ฐ์ ํด๊ฐ๋ ์ฌ์ ์ ๊ฐ๊น์์.
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ด ๋ง๋ ๋ชจ๋ฌ ํ๋ํ๋์
๋ ๋ง์ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ์ ์๊ฒ ๋ง๋๋ ์์ ์ถ๋ฐ์ ์ด ๋์์ผ๋ฉด ํฉ๋๋ค.