[React] 리액트 포탈 React Portal을 통해 모달 만들기

2023. 3. 5. 00:30WEB Dev/Javascript | REACT | Node.js

728x90

 

 

해당 내용은 Udemy | React 완벽 가이드 with Redux, Next.js, TypeScript 강의에서 일부 발췌하였습니다. 

가독성을 위해 일부 코드삭제가 있어 그대로 따라하면 문제가 생길 수 있습니다.


📌 들어가며

 

유데미 리액트 강의를 들으면서 Portal 사용법을 익혀두면 좋을 것 같다고 생각해서 별도로 작성한다.

유데미 강의는 별도로 필기를 하면서 듣고 있는데, 포탈 강의때는 쉽네 하고 넘겼다가 혼자서 못하겠어서 다시 한 번 복습 겸 정리!

 

 

 

 

📌 리액트 포탈이란

 

Portal은 JSX를 새 창이 아니라 화면 최상단으로 띄워야 할 때, 리액트가 렌더링되는 div#root 내부가 아닌 그 위로 렌더링 할 수 있게 해주는 방법이다. 보통 우리가 흔하게 아는 모달, 그러니까 정보를 가진 JSX를 렌더링 할 때 주로 이용한다.

포탈을 사용하지 않고 모달을 띄우려면 CSS 사용이 복잡하게 되기 때문에 CSS를 다루기 어렵거나 화면을 그릴 때 복잡도가 증가한다면 포탈을 사용해보자.

 

 

📌 사용하기

 

 

 

1. 모달 컴포넌트를 만든다.

 

우선 별도로 모달 컴포넌트를 분리해준다.

모달 컴포넌트 내에는 백드롭(BackDrop, 모달 바깥쪽으로 펼쳐지는 배경을 말한다.), 모달 오버레이(ModalOverlay), 모달(Modal) 세 개의 컴포넌트를 생성해준다.

 

//Modal.js

import { Fragment } from 'react';

const BackDrop = props => {
  return <div className='backdrop'></div>
};

const ModalOverlay = props => {
  return <div className='modal'}>
    <div className='content'>{props.children}</div>
  </div>
};

const Modal = props => {
  return <Fragment>
	</Fragment>
};

export default Modal;

 

 

 

2. ReactDOM을 import 한다.

 

포탈을 사용하기 위해서는 ReactDOM을 react-dom에서 import 해줘야 한다.

 

//Modal.js

import { Fragment } from 'react';
import ReactDOM from 'react-dom';

const BackDrop = props => {
  return <div className='backdrop'></div>
};

const ModalOverlay = props => {
  return <div className='modal'}>
    <div className='content'>{props.children}</div>
  </div>
};

const Modal = props => {
  return <Fragment>
	</Fragment>
};

export default Modal;

 

 

 

 

3. index.html에 렌더링할 위치를 만들어준다.

 

포털이 렌더링 될 위치를 미리 만들어준다.

 

/public/index.html에 들어가서 div#root 위에 div#overlays를 만들어준다.

 

//index.js

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="overlays"></div>
  <div id="root"></div>
  <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
</body>

 

 

 

 

4. ReactDom.createPortal 메소드를 사용한다. 

 

다시 Modal.js 로 돌아와

createPortal에는 첫 번째 인수로 렌더링할 컴포넌트가 필요하고 두 번째 인수로 어디에 포털할 지 해당 노드를 지정해주어야 한다. 

 

ReactDOM.createPortal(무엇을 포털할 지, 어디에 포털할 지)

 

getElementById를 이용해 index에 만들어 둔 div#overlays를 가지고 온다.

 

const portalElement = document.getElementById("overlays");

 

그리고 Fragment로 감싸진 Modal 컴포넌트 JSX에 BackDrop 컴포넌트와 ModalOverlay 컴포넌트를 ReactDOM.createPortal로 불러온다.

* ModalOverlay에는 props.children을 전달해야 한다. 

 

 

//Modal.js

import { Fragment } from 'react';
import ReactDOM from 'react-dom';

const BackDrop = props => {
  return <div className='backdrop'></div>
};

