2023. 10. 19. 17:27ㆍ프레임워크̸라이브러리/React
리액트는 index.html
에서 "root"라는 id를 가진 div에 App.js
를 렌더링 시켜 사용자에게 보여줄 화면을 정의한다. 즉, 모든 리액트 컴포넌트/페이지가 root라는 컨테이너 안에 담기게 된다.
그런데 서비스를 만들다보면 modal, dialog, overlay 등 메인 컨테이너가 아닌 별도의 컨테이너에 내용을 담아야 하는 경우가 생긴다. 이 때 z-index만으로 화면을 구성해도 되지만, z-index에 대한 정의가 제대로 되지 않을 경우 원하는 결과가 나오지 않을 수 있다.
그래서 사용하는 것이 리액트의 Portal이다.
Portals
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.
1. Portal을 사용하지 않고 모달을 생성했을 때 발생하는 오류
[page.jsx]
import React, { useState } from 'react';
import Modal from './Modal';
import styles from './modal.module.scss';
const page = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<div className={styles.modalWrapperStyle}>
<button onClick={() => setIsModalOpen(true)}>open</button>
{/* modal */}
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)}>
modal contents
</Modal>
</div>
<div className={styles.higherIndexWrapperStyle}>Z-index 2</div>
</div>
);
};
export default page;
[Modal.jsx]
import React from 'react';
import styles from './modal.module.scss';
const Modal = ({ open, onClose, children }) => {
if (!open) return null;
return (
<>
<div className={styles.overlayStyle} />
<div className={styles.modalStyle}>
<button onClick={onClose}>close</button>
{children}
</div>
</>
);
};
export default Modal;
[modal.module.scss]
.modalWrapperStyle {
position: relative;
z-index: 1;
}
.higherIndexWrapperStyle {
position: relative;
z-index: 2;
background-color: cyan;
padding: 10px;
}
.modalStyle {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 50px;
z-index: 999;
}
.overlayStyle {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 999;
}
z-index가 각각 1, 2인 Wrapper div를 두 개 만들고 z-index: 1;
인 div에 모달 컴포넌트를 넣었다.
z-index가 1로 설정된 부모의 하위에 들어있는 자식들은 아무리 z-index를 높게 설정해도(ex. z-index: 999;
) 1보다 커질 수 없다. 따라서 z-index 2로 설정된 부분이 위의 결과처럼 overlay부분을 뚫고 나오게된다.
2. Portals 적용하기
<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>
children
: 엘리먼트, 문자열, 혹은 fragment와 같이 어떤 종류든 렌더링할 수 있는 요소
domNode
: DOM 엘리먼트
key
: portal의 고유한 문자열 또는 숫자로 사용될 키 (옵션)
1) Container 생성
[public/index.html]
...
<div id="root"></div>
<div id="portal"></div>
...
2) createPortal 이용하기
[Modal.jsx]
import React from 'react';
import { createPortal } from 'react-dom';
import styles from './modal.module.scss';
const Modal = ({ open, onClose, children }) => {
if (!open) return null;
return createPortal(
<>
<div className={styles.overlayStyle} />
<div className={styles.modalStyle}>
<button onClick={onClose}>close</button>
{children}
</div>
</>,
document.getElementById('portal'),
);
};
export default Modal;
따란~🤗 이렇게 createPortal로 설정한 부분이 원하는 DOM 엘리먼트 안으로 쏙 들어간 걸 확인할 수 있다!
포탈은 이벤트 버블링이 가능하다. 비록 포탈로 생성한 부분이 부모 DOM 밖에서 생성되더라도 (DOM 트리에서의 위치와 상관없이) portal은 여전히 React 트리에 존재하기 때문에 React 트리에 포함된 상위로 이벤트 버블링이 가능하다.
* Event Bubbling: 중첩된 자식 요소에서 이벤트가 발생할 때 그 이벤트가 부모로 전달되는 것.
∴ 비록 상위 DOM 트리가 아니더라도, React 트리에서 상위이면 이벤트 버블링이 가능하다.
이벤트 버블링 예시
[page.jsx]
import React, { useState } from 'react';
import Modal from './Modal';
import styles from './modal.module.scss';
const page = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<div
className={styles.modalWrapperStyle}
onClick={() => console.log('clicked')} // here
>
<button onClick={() => setIsModalOpen(true)}>open</button>
{/* modal */}
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)}>
modal contents
</Modal>
</div>
<div className={styles.higherIndexWrapperStyle}>Z-index 2</div>
</div>
);
};
export default page;
모달을 클릭하면 이벤트가 상위로 전달되어서 상위 요소인 div에도 이벤트가 전달되고, div에 있는 핸들러가 호출된다.
'프레임워크̸라이브러리 > React' 카테고리의 다른 글
[개발 표준 수립] WYSIWYG 에디터 선정 (0) | 2023.11.22 |
---|---|
[개발 표준 수립] UI 라이브러리 선정 (0) | 2023.11.22 |
React를 사용하는 이유 (0) | 2023.05.08 |
새 프로젝트에서 MobX를 사용해도 될까? Redux vs MobX (0) | 2020.06.21 |