Modal은 웹에서 상당히 많이 사용되는 UI인데 기존에는 Modal 컴포넌트를 position: absolute
로 띄어 구현했는데 여러 군데에서 컴포넌트를 재활용함에 있어 다른 엘리먼트의 z-index
가 더 높거나 또는 부모 엘리먼트의 스타일을 상속받아 예상 밖으로 UI가 무너지는 상황을 종종 만나게 되었다.
이런 상황을 피하기 위해 React Portal을 활용하여 더 유연한 Modal 컴포넌트를 구현하였다.
부모 컴포넌트 바깥에 있는 DOM 노드로 자식을 렌더링할 수 있게 해주는 기능이다
ReactDOM.createPortal(child, container)
child는 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류이든 렌더링할 수 있는 React 자식,container는 DOM 엘리먼트이다.
부모 컴포넌트 바깥에 렌더링이 되기 때문에 부모 스타일 상속이나 z-index
에 대한 영향을 피할 수 있다.
★ 위치는 부모 바깥이지만 모든 다른 면에서 일반적인 React 자식처럼 동작한다. (context, props, 이벤트 버블링)
public/index.html
(...)
<body>
<div id="root"></div>
<div id="modal-root"></div> // 모달이 실제로 렌더링 될 곳
</body>
src/component/Portal.tsx
import React from 'react';
import { createPortal } from 'react-dom';
const Portal: React.FC<Props> = ({ children }) => {
const rootElement = document.getElementId('#modal-root');
return (
<>
{rootElement ? createPortal(children, rootElement) : children}
</>
)
}
export default Portal;
Modal 컴포넌트
src/components/Modal/index.tsx
import React from 'react';
import styled from '@emotion/styled/macro';
import './index.css';
import Portal from './Portal';
(...styled-component)
interface Props {
isOpen: boolean;
onClose: () => void;
selector?: string;
}
const Modal: React.FC<Props> = ({ children, onClose, isOpen, selector = '#modal-root' }) => (
<>
{
isOpen ? (
<Portal selector={selector}>
<Overlay>
<Dim onClick={onClose} />
<Container>{children}</Container>
</Overlay>
</Portal>
)
}
</>
)
export default Modal;
버튼을 누르면 Modal 컴포넌트가 Portal을 통해 index-html에서 지정한 Dom 위치에 렌더링된다.
개발자 도구에서 보면 전체 화면과 Modal 컴포넌트 위치가 분리된 것을 볼 수 있다.
next는 html 파일이 없으므로 react 처럼 index.html에 div를 주입할 수 없기 때문에 _document.js파일에 div를 주입한다.
import React from "react";
import Document, { Html, Head, Main } from "next/document";
export default class MyDocument extends Document {
render() {
return (
<Html>
<body>
<div id="portal" />
<Main />
</body>
</Html>
);
}
}
리액트의 컴포넌트에 transition 효과를 쉽게 줄 수 있는 공식 라이브러리, 페이지 간의 라우팅에서 transition 효과를 쉽게 줄 수 있다.
Modal을 띄움에 있어 적용해보도록 하자.
src/components/Modal/index.tsx
import { CSSTransition } from 'react-transition-group';
(...)
const Modal: React.FC<Props> = ({ children, onClose, isOpen, selector = '#modal-root' }) => (
<CSSTransition in={isOpen} timeout={300} classNames="modal" unmountOnExit>
<Portal selector={selector}>
<Overlay>
<Dim onClick={onClose} />
<Container>{children}</Container>
</Overlay>
</Portal>
</CSSTransition>
)
transition 효과를 줄 컴포넌트를 CSSTransition으로 감싸준뒤 속성을 부여한다.
in={isOpen} => 효과 스위치
className => 어떻게 꾸밀지 작성된 클래스
timeout => 작동시간
unmountOnExit => 애니메이션 종료 후 마운트를 해제하기 위한 설정
반대로 마운트 될때 옵션을 줄 수 있는 mountOnEnter도 있으며
둘다 기본값은 false이다.
Modal
src/components/Modal/index.css
.modal-enter {
opacity: 0;
}
.modal-enter-active {
opacity: 1;
transition: opacity 300ms;
}
.modal-exit {
opacity: 1;
}
.modal-exit-active {
opacity: 0;
transition: opacity 300ms;
}
이와같이 원하는 효과를 보여줄 CSS 속성값을 설정하면 적용이 가능하다.