[javascript] dialog 태그로 팝업창과 모달 대화상자 만들기

dialog 태그

별도의 창에 표시하지 않는 팝업창과 모달 대화상자를 생성하는 방법은 자바스크립트 코딩을 할 때 꽤 귀찮은 작업입니다. 웹 페이지 안에 모달창을 생성하는 많은 라이브러리들이 있지만 보다 가볍고 빠르게 로딩되는 요즘의 웹 제작 추세와는 거리가 있기 때문에 많은 경우 모달창을 커스텀 제작을 해서 사용하기도 합니다.

웹페이지 가운데 표시되는 인라인 팝업창 디자인 만들기
웹페이지 가운데 표시되는 인라인 팝업창 디자인 만들기 많은 사용자들이 웹브라우저 설정에서 실제 모달 팝업창이 안 뜨게 차단해놓는 경우가 많기 때문에 공지사항이나 알림과 같은 방문자에게 꼭 보여야 하는 내용을 웹사이트에 표시하려면 웹페이지 안에 팝업창처럼 보이는 요소를 표시하는 것

웹 페이지 안에 모달창을 생성해서 작동하도록 하려면 여러 개의 HTML 태그와 JavaScript 코드, 그리고 CSS 코드를를 사용해야 했습니다.

드디어 이 문제를 해결해주는 새로운 HTML 태그가 추가되면서 번거로운 모달창을 제작하는 번거로운 제작 방식은 이제 필요가 없어졌습니다. 태그에는 내장된 자바스크립트 함수가 있어 모달창 처리를 자동으로 처리를 해주기 때문에 복잡한 자바스크립트 코드와 추가의 HTML 태그들이 필요가 없습니다.

그리고 아주 간결하고 짧은 자바스크립트 코드만으로도 모달 대화상자, 또는 팝업창을 열고 닫을 수 있기 때문에 모달 대화상자 제어가 훨씬 쉽고 단순합니다.

CSS로 모달 대화상자를 디자인하는 방법도 단순해져서 대화상자의 레이아웃과 대화상자 외부의 배경 처리를 하나의 속성으로 설정할 수 있습니다.

편의상 웹페이지 안에 인라인으로 표시되는 모달 대화상자, 또는 팝업창을 모달창이라고 하겠습니다.

HTML에서 모달창을 생성하는 태그는 <dialog> 입니다. 이 모달창 표시 태그는 별도의 이벤트처리 없이 완전한 모달창의 기능을 합니다.

모달창 내용 작성

간단하게 로그인 모달창을 하나 만드는 과정을 통해 <dialog> 태그의 사용 방법을 알아보겠습니다. 로그인 모달창에는 ID와 패스워드를 입력하는 <input> 태그 두 개, 그리고 로그인을 실행하는 버튼 하나를 사용합니다.

<dialog id="modal">
    <form name="loginPopup" action="procLogin">
        <label for="userid">ID</label> <input name="userid" id="userid">
        <label for="password">PW</label> <input name="password" id="password">
        <button name="submitForm">로그인</button>
    </form>
</dialog>

단순한 로그인 폼을 <dialog> 태그로 감싼 HTML 코드입니다. <dialog> 태그와 그 안의 태그 내용은 기본 HTML 표시 상태에서는 보이지 않습니다.

dialog 태그는 display: none; 속성으로 생성됩니다.

<form> 태그 안에 버튼이 한 개만 있으면 이 버튼을 누르면 폼은 자동 전송됩니다. 온전한 로그인 폼으로 동작할 수 있도록 로그인 폼을 취소하는 버튼과 함께 모달창의 배경 영역을 클릭하면 로그인을 취소하는 동작까지 하도록 이벤트 처리까지 구현합니다.

<dialog> 태그의 기본 상태구현 방식

<dialog> 태그는 완전히 새로운 구현 방식은 아니며, CSS를 사용해서 구현하던 복잡한 CSS와 Javascript 코드를 내장 태그로 구현한 것입니다. 다만 CSS와 Javascript로 구현하는 것보다는 조금 더 세련되게 내장 기능으로 지원하는 것입니다.

<dialog> 태그의 기본 CSS는 다음과 같습니다.

dialog {
    display: none;
    position: absolute;
    inset-inline-start: 0px;
    inset-inline-end: 0px;
    width: fit-content;
    height: fit-content;
    background-color: canvas;
    color: canvastext;
    margin: auto;
    border-width: initial;
    border-style: solid;
    border-color: initial;
    border-image: initial;
    padding: 1em;
}

<dialog> 태그의 내장 함수

<dialog> 태그에는 다음 두 개의 함수가 기본 내장되어 있습니다.

  • showModal(): 함수를 호출하면 <dialog> 태그가 모달창으로 웹 브라우저 가운데 표시됩니다. <dialog> 안의 HTML 내용이 모달창 안에 표시됩니다.
  • close(): 표시중인 모달창을 닫습니다.

