React Portal 모달 - React Portal modal

(앞서 작성한 내용은 배움의 부족으로 나도 알아보기 애매해서... 다시 정리해서 올렸다.)

2021-10-22 수정 완료//

(궁금한 점이 있거나, 틀린 부분이 있으면 말씀주세요~~!🧸)


프로젝트의 기획이 바뀌면서 모달을 만들어야 할 일이 생겼다.

Modal은 리액트를 공부하다보면 한 번 쯤은 만들어보는 경우가 많이 있어서 간단하다고 생각했다.

그러나 공부를 할 때는 작동 방법에 대해서 배우느라 한 페이지에서 모달 창이 잠깐 나타나기만 하면 됐다.

하지만 프로젝트에서는 여러 곳에서 같은 Modal 창을 사용해야 한다.

그러면 단순히 component로 만들면 되지 않을까? 라고 생각 할 수 있지만 모달창은 의외로 복잡하다 (나만 그런가....)

한 사이트의 글을 발췌해보면 아래와 같다.

"React's design and architectural best practices mean modal dialogs require more effort than throwing together a simple component."

대충 줄이자면 좋은 모달을 만들려면 생각보다 노력을 기울여야한다는 것이다.

물론 일일이 하드코딩으로 만드는 것도 한 방법이지만, 페이지 수가 많아지면 이 또한 고역이다.

그렇다면 좋은 리액트 모달을 만들기 위해서는 우리는 어떤 것을 생각해야 할까?

  1. 접근성을 위해서 DOM 본문 속성 끝에 모달을 추가해야한다. 하지만 리액트에서는 컴포넌트가 최상위 컴포넌트 내부에 마운트 되기 때문에 이는 사실 일반적이라고 할 수 없다.
  2. 모달이 DOM에 마운트 되고 보여질 때 까지 기다린다.
  3. "닫기"와 같은 모달 닫는 창을 닫았을 때 DOM에서 모달창이 사라져아한다.
  4. 모달을 나타내고 숨기는데 jQuery와 같은 라이브러리를 사용하여 DOM을 직접 조작하지 않는다

이를 위해서 portal이라는 것을 사용했다

모달을 나타내기 위해서 두 가지 정도의 컴포넌트가 필요하다.

첫 번째는 모달창을 보여주고 닫게 만드는 useModal.js 파일 그리고 두 번째는 useModal을 이용하여 만든 실제 모달창 이다

하나씩 살펴보자!


(여기서부터 대거 수정! 심플한 방식으로 설명해볼게요!!)

1. index.html에 새로운 div 태그를 만들자.

[ index.html ]

<body>
    <div id="root"></div>
    <div id="modal"></div>
</body>

리액트를 하면서 index.html을 열어본 분들이라면 기본적으로 "root" div 태그가 있는 것을 알고 있을 것이다.

이 root id를 가진 div 태그는 index.js에서 document.getElementById("root")로 사용하고 있다.

React Portal 모달 - React Portal modal

portals를 만들려는 이유 자체가 부모 컴포넌트의 종속을 받지 않기 위해서라는 이유도 있으므로 id가 modal이라는 새로운 div 태그를 만들어줬다.

2. Modal.jsx

html에 div 태그를 심어줬다면 Modal을 만들면 된다.

우선 만든 예시를 보자면 아래와 같다.

[ Modal.jsx ]

import React from "react";
import ReactDOM from "react-dom";

const modalStyle = {
    position: "fixed",
    top: "50%",
    left: "50%",
    transform: "translate(-50%, -50%)",
    width: "600px", maxWidth: "100%",
    height: "400px", maxHeight: "100%",
    backgroundColor: "lightYellow", zIndex: "999"
};
   
const overlayStyle = {
    position: "fixed",
    top: "0",
    left: "0", 
    height: "100%",
    width: "100%",
    backgroundColor: "rgba(0, 0, 0, 0.8)",
    zIndex: "888"
};

const Modal = ({ children }) => {
    return ReactDOM.createPortal(
        <>
            <div style={modalStyle}>{children}</div>
            <div style={overlayStyle}></div>
         </>,
         document.getElementById("modal")
        );
    };

export default Modal;

이 글을 보는 분들이 사용 하시기 편하라고 CSS를 함께 넣어놓은 것이니 헷갈리지 않았으면 좋겠다.