const ModalOverlay = props => {
  return <div className='modal'}>
    <div className='content'>{props.children}</div>
  </div>
};

const Modal = props => {

  const portalElement = document.getElementById("overlays");

  return <Fragment>
    {ReactDOM.createPortal(<BackDrop />, portalElement)}
    {ReactDOM.createPortal(<ModalOverlay>{props.children} </ModalOverlay>, portalElement)}
  </Fragment>
};

export default Modal;

 

이렇게 하면 Modal 컴포넌트 작성은 끝난다.

 

 

 

 

5. 모달로 띄울 컴포넌트를 Modal 컴포넌트로 감싸준다.

 

//Cart.js

import Modal from '../UI/Modal';

const Cart = props => {
  return (
    <Modal>
      <div className='total'>
          <span>Total Amount</span>
          <span>35.62</span>
      </div>
      <div className='actions'>
        <button className='button--alt'>Close</button>
        <button className='button'>Close</button>
      </div>
    </Modal>)
};

export default Cart;

 

이제 모달로 띄울 Cart 컴포넌트를 최상단 컴포넌트인 App.js에 import 해준다.

위치는 크게 상관없다. 결과적으로는 portal로 인해 렌더링은 정리가 될 것이다.

 

//App.js

import { Fragment } from "react";
import Cart from "./components/Cart/Cart";
import Header from "./components/Layout/Header";
import Meals from "./components/Meals/Meals";

function App() {
  return (<Fragment>
    <Cart />
    <Header />
    <main role="main">
    <Meals />
    </main>
    </Fragment>
  );
}

export default App;

 

이렇게만 해도 이제 portal로 인해 화면 최상단에 Modal이 뜨게 된다.

 

 

 

 

 

 

 

6. Modal을 필요할 때만 뜨도록 한다.

 

Modal이 렌더링 되고 있는 최상단 컴포넌트인 App.js 에서 state를 생성해준다.

useState를 추가하고 아래와 같이 state를 만들어준다. cartIsShown의 값이 false일 때는 모달이 뜨지 않고, true일 때 모달일 뜰 수 있게 만들 것이다.

 

  const [cartIsShown, setCartIsShown] = useState(false);

 

 

그리고 액션을 취할 함수들을 만들어준다. 

 

 

  const [cartIsShown, setCartIsShown] = useState(false);

  const showCartHandler = () => {
    setCartIsShown(true);
  };

  const hideCartHandler = () => {
    setCartIsShown(false);
  }

 

Cart 컴포넌트는 cartIsShown이 true여야 렌더링 되기 때문에 아래와 같은 조건문으로 변경해 렌더링 해준다.

 

 

//App.js

import { Fragment } from "react";
import Cart from "./components/Cart/Cart";
import Header from "./components/Layout/Header";
import Meals from "./components/Meals/Meals";

function App() {

  const [cartIsShown, setCartIsShown] = useState(false);

  const showCartHandler = () => {
    setCartIsShown(true);
  };

  const hideCartHandler = () => {
    setCartIsShown(false);
  }

  return (<Fragment>
    {cartIsShown && <Cart />}
    <Header />
    <main role="main">
    <Meals />
    </main>
    </Fragment>
  );
}

export default App;

 

 

실제 모달의 상태를 바꾸는 버튼은 Header.js 컴포넌트에 있기 때문에 <Header />에게 props로 함수를 내려준다.

 

 

//App.js

import { Fragment } from "react";
import Cart from "./components/Cart/Cart";
import Header from "./components/Layout/Header";
import Meals from "./components/Meals/Meals";

function App() {

  const [cartIsShown, setCartIsShown] = useState(false);

  const showCartHandler = () => {
    setCartIsShown(true);
  };

  const hideCartHandler = () => {
    setCartIsShown(false);
  }

  return (<Fragment>
    {cartIsShown && <Cart />}
    <Header onShowCart={showCartHandler} />
    <main role="main">
    <Meals />
    </main>
    </Fragment>
  );
}

export default App;

 

트리거가 될 버튼에 onClick으로 이벤트가 일어날 수 있도록 해준다.

 

//Header.js

