효율적인 리액트 모달(react-modal) 만들기

bestsup
7 min readMay 1, 2020

--

안녕하세요.

이번 글에선 웹을 만들 때 가장 흔하게 볼 수 있는 모달(팝업, 다이얼로그 등)을 같이 만들어보도록 하겠습니다.

아래와 같은 결과물을 목표로 시작해보겠습니다.

모달 스타일링

먼저 모달의 스타일을 잡아보도록 하겠습니다.

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)

https://reactjs.org/docs/portals.html#event-bubbling-through-portals

간단하게 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를 추가해준 이유는 추후에 언제든지 원하는 아이디에 포탈을 열어주기 위함입니다. (다른 방법들도 많으니 이는 다른 블로그를 참고해주세요.)

완성되었습니다.

최종 코드입니다.

감사합니다.

--

--