모달은 기본 창 (window) 위에 컴포넌트를 뛰우는 방식
모달 아래의 창은 비활성 상태 (dimmed) 이기 때문에 사용자가 활서된 모달 창 외부의 콘텐츠와 인터페이스 할 수 없음
사용자의 주의 또는 이목을 끌기 위하여 주로 사용
포탈은 외부 DOM에 엘리먼트를 렌더링하는 방법을 제공
ReactDOM.createPortal(child, container)
첫 번째 인자(child
)는 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류든 렌더링할 수 있는 React자식
입니다. 두 번째 인자(container
)는 DOM 엘리먼트입니다.
아래의 예시에서 Portal 컴포넌트는 새로운 엘리먼트를 반환하지 않고, rootElement
하위에 자식 엘리먼트를 렌더링 합니다.
여기서 rootElement
는 선안하기에 따라서 DOM 내부에 어디에 있던지 상관 없습니다.
import React from 'react';
import { createPortal } from 'react-dom';
interface Props {
selector?: string;
}
const Protal: React.FC<Props> = ({ children, selector }) => {
const rootElement = selector && document.querySelector(selector);
return (
<>
{rootElement ? createPortal(children, rootElement) : children}
</>
)
}
export default Portal;
react-transition-group
은 리액트 컴포넌트에 트랜지션(transition)
을 쉽게 줄 수 있는 라이브러리입니다.
컴포넌트가 appear
, enter
, exit
될 때 적절한 트랜지션을 줄 수 있기 때문에 모달 on & off 시 좀 더 자연스러운 화면 전환 효과를 줄 수 있습니다.
<CSSTransition />
은 트랜지션의 appear
, enter
, exit
동안 한 쌍의 클래스 이름을 적용합니다.
첫 번째 클래스가 적용된 다음 CSS 전환을 활성화하기 위해 두 번째 *-active
클래스가 적용됩니다.
전환 후에 일치하는 *-done
클래스 이름이 적용되어 전호나 상태를 유지합니다.
// App.js
function App() {
const [inProp, setInProp] = useState(false);
return (
<div>
<CSSTransition in={inProp} timeout={200} classNames="my-node">
<div>
{"I'll receive my-node-* classes"}
</div>
</CSSTransition>
<button type="button" onClick={() => setInProp(true)}>
Click to Enter
</button>
</div>
);
}
// style.css
.my-node-enter {
opacity: 0;
}
.my-node-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active {
opacity: 0;
transition: opacity 200ms;
}
npx create-react-app modal-playground --template typescript
npm i @emotion/styled @emotion/react react-transition-group
npm i @types/react-transition-group -D
: 모달을 외부 DOM 에 렌더링하는 역할을 합니다.
<div id="root"></div>
<div id="modal-root"></div> // 모달이 실제로 렌더링 될 곳
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import React, { useState } from 'react';
import styled from '@emotion/styled/macro';
import { CSSTransition } from 'react-transition-group';
import Modal from './components/Modal';
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
`;
const Button = styled.button`
width: 280px;
height: 60px;
border-radius: 12px;
color: #fff;
background-color: #3d6afe;
margin: 0;
border: none;
font-size: 24px;
&:active {
opacity: 0.8;
}
`;
const ModalBody = styled.div`
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
background: #fff;
max-height: calc(100vh - 16px);
overflow: hidden auto;
position: relative;
padding-block: 12px;
padding-inline: 24px;
`;
function App() {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
return (
<Container className="app">
<Button onClick={handleOpen}>OPEN</Button>
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalBody>
<h2>Text in a modal</h2>
<p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
</ModalBody>
</Modal>
</Container>
);
}
export default App;
import React, { ReactNode } from "react";
import { createPortal } from "react-dom";
interface Props {
selector?: string;
children?: ReactNode | undefined;
}
const Portal: React.FC<Props> = ({ children, selector }) => {
const rootElement = selector && document.querySelector(selector);
return <>{rootElement ? createPortal(children, rootElement) : children}</>;
};
export default Portal;
import React, { ReactNode } from "react";
import { CSSTransition } from "react-transition-group";
import styled from "@emotion/styled/macro";
import "./modal.css";
import Portal from "./Portal";
const Overlay = styled.div`
position: fixed;
z-index: 10;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
`;
const Dim = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
`;
const Container = styled.div`
max-width: 456px;
position: relative;
width: 100%;
`;
interface Props {
isOpen: boolean;
onClose: () => void;
selector?: string;
children?: ReactNode | undefined;
}
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>
);
export default Modal;
.modal-enter {
opacity: 0;
}
.modal-enter-active {
opacity: 1;
transition: opacity 300ms;
}
.modal-exit {
opacity: 1;
}
.modal-exit-active {
opacity: 0;
transition: opacity 300ms;
}
Reference
FastCampus React 강의