두 개의 함수만으로 모달창을 열었다 닫을 수 있습니다. 다음과 같은 단순한 HTML 태그로 간단하게 모달창을 열었다 닫는 기능을 자바스크립트로 구현해보겠습니다.

HTML 페이지가 로디오디면 모달창이 바로 표시되도록 이벤트 핸들러를 등록해 모달창을 표시해보겠습니다.

window.addEventListener('DOMContentLoaded',()=>{
    document.getElementById('modal').showModal();
})

showModal() 함수를 호출해서 모달 상태가 되면 <dialog> 태그에는 2가지 변경 사항이 적용됩니다.

  1. <dialog> 태그에 open 속성이 추가됩니다. <dialog> 태그를 보이도록 합니다.
  2. <dialog> 태그 끝나는 태그 앞(beforeEnd)에 ::backdrop 가상 요소가 추가됩니다. ::backdrop 가상 요소는 모달창 뒤에 보이는 웹페이지 내용의 접근을 차단하는 모달 기능을 구현합니다.

<dialog> 태그에 추가된 두가지 변경 내용은 다음과 같은 기능을 합니다.

open 속성

<dialog> 태그가 보이도록 display 속성을 변경합니다.

dialog[open] {
    display: block;
}

showModal() 함수로 <dialog> 태그에 open 속성을 추가하면 웹브라우저의 내부 구현체가 동작하면서 <dialog> 태그에 다음 CSS가 추가 적용됩니다.

dialog:-internal-dialog-in-top-layer {
    position: fixed;
    inset-block-start: 0px;
    inset-block-end: 0px;
    max-width: calc(100% - 6px - 2em);
    max-height: calc(100% - 6px - 2em);
    user-select: text;
    visibility: visible;
    overflow: auto;
}

구글 크롬 웹 브라우저를 기준으로 가상 클래스 "-internal-dialog-in-top-layer "가 추가된 <dialog> 태그는 HTML 컨텐츠 최상위 요소로 처리됩니다. <dialog> 태그에 open 속성이 추가되면 </html> 태그 밑에 #top-layer라는 가상 레이어가 추가되면서 <dialog> 태그를 이 레이어 위치로 이동시키게 됩니다.

웹 브라우저 내부 구현체이기 때문에 사용자가 제어할 수는 없지만 동작 방식이 이렇다는 것은 알고 넘어가야 합니다.

<dialog> 태그가 없던 시기에 모달창 구현을 위해 모달창으로 사용하는 태그를 Z-Index를 사용해 최상위 레이어로 이동시키는 방식을 웹 브라우저 내부에 구현했다고 생각하면 됩니다.

::backdrop 가상 요소

모달창으로 표시되는 <dialog> 태그의 내용과 웹페이지의 다른 HTML 내용을 구분하는 일종의 가림막 역할을 합니다. 마우스 이벤트 전달을 차단해서 다른 웹페이지 내용에 접근하는 것을 막습니다.

::backdrop 가상요소는 다음의 CSS가 기본 적용됩니다. 90% 투명도를 가진 검정 배경색으로 된 요소가 웹 브라우저 창 전체 크기로 모달창 배경을 채워서 배경의 다른 웹페이지 컨텐츠로의 접근을 막습니다.

dialog:-internal-dialog-in-top-layer::backdrop {
    position: fixed;
    inset: 0px;
    background: rgba(0, 0, 0, 0.1);
}

::backdrop 가상 요소는 CSS로 백드롭 배경색과 일수 속성을 커스터마이징 할 수 있습니다.

dialog::backdrop {
    background: rgba(0,255,255,.25);
}  

모달창 열기

모달창을 열고 닫는 기능을 하는 함수 두 개를 작성합니다. 별다른 기능은 없고 <dialog> 태그의 내장 함수인 showModal(), close() 함수를 호출하는 래퍼 기능만 합니다.

// 모달창 열기
function openDialog() {
    document.getElementById('modal').showModal();
}

// 모달창 닫기
function closeDialog() {
    document.getElementById('modal').close();
}

모달창을 닫는 방법은 뒤에서 설명하므로 먼저 모달창을 열어보겠습니다. 로그인 모달창을 호출하는 기능을 하는 버튼을 하나 만듭니다.

<input type="button" id="btnOpenDialog" value="모달창열기">

버튼에 클릭 이벤트 리스너를 추가해서 앞서만든 openDialog() 함수를 호출하도록 합니다. 로그인 버튼을 클릭하면 로그인 모달창이 표시됩니다.

document.getElementById('btnOpenDialog').addEventListener('click', openDialog);        

모달창 닫기

모달창의 "로그인" 버튼을 누르면 폼이 전송되므로 로그인을 취소할 수 있도록 로그인 폼 안에 폼 전송 없이 모달창을 닫는 "취소" 버튼을 추가합니다.