import { Fragment } from 'react';
import classes from './Header.module.css';
import mealsImage from '../../assets/meals.jpg';
import HeaderCartButton from './HeaderCartButton';

const HeaderImg = () => {
  return <div className={classes['main-image']}>
    <img src={mealsImage} alt="A table full of delicious food!" />
  </div>
}

const Header = props => {
  return <Fragment>
    <header className={classes.header}>
       <h1>ReactMEALS</h1>
       <HeaderCartButton onClick={props.onShowCart} />
    </header>
    <HeaderImg />
  </Fragment>
};

export default Header;

 

 

이제 닫기를 만들어주기 위해 백드롭과 Close 버튼에 hideCartHandler 함수를 전달해주어야 한다.

onClose라는 props명을 만들어 Cart.js 컴포넌트에 전달해준다.

 

//App.js

import { Fragment } from "react";
import Cart from "./components/Cart/Cart";
import Header from "./components/Layout/Header";
import Meals from "./components/Meals/Meals";

function App() {

  const [cartIsShown, setCartIsShown] = useState(false);

  const showCartHandler = () => {
    setCartIsShown(true);
  };

  const hideCartHandler = () => {
    setCartIsShown(false);
  }

  return (<Fragment>
    {cartIsShown && <Cart onClose={hideCartHandler} />}
    <Header onShowCart={showCartHandler} />
    <main role="main">
    <Meals />
    </main>
    </Fragment>
  );
}

export default App;

 

 

 

우선 Close 버튼을 클릭했을 때 모달이 닫히게 하기 위해 Cart 컴포넌트 내의 Close 버튼으로 hideCartHandler 함수를 전달해준다.

그리고 모달에서는 보통 백드롭을 클릭했을 때 모달이 꺼지는 UX를 많이 구현하기 때문에 백드롭에도 hideCartHandler가 내려갈 수 있도록 Modal.js 컴포넌트에도 전달해준다.

 

//Cart.js

import Modal from '../UI/Modal';

const Cart = props => {
  return (
    <Modal onClick={props.onClose}>
      <div className='total'>
          <span>Total Amount</span>
          <span>35.62</span>
      </div>
      <div className='actions'>
        <button className='button--alt'>Close</button>
        <button className='button' onClick={props.onClose}> close</button>
      </div>
    </Modal>)
};

export default Cart;

 

 

Modal.js로 받은 props 함수를 BackDrop 컴포넌트로 전달해준다.

 

 

//Modal.js

import { Fragment } from 'react';
import ReactDOM from 'react-dom';

const BackDrop = props => {
  return <div className='backdrop' onClick={props.onClick}></div>
};

const ModalOverlay = props => {
  return <div className='modal'}>
    <div className='content'>{props.children}</div>
  </div>
};

const Modal = props => {

  const portalElement = document.getElementById("overlays");

  return <Fragment>
    {ReactDOM.createPortal(<BackDrop onClick={props.onClick} />, portalElement)}
    {ReactDOM.createPortal(<ModalOverlay>{props.children} </ModalOverlay>, portalElement)}
  </Fragment>
};

export default Modal;

 

 

 

 

📌 후기

 

포털 생성 자체는 어렵지 않지만 역시 모달 onoff를 관리하는 state 관리가 어려운 것이었다. 해당 state를 변경하는 함수를 props drilling으로 내려줘야 하는 깊이도 예상이 어렵기도 했다. (다른 상태관리를 사용하면 어렵지 않으나 모달을 재사용하기 위해서는 props로 상태를 내려주는 것이 더 낫다는 강사의 말)

 

Modal은 바닐라로 개발해도 CSS때문에 애를 먹는 지점이 있는데 아예 렌더링하는 위치가 root 상단으로 떠버리니 absolute와 같은 효과가 있어서 CSS 적용도 크게 어렵지 않은 점이 있다.

만약 CSS reset 등을 root 에 걸었다면 해당 속성이 적용이 안 될 것이니 body에서 reset을 해줬는지 확인도 필요할 것 같다.

 

 

 

📌 공식문서

https://ko.reactjs.org/docs/portals.html

728x90