일반적으로 modal을 만들면 뒷 배경을 blur 처리 하던지, 흑백으로 만들기 때문에 div 태그 2개를 만들어놓고, modal용 div에는 children을 넣어두었다.

그리고 중요한 것은 document.getElementById를 해서 아까 index.html에 넣어둔 modal이라는 id를 불러오는 것이다.

3.  modal을 사용하고 싶은 곳.jsx

자, 이제 우리의 portals Modal을 사용하고 싶은 컴포넌트로 가자!

거기서 힘껏 만들어놨던 Modal을 import 해주면 된다.

import React, { useState } from "react";
import Modal from "./Modal";

const 모달사용하고싶은곳 = () => {
    return <Modal>이것이 모달이다!</Modal>
}

이게 끝이다!


여기서 추가적으로 버튼을 만들어서 켜고 끄는 기능을 넣고 싶다면?

마지막 modal을 사용하고 싶은 곳.jsx로가서 useState를 불러온다.

그리고 modal을 열고 닫는 기능도 하나 만들어둔다.

그 예는 아래와 같다.

import React, { useState } from "react";
import Modal from "./Modal";

const 모달사용하고싶은곳 = () => {
    const [isOpen, setIsOpen] = useState(false);

    const modalOpen = () => { setIsOpen(!isOpen); };

    return (
        <button onClick={modalOpen}>모달 열기</button>
        <Modal>
            <h2>모달을 열면 이렇게 되요!</h2>
             <button onClick={modalOpen}>모달 닫기</button>
        </Modal>
    )
}

이렇게 해주면 켜고 끄는 버튼까지 함께 넣을 수 있을 것이다!

(Until Here)



이렇게 만드는 것은 간단한 버젼일 뿐이며, 선택에 따라서 Modal 자체를 생성하는 곳, On/Off Control 하는 곳, Modal 창을 보여주는 컴포넌트 등으로 세세하게 나눠서 할 수도 있으니 자신의 상황에 잘 맞춰서 만들어볼 수도 있다.


사실 이 외에도 React Modal libarary 등을 사용하는 방법도 있다.

더 쉽고 간단한 솔루션들이 있으니 필요에 따라서 사용을 하면 도움이 될 것이다!

This week we'll be making a modal popup, we'll be making it using portals and inert. Both of which are very cool in their own right. I'll be making a portal component we can use to help with the modal, but I'll try and make it in such a way it's helpful for future projects too.

Here's what we're going to make.

Portals

What are portals? Portals are a way to render children into a DOM node anywhere within your app, be it straight into the body or into a specific container.

How is that useful? Specifically in our component it means we can have our <Modal> component anywhere and append the content to the end of the body so it's always over the top of everything. It will also be helpful with setting inert on everything except our <Modal>.

How do I use it? Portals are on ReactDOM you call the function createPortal. This function takes 2 parameters the child, element(s) to spawn, and the container, where to spawn them. Generally you'd expect it to look a little something like this.

return ReactDOM.createPortal(
  this.props.children,
  document.body
);

Enter fullscreen mode Exit fullscreen mode

Portal Component

I'm going to take the relatively simple createPortal and add a layer of complexity and contain it within a component. Hopefully this will make using the <Portal> easier down the line.

Let's dive into the code.

// imports
import React from "react";
import ReactDOM from "react-dom";

// export function
// get parent and className props as well as the children
export default function Portal({ children, parent, className }) {
  // Create div to contain everything
  const el = React.useMemo(() => document.createElement("div"), []);
  // On mount function
  React.useEffect(() => {
    // work out target in the DOM based on parent prop
    const target = parent && parent.appendChild ? parent : document.body;
    // Default classes
    const classList = ["portal-container"];
    // If className prop is present add each class the classList
    if (className) className.split(" ").forEach((item) => classList.push(item));
    classList.forEach((item) => el.classList.add(item));
    // Append element to dom
    target.appendChild(el);
    // On unmount function
    return () => {
      // Remove element from dom
      target.removeChild(el);
    };
  }, [el, parent, className]);
  // return the createPortal function
  return ReactDOM.createPortal(children, el);
}

Enter fullscreen mode Exit fullscreen mode

Inert

What is inert? Inert is a way to let the browser know an element, and it's children, should not be in the tab index nor should it appear in a page search.

