[ TIL 221215 ] 모달(Modal) 이란?

ponyo·2022년 12월 14일
0

Today I Learned

목록 보기
27/30

모달은 기본 창 (window) 위에 컴포넌트를 뛰우는 방식

모달 아래의 창은 비활성 상태 (dimmed) 이기 때문에 사용자가 활서된 모달 창 외부의 콘텐츠와 인터페이스 할 수 없음

사용자의 주의 또는 이목을 끌기 위하여 주로 사용


포탈 (Potal)

포탈은 외부 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

react-transition-group 은 리액트 컴포넌트에 트랜지션(transition) 을 쉽게 줄 수 있는 라이브러리입니다.

컴포넌트가 appear, enter, exit 될 때 적절한 트랜지션을 줄 수 있기 때문에 모달 on & off 시 좀 더 자연스러운 화면 전환 효과를 줄 수 있습니다.

CSSTransition

<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;
}

모달 (Modal) 실습

Preview

리액트 프로젝트 생성

npx create-react-app modal-playground --template typescript

패키지 설치

npm i @emotion/styled @emotion/react react-transition-group
npm i @types/react-transition-group -D
  • dependencies
    • @emotion/react
    • @emotion/styled
    • react-transition-group
  • devDependencies
    • @types/react-transition-group

Portal

: 모달을 외부 DOM 에 렌더링하는 역할을 합니다.

public/index.html

<div id="root"></div>
<div id="modal-root"></div> // 모달이 실제로 렌더링 될 곳

src/index.tsx

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();

src/App.tsx

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;

src/components/Portal.tsx

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;

src/components/Modal.tsx

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;

src/components/modal.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;
}

Reference
FastCampus React 강의

profile
😁

0개의 댓글