안녕하세요.
이번 글에선 웹을 만들 때 가장 흔하게 볼 수 있는 모달(팝업, 다이얼로그 등)을 같이 만들어보도록 하겠습니다.
아래와 같은 결과물을 목표로 시작해보겠습니다.
모달 스타일링
먼저 모달의 스타일을 잡아보도록 하겠습니다.
styled-components를 사용하였습니다. ModalOverlay는 모달 내용에 집중도를 높이기 위해 dimmed 효과를 줍니다. ModalWrapper는 모달 내용이 들어갈 예정입니다. 여기서 tabIndex를 -1로 해주는 이유는 페이지 내에서 키보드로 포커스 시킬 순서에서 제외하기 위함입니다.
모달은 어떤 트리거를 통해 화면에 그려지기 때문에 페이지 내에서 키보드로 포커스 될 필요가 없습니다. 따라서 명시적으로 -1로 설정해두어 불필요한 키보드 이동을 방지하는 거죠. className을 전달해주는 이유는 styled-components 때문입니다. 모달을 styled-component 방식으로 스타일링이 필요할 경우에 대비해서 말이죠.
위의 컴포넌트가 잘 나오는지 간단히 확인해보겠습니다.
function ModalPage() {
return (
<Modal visible={true}>Hello</Modal>
)
}
이러면 화면에 아주 간단한 모달이 뜨게 됩니다. 그런데 이 모달을 열고 닫으려면 좀 더 추가적인 작업을 해야겠죠?
Modal.js의 코드를 좀 더 수정해보도록 하겠습니다.
function ModalPage() {
const [modalVisible, setModalVisible] = useState(false) const openModal = () => {
setModalVisible(true)
} const closeModal = () => {
setModalVisible(false)
} return (
<>
<button onClick={openModal}>Open Modal</button>
{
modalVisible && <Modal
visible={modalVisible}
closable={true}
maskClosable={true}
onClose={closeModal}>Hello</Modal>
}
</>
)
}
여기까지만 해도 여러분이 원하는 모달의 기능을 할 수 있습니다. closable과 maskClosable은 각각 close 아이콘의 유무, dimmed 처리된 영역을 클릭 했을 때 모달의 닫힘을 결정하는 boolean 값인데요. 일반적으로 모달이 그렇게 동작할 것이라고 예상하기 때문에 이 부분은 defaultProps를 통해 true 값을 주도록 하겠습니다.
Modal.defaultProps = {
closable: true,
maskClosable: true,
visible: false
}
(!) 모달이 떴는데 뒤의 영역 스크롤이 동작해요.
좀 더 추가작업을 해보도록 하겠습니다. 현재 모달을 스크롤 되는 페이지에 추가한다면 모달이 떠있음에도 불구하고 아래와 같이 바닥페이지의 스크롤이 동작할 것입니다.
모바일과 PC 둘 다 대응할 수 있는 방법입니다.
// Modal.jsuseEffect(() => {
document.body.style.cssText = `position: fixed; top: -${window.scrollY}px`
return () => {
const scrollY = document.body.style.top
document.body.style.cssText = `position: ""; top: "";`
window.scrollTo(0, parseInt(scrollY || '0') * -1)
}
}, [])
cssText를 쓰는 이유는 style을 여러번 접근하면 그 횟수만큼 reflow가 발생하게 됩니다. cssText를 이용하면 1번만 계산하기 때문에 이렇게 js로 css를 건드릴 경우 퍼포먼스를 위해 필수로 해주시는게 좋습니다. (class 명을 추가해줘도 됩니다.)
자 이제 스크롤도 막았고 여기서 끝낼 수도 있지만 이번 글의 목표는 효율적인 모달을 만드는 것이 목표이므로 한 가지 작업을 더 추가하도록 하겠습니다.
React Portals (React > 16)
간단하게 Portals 를 사용하면 DOM 의 트리구조에종속되지 않고 원하는 곳에 컴포넌트를 렌더링 할 수 있는 방법입니다.
지금 상태의 modal은 `<div id=”root”>` 아래 어딘가에 존재하고 있을 것입니다. 애니메이션이 없는 상태에서는 사실 크게 문제될 것은 없습니다. 하지만 애니메이션을 저 Modal에 입힐 경우 해당 DOM의 위치에 따라 성능에 영향을 받습니다. index.html 파일을 열어 1줄을 추가해보도록 하겠습니다.
<body>
<noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div>
<div id="modal-root"></div> // 추가 됨</body>
Modal을 이제 modal-root 하위 트리에서 렌더링되게 만들어 보겠습니다.
// Modal.js
return (
<Portal elementId="modal-root">
<ModalOverlay visible={visible} />
<ModalWrapper
className={className}
onClick={maskClosable ? onMaskClick : null}
tabIndex="-1"
visible={visible}
>
<ModalInner tabIndex="0" className="modal-inner" size={size}>
{closable && <CloseButton className="modal-close" onClick={close} />}
{children}
</ModalInner>
</ModalWrapper>
</Portal>
)
Modal 컴포넌트를 위에서 만든 Portal 컴포넌트로 감싸줍니다. elementId 이름의 props를 추가해준 이유는 추후에 언제든지 원하는 아이디에 포탈을 열어주기 위함입니다. (다른 방법들도 많으니 이는 다른 블로그를 참고해주세요.)
완성되었습니다.
최종 코드입니다.
감사합니다.