"취소" 버튼에는 버튼의 타입을 "button"으로 선언(type="button")해야 폼이 전송되지 않고 이벤트 핸들러로 등록한 closeDialog() 함수가 호출됩니다.

<dialog id="modal">
    <form name="loginPopup" action="procLogin">
        <label for="userid">ID</label> <input name="userid" id="userid">
        <label for="password">PW</label> <input name="password" id="password">
        <button name="submitForm">로그인</button>
        <button type="button" id="btnCloseDialog">취소</button>
    </form>
</dialog>

취소 버튼이 모달창을 닫는 함수를 호출하도록 클릭 이벤트 핸들러를 등록합니다.

document.getElementById('btnCloseDialog').addEventListener('click', closeDialog);     

백드롭 영역을 클릭하면 모달창이 닫히게 하기

모달창 열기/닫기는 완성되었지만, 모달창을 닫을 때는 항상 "취소" 버튼을 눌러 닫아야 합니다. UI의 편의성을 조금 더 높이려면 모달창 바깥의 백드롭 영역을 클릭했을 때 "취소" 버튼을 누른것처럼 동작하도록 해야 합니다.

가상요소인 ::backdrop은 쿼리 선택자로 선택할 수 없으므로 클릭한 영역이 모달창 영역이 아닌지를 확인하는 방식으로 판단해야 합니다.

클릭한 요소가 모달창 안에 포함된 요소인지 contains() 함수로 확인해서 포함되지 않았으면 모달창을 닫습니다.

const dialog = document.getElementById('modal');
dialog.addEventListener('click', (e)=>{!dialog.contains(e.target)??dialog.close();});

이 방법은 잘 동작하지만 한가지 문제가 있습니다. <dialog> 태그 안의 폼, 또는 폼 요소를 클릭하면 모달창이 잘 닫히지만 폼과 모달창 사이의 빈 영역을 클릭하면 모달창이 닫히지 않습니다.

녹색 부분이 패딩 영역으로 클릭해도 등록한 클릭 이벤트 리스너가 호출되지 않는다.

<dialog> 태그는 기본 값으로 1em의 패딩 영역을 가지고 있습니다.패딩 영역을 클릭하면 contains() 함수로 확인했을 때 클릭한 요소가 <dialog> 태그 안에 포함된 요소가 아니기 때문에 모달창이 닫힙니다.

따라서 <dialog> 태그 안의 요소들을 래퍼 태그를 이용해서 감싼 후, <dialog> 태그의 패딩을 없애고 래퍼 태그에 패딩 값을 지정하면 <dialog> 태그의 패딩 영역 문제를 피할 수 있습니다.

<dialog id="modal">
    <div class="wrapper">
        <form name="loginPopup">
            <label for="userid">ID</label> <input name="userid" id="userid">
            <label for="password">PW</label> <input name="password" id="password">
            <button name="submitForm">로그인</button>
            <button type="button" id="btnCloseDialog">취소</button>
        </form>
    </div>
</dialog>

CSS는 다음과 같이 작성해서 패딩 영역을 유지합니다.

#modal{
    padding: 0;
}
.wrapper{
    padding: 1em;
}

앞서 작성했던 모달창 클릭 이벤트 핸들러는 다음과 같이 수정합니다. 래퍼 태그가 모달창 표시 영역을 채우고 있기 때문에 래퍼 태그와 래퍼 태그 안의 폼 요소들만 이벤트 핸들러 콜백 함수를 호출합니다.

const dialog = document.getElementById('modal');
const wrapper = document.querySelector('.wrapper')
dialog.addEventListener('click', (e)=>!wrapper.contains(e.target)&&dialog.close());

코드 완성

모달창을 열고 닫는 openDialog(), closeDialog() 함수는 한 개의 함수로 더 간결하게 구현할 수 있습니다.

파라미터로 불리언 값을 넘겨 true이면 모달창 표시, false면 모달창을 닫는 내장 함수를 호출하도록 합니다. 모달창을 열고 닫는 이벤트 핸들러에서 호출하는 함수도 변경합니다.

//모달창 열기/닫기
const showDialog = (show)=>show ? dialog.showModal():dialog.close();

// 열기/닫기 버튼 이벤트 핸들러 추가
document.getElementById('btnOpenDialog').addEventListener('click', showDialog, true);        
document.getElementById('btnCloseDialog').addEventListener('click', showDialog, false);

모달창 열기/닫기 함수를 조금 더 세련되게 구현하고 싶으면 불리언 파라미터 없이 모달창의 현재 표시 여부에 따라 토글(toggle) 되도록 하면 됩니다.

const showDialog = ()=>!document.querySelector('#dialog[open]') ? dialog.showModal():dialog.close();