-
[React] createPortal 사용 이유 (모달창)React 2024. 4. 4. 20:20
리액트로 코드를 작성하면서 모달창을 만드는 경우가 많습니다.
언듯 보면 간단해 보이지만, 실제로는 예상하지 못한 복잡성을 내포하고 있습니다.
최근 저는 모달창을 구현하면서 z-index 관련 문제가 발생하였고,
이를 해결하기 위해 createPortal을 이용한 내용을 작성해보고자 합니다.| 문제 상황
문제 상황을 보기 위해 리액트 프로젝트를 만들고 간단한 모달창 하나를 만들어 보겠습니다.
먼저 props로 상태를 받아서 화면에 보여주고 닫을 수 있는 Modal 컴포넌트 하나를 만들어 주겠습니다.
// Modal import styles from "./index.module.css"; type Props = { open: boolean; onClose: VoidFunction; }; const Modal = ({ open, onClose }: Props) => { if (!open) return null; return ( <> <div className={styles.overlay} /> <div className={styles.modal}> <div>모달</div> <button onClick={onClose}>닫기</button> </div> </> ); }; export default Modal;
.modal { display: flex; flex-flow: column; justify-content: space-around; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #fff; padding: 30px; width: 300px; height: 200px; z-index: 1000; } .overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.6); z-index: 1000; }
다음으로 모달을 표시할 App에서 해당 Modal을 가져오고,
useState를 통해 상태를 만들고, 이를 props로 넘겨줍니다.
// App.tsx import { useState } from "react"; import "./App.css"; import Modal from "./Modal"; const App = () => { const [isModal, setIsModal] = useState(false); return ( <> <h1>홈</h1> <div> <button onClick={() => setIsModal(true)}>모달 열기</button> <Modal open={isModal} onClose={() => setIsModal(false)} /> </div> </> ); }; export default App;
그럼 다음과 같은 간단한 모달창이 하나 만들어집니다.
여기서 App에서 모달을 감싸고 있는 div박스보다 index가 높은 빨간 박스 하나를 생성해 보겠습니다.
// App const App = () => { const [isModal, setIsModal] = useState(false); return ( <> <h1>홈</h1> <div className="modalWrap"> <button onClick={() => setIsModal(true)}>모달 열기</button> <Modal open={isModal} onClose={() => setIsModal(false)} /> </div> {/* 빨간 박스 */} <div className="higherIndexWrap" /> </> ); };
.modalWrap { position: relative; z-index: 1; border: 1px solid blue; } .higherIndexWrap { position: relative; z-index: 2; background-color: red; padding: 20px; }
여기서 스타일을 보면 모달창의 z-index는 1000, 빨간 박스의 z-index속성은 2로 주었습니다.
그럼 모달창을 열 때 당연히 z-index가 높은 모달창이 최상단에 보이겠다 생각할 수 있지만
z-index 속성은 부모 태그의 z-index값이 우선시되어서 아래와 같이 빨간 박스가 위로 올라오게 됩니다.
이를 해결하려면 모달창을 부모 요소로 감싸지 않고 따로 최상단에 놓는 방식으로 하면 되지만
코드를 작성하고, 스타일을 적용하면서 이런 식으로 부모에 감싸서 스타일을 적용하는 경우가 많습니다.
이때 다른 선택지로 사용할 수 있는 게 리액트에서 제공하는 createPortal을 이용하는 것입니다.
| createPortal
createPortal은 리액트의 한 기능으로, 원하는 컴포넌트를 현재 컴포넌트가 위치한 트리와
다른 위치의 DOM 노드에 렌더링 할 수 있게 해 줍니다.이는 주로 모달, 튤팁, 팝업 등에 유용하게 사용될 수 있고, 다음과 같은 특징이 있습니다.
- createPortal을 사용하면 모달이나 팝업 같은 컴포넌트를 루트 레벨의 DOM 노드로 이동시켜, 앱의 다른 부분과 격리시킬 수 있습니다. 따라서 z-index 충돌과 같은 CSS문제를 방지할 수 있고, 레이아웃의 복잡성을 줄이는 데 도움이 됩니다.
- 포탈을 사용하여 생성된 컴포넌트는 물리적으로는 다른 위치에 있지만, 이벤트 버블링 측면에서는 여전히 React 컴포넌트 트리의 일부로 작동합니다. 따라서 이벤트 버블링을 통한 이벤트 관리를 기존과 똑같이 쉽게 할 수 있습니다.
createPortal은 다음과 같이 사용할 수 있고 각각 인자는 다음을 의미합니다.
ReactDOM.createPortal(children, container, key?)
- children: 이는 렌더링 될 리액트 컴포넌트입니다. createPortal을 통해 다른 DOM 노드로 전송될 콘텐츠입니다.
- container: 이는 children이 렌더링 될 DOM 엘리먼트입니다. 리액트는 이 container 내부에 children을 렌더링 합니다.
- key?: 선택적으로 넣을 수 있고, 리액트의 key prop과 유사합니다.
| 적용
위에 문제가 발생했던 코드에 적용해 보겠습니다.
우선 리액트의 root가 있는 index.html에 모달을 렌더링 할 컨테이너를 하나 생성합니다.
<body> <div id="root"></div> {/* 포탈 박스 */} <div id="portal"></div> <script type="module" src="/src/main.tsx"></script> </body>
그다음 기존에 모달 컴포넌트를 createPortal을 사용해 포탈 박스 안으로 이동시킵니다.
import ReactDOM from "react-dom"; import styles from "./index.module.css"; type Props = { open: boolean; onClose: VoidFunction; }; const Modal = ({ open, onClose }: Props) => { if (!open) return null; // 포탈 박스 가져오기 const portalElement = document.getElementById("portal"); // 포탈 박스 없으면 null if (!portalElement) return null; // 포탈 박스 있으면 이동 return ReactDOM.createPortal( <> <div className={styles.overlay} /> <div className={styles.modal}> <div>모달</div> <button onClick={onClose}>닫기</button> </div> </>, portalElement ); }; export default Modal;
이런 식으로 코드를 작성하면 해당 컴포넌트가 물리적으로 "<div id="portal"></div>" 내부에 위치하게 됩니다.
이에 따라 기존의 z-index 문제도 해결됩니다.
또한 포탈로 생성한 부분이 부모 DOM 밖에 생성되지만 포탈은 여전히 리액트 트리에 존재하기 때문에
리액트 트리에 포함된 상위 요소로 이벤트 버블링이 가능합니다.
'React' 카테고리의 다른 글
[React] next.js ssr api요청에 쿠키 포함해서 보내기 (0) 2024.04.19 Vite.config에 환경변수 넣기 (0) 2024.04.15 [TanStack Query] Optimistic Update(낙관적 업데이트)로 사용자 경험 개선하기 (0) 2024.03.11 [React] React lazy로 웹 성능 최적화 하기 (코드 스플리팅) (0) 2024.02.12 [React] 상태 관리를 통한 사용자 경험 개선 (페이지 전환) (0) 2024.02.06