React Portal은 부모 컴포넌트의 DOM 계층 구조 밖에 컴포넌트를 렌더링할수 있게 해주는 역할을 한다.
사용법은 다음과 같이 매우 간결하다.
// 컴포넌트를 렌더링하고자 하는 위치의 부모 컴포넌트의 element
const rootEl = document.getElementById("root");
// 렌더링할 컴포넌트
const PortalComponent = () => <div>portal</div>
ReactDOM.createPortal(PortalComponent, rootEl)
일반적으로 Modal창은 한 프로젝트에서 여러개를 사용하므로, 모듈화시키는 것이 좋다.
필자는 다음고 같이 구현하였다.
우선 Portal을 구현한 컴포넌트를 추가하여 children을 렌더링하게 한다.
const rootEl = document.getElementById("root");
const Portal = ({ children }) => {
return reactDom.createPortal(children, rootEl);
};
그 다음, 뒷배경을 구현하기 위해 그 안에 ModalBackground를 추가한 ModalWrapper 컴포넌트를 만든다.
const ModalWrapper = ({ isOpen, closeModal, children }) => {
const onClickModal = useCallback((e) => {
e.stopPropagation();
}, []);
if (!isOpen) {
return null;
} else {
return (
<Portal>
<ModalBackground onClick={closeModal}>
<div onClick={onClickModal}>{children}</div>
</ModalBackground>
</Portal>
);
}
};
const ModalBackground = styled.div`
width: 100%;
height: 100%;
position: absolute;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 500;
`;
다음과 같이 구현하면, ModalWrapper로 감싸주게 되면 어떤 컴포넌트라도 Modal로써 사용할수 있게 된다.
const MyModalMaker = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const onOpen = () => {
setIsOpen(true)
}
const closeModal = () => {
setIsOpen(false)
}
return (
<>
// 클릭시 Modal open
<button onClick={onOpen}>open</button>
<ModalWrapper isOpen={isOpen} closeModal={closeModal}>
<AlertModal closeModal={closeModal} />
</ModalWrapper>
</>
);
};
다음과 같이 ModalWrapper를 사용하여 Modal관련 로직과 Modal이 될 컴포넌트의 로직을 성공적으로 분리할수 있게 되었다.
하지만 여기서 한가지 번거로운 점이 있는데, 여러 모달창들을 추가할때마다 모달창에 대한 상태를 계속 추가해주어야 한다는 문제점이 생긴다.
// 한 컴포넌트 내에 모달창 3개를 구현할 상황에서..
const [isOpen1, setIsOpen1] = useState<boolean>(false);
const [isOpen2, setIsOpen2] = useState<boolean>(false);
const [isOpen3, setIsOpen3] = useState<boolean>(false);
// 다음과 같이 계속 추가해주어야 한다.
const onOpen1 = () => {...}
const closeModal1 = () => {...}
const onOpen2 = () => {...}
const closeModal2 = () => {...}
const onOpen3 = () => {...}
const closeModal3 = () => {...}
또한 지금은 open, close 기능밖에 없지만 추후 다양한 기능이 추가될 경우, 같이 구현해주어야 할 상태들이 많이 있을수도 있다.
따라서 필자는 해당 기능과 컴포넌트를 커스텀 훅을 사용하여 다음과 같이 추상화하였다.
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
}
const closeModal = () => {
setIsOpen(false);
}
// 다음과 같이 부가기능도 마음껏 추가할수 있다.
const toggleModal = () => {
setIsOpen(!isOpen);
}
// 다음과 같이 isOpen, closeModal이 미리 추가된 ModalWrapper을 리턴
const ModalWrapper = ({children}) => {
return <ModalWrapper isOpen={isOpen} closeModal={closeModal}>{children}</ModalWrapper>
}
// 다른 형태의 Modal도 선택적으로 추가할수 있다.
const NewModalWrapper = ({children}) => {
return <NewModalWrapper isOpen={isOpen} closeModal={closeModal}>{children}</NewModalWrapper>
}
return {isOpen, openModal, closeModal, toggleModal, ModalWrapper};
}
export default useModal;
다 만들어놓은 useModal을 사용하면 단지 다음과 같이 하면 된다!
const MyModalMaker = () => {
const {openModal, closeModal, ModalWrapper} = useModal();
return (
<>
// 클릭시 Modal open
<button onClick={openModal}>open</button>
<ModalWrapper>
<AlertModal closeModal={closeModal} />
</ModalWrapper>
</>
);
};