How is that useful? Again looking at our specific needs it means the users interactions are locked within the <Modal> so they can't tab around the page in the background.

How do I use it? Inert only works in Blink browsers, Chrome, Opera and Edge, at the moment but it does have a very good polyfill. Once the polyfill is applied you simply add the inert keyword to the dom element.

<aside inert class="side-panel" role="menu"></aside>

Enter fullscreen mode Exit fullscreen mode

const sidePanel = document.querySelector('aside.side-panel');
sidePanel.setAttribute('inert', '');
sidePanel.removeAttribute('inert');

Enter fullscreen mode Exit fullscreen mode

Modal

Now let's put it all together, I'll break the code down into 3 sections styles, events + animations and JSX.

Styles

I'm using styled-components, I'm not really going to comment this code just let you read through it. It's really just CSS.

const Backdrop = styled.div`
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(51, 51, 51, 0.3);
  backdrop-filter: blur(1px);
  opacity: 0;
  transition: all 100ms cubic-bezier(0.4, 0, 0.2, 1);
  transition-delay: 200ms;
  display: flex;
  align-items: center;
  justify-content: center;

  & .modal-content {
    transform: translateY(100px);
    transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
    opacity: 0;
  }

  &.active {
    transition-duration: 250ms;
    transition-delay: 0ms;
    opacity: 1;

    & .modal-content {
      transform: translateY(0);
      opacity: 1;
      transition-delay: 150ms;
      transition-duration: 350ms;
    }
  }
`;

const Content = styled.div`
  position: relative;
  padding: 20px;
  box-sizing: border-box;
  min-height: 50px;
  min-width: 50px;
  max-height: 80%;
  max-width: 80%;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
  background-color: white;
  border-radius: 2px;
`;

Enter fullscreen mode Exit fullscreen mode

Events + Animations

// set up active state
const [active, setActive] = React.useState(false);
// get spread props out variables
const { open, onClose, locked } = props;
// Make a reference to the backdrop
const backdrop = React.useRef(null);

// on mount
React.useEffect(() => {
  // get dom element from backdrop
  const { current } = backdrop;
  // when transition ends set active state to match open prop
  const transitionEnd = () => setActive(open);
  // when esc key press close modal unless locked
  const keyHandler = e => !locked && [27].indexOf(e.which) >= 0 && onClose();
  // when clicking the backdrop close modal unless locked
  const clickHandler = e => !locked && e.target === current && onClose();

  // if the backdrop exists set up listeners
  if (current) {
    current.addEventListener("transitionend", transitionEnd);
    current.addEventListener("click", clickHandler);
    window.addEventListener("keyup", keyHandler);
  }

  // if open props is true add inert to #root
  // and set active state to true
  if (open) {
    window.setTimeout(() => {
      document.activeElement.blur();
      setActive(open);
      document.querySelector("#root").setAttribute("inert", "true");
    }, 10);
  }

  // on unmount remove listeners
  return () => {
    if (current) {
      current.removeEventListener("transitionend", transitionEnd);
      current.removeEventListener("click", clickHandler);
    }

    document.querySelector("#root").removeAttribute("inert");
    window.removeEventListener("keyup", keyHandler);
  };
}, [open, locked, onClose]);

Enter fullscreen mode Exit fullscreen mode

JSX

The main thing to see here is (open || active) this means if the open prop or the active state are true then the portal should create the modal. This is vital in allowing the animations to play on close.

Backdrop has className={active && open && "active"} which means only while the open prop and active state are true the modal will be active and animate into view. Once either of these become false the modal will animate away for our transition end to pick up.

return (
  <React.Fragment>
    {(open || active) && (
      <Portal className="modal-portal">
        <Backdrop ref={backdrop} className={active && open && "active"}>
          <Content className="modal-content">{props.children}</Content>
        </Backdrop>
      </Portal>
    )}
  </React.Fragment>
);

Enter fullscreen mode Exit fullscreen mode

Fin

And that's a modal popup in ReactJS, I hope you found this helpful and maybe have something to take away. As always I'd love to see anything you've made and would love to chat down in the comments. If I did anything you don't understand feel free to ask about it also if I did anything you think I could have done better please tell me.

Thank you so much for reading!
🦄❤️🤓🧠❤️💕🦄🦄🤓🧠🥕