사이드 프로젝트의 헤더에 구현해둔 언어 변경 모달을 이를 createPortal을 사용한 모달로 바꾸려고 시도했었다. 하지만 결과적으로는 전체 화면 사용이 필요 없는 경우였고(hover하면 모달이 뜨고 커서가 나가면 사라지는 형태), position: absolute를 사용한 모달의 css 때문인지 모달을 보여지게 하는 데 애를 먹어서 적용을 하지는 않았다. (이제 와서 생각해보니 모달이 아니라 드롭다운이었을지도)
하지만 이 과정에서 createPortal에 대해 조사하게 되면서 정말 좋은 기능이라는 생각이 들어 포스팅하게 되었다.
createPortal은 리액트에서 제공하는 '부모 컴포넌트의 DOM 계층 구조 밖에서 자식을 렌더링하는 최고의 방법'이다.
리액트의 모든 컴포넌트를 렌더링하는 id='root' div의 형제인 척 하면서 실제로는 root div의 자식 컴포넌트로 기능한다. 렌더링만 root 형제 위치, 즉 root 밖에서 이루어진다.
이러한 리렌더링 이슈, css 제약, 의미적 부조화 문제를 편하게 극복할 수 있게 하는 방법이 바로 createPortal이다. createPortal은 모달 컴포넌트가 실제로는 리액트 DOM 내에서 동작하면서 DOM 상에서는 리액트 DOM 내에 '중첩'되지 않도록 해 준다.
형제처럼 보여도 실제로는 root의 자식처럼 동작하고 실제로는 root 바깥에서 렌더링한다.
// public/index.html
<body>
<div id="root"></div>
<div id="modal"></div> // 추가
</body>
이 과정은 생략 가능해서 ModalPortal 컴포넌트 없이 바로 portal을 사용할 곳에서 createPortal 메서드로 모달 컴포넌트를 감싸도 된다.
하지만 JSX 형태를 최대한 단순하게 유지하기 위해 개인적으로는 이렇게 분리하는 게 더 깔끔한 듯 하다.
//Portal.js
import reactDom from "react-dom";
const ModalPortal = ({ children }) => {
const el = document.getElementById("modal");
return reactDom.createPortal(children, el);
};
export default ModalPortal;
onClose 함수를 prop으로 받는(모달에는 '닫는' 기능만 있으면 된다) 렌더링할 모달 컴포넌트를 만든다.
//Modal.js
import React from "react";
import styled from "styled-components";
const Modal = ({ onClose }) => {
return (
<Background>
<Content>
// ...
</ Content>
</Background>
);
};
export default Modal;
// 모달을 한가운데 위치시키는 Background 스타일
const Background = styled.div`
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 0;
top: 0;
text-align: center;
`;
const Content = styled.div`
height: 100%;
width: 950px;
margin-top: 70px;
position: relative;
overflow: scroll;
background: #141414;
`;
모달을 띄울지 여부는 보통 state를 사용해 조작한다. state를 사용할 스코프를 벗어나지 않도록 여기에 Portal과 모달 컴포넌트를 넣어준다. (state를 활용하여 모달을 조건부 렌더링)
//modal을 띄우려는 컴포넌트 파일
import styled from "styled-components";
import ModalPortal from "../Components/Modal/Portal";
import Modal from "./Modal/Modal";
const Carousel = props => {
const [modalOn, setModalOn] = useState(false);
const handleModal = () => {
setModalOn((modalOn) => !modalOn);
};
return (
<>
<Container>
<button onClick={handleModal}/>
// Portal과 모달 조건부 렌더링
<ModalPortal>
{modalOn && <Modal onClose={handleModal} />}
</ModalPortal>
</Container>
</>
);
};
export default Carousel;
화면 전체에 모달이 떴을 때 뒤의 배경이 스크롤 되는 경우가 있을 수 있는데 이를 막는 방법이다. (참고: 단민님 블로그)
방법: 최상단 html 요소에 css 한 줄을 넣었다 뺐다 하면 된다.
(다른 블로그에서 가져온 코드이므로 위와 코드가 다르다 - 유의)
const BuyMeACoffee: React.FC = () => {
const [isModalOpened, setIsModalOpened] = useState(false);
const html = document.querySelector('html'); // 여기
const openModal = () => {
setIsModalOpened(true);
html?.classList.add('scroll-locked'); // 여기
};
const closeModal = () => {
setIsModalOpened(false);
html?.classList.remove('scroll-locked'); // 여기
};
return (
<>
<S.Button onClick={openModal}>
<S.Text>
{'BuyMeACoffee'.split('').map((char, index) => (
<p key={index}>{char}</p>
))}
</S.Text>
</S.Button>
{isModalOpened &&
createPortal(
<S.ModalBackground onClick={closeModal}>
<S.Modal>
<S.Title>Buy Me A Coffee ☕️</S.Title>
</S.Modal>
</S.ModalBackground>,
document.body,
)}
</>
);
};