문제의 발단
앞서 배운 Flagment와 더불어 간결한 코드를 작성하는 데 도움을 주는 기능이 있다.
아래의 예를 살펴보자.
이 코드를 실제 DOM으로 렌더링하게 되면 오른쪽 HTML처럼 렌더링 될 것이다.
이 코드 자체에는 문제는 없지만 코딩 방식에 문제가 있다.
Modal은 의미적이거나 간결한 HTML 구조를 갖추는 관점에서 보면 별로 좋지 못하다.
기본적으로 Modal은 페이지 위에 표시되는 overlay이기 때문이다.
Modal은 전체 페이지에 대한 overlay며 다른 것 위에 있다.
그런데 이런 Modal이 HTML 코드 안에 중첩되어 있다면 기술적으로는 스타일링 덕분에 잘 작동하더라도 문제가 발생할 수 있다.
만약 overlay 내용이 중첩되어 있다면, 렌더링 되는 HTML 코드를 해석할 때 이것이 일반적인 overlay인지 인식하지 못할 수 있다.
왜냐하면 CSS 스타일링은 렌더링하는 요소를 해석하는 데이 그다지 큰 역할을 하지 않기 때문이다.
또한 구조적으로 HTML코드 안 깊은 곳에 자리잡고 있기 때문에 Modal이 다른 모든 내용에 대한 overlay인지 명확하지 않다.
이와 비슷한 문제로 일반적인 웹 개발에서는 HTML, CSS, JavaScript는 매우 유연하기 떄문에 많은 것들이 작동할 수 있다.
그렇다고 해서 작동한다는 이유만으로 좋은 구현이라고 할 수 없다.
<div onClick={clickHandler}>Click me, I'm a bad button</div>
이러한 문제들는 React의 개념을 사용하여 해결할 수 있다.
Portal
React의 portal을 사용하여 왼쪽의 코드를 유지하면서 데이터를 전달할 때 마찰이 없도록 할 수 있다.
그 방법으로는 Modal이 담긴 HTML 내용을 일반적인 중첩된 코드 안이 아니라 다른 곳에 렌더링하면 된다.
일반적으로 Create React App으로 생성된 project의 코드들은 <div id="root">
의 내부 코드라는 사실을 확인할 수 있다.
<html>
<head></head>
<body>
<div id="root">
<!- YOUR CODE ... -!>
</div>
</body>
<html>
이는 index.js
에서 해당 div아래로 App component를 할당해주었기 때문이며, 개발자는 그 아래로 코드를 작성할 수 밖에 없다.
// 📂 index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
물론 큰 차이가 없겠지만 back-drop과 overlay를 <body>
의 직계 자식이 되게끔 만들고 싶다.
Portal에는 크게 두가지가 필요하다.
- component를 이동시킬 장소를 설정해야 함.
- component에게 (1)장소에 portal되어야 한다고 명시해줘야 함.
우선 그 첫 번째로 이동시킬 장소를 표시해보자.
나중에 찾아올 수 있게 div에 id를 추가해준다.
📂 public/index.html
...
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="backdrop-root"></div>
<div id="overlay-root"></div>
<div id="root"></div>
...
root를 여러개 만들어서 다른 종류의 component들을 여기로 portal될 수 있게 할 수 있다.
portal 불러오기
protal은 react
에 정의되어 있지는 않지만 React와 함께 제공되어지는 다른 library인 react-dom
에서 정의해준다.
react
를 모든 React의 기능들이 담긴 일종의 라이브라고 한다면 react-dom
은 그런 로직들과 기능들을 웹 브라우저로 가져오기 위해서 사용되며, DOM에서의 작업들과 호환되게 해주는 library이다.
즉, react-dom
은 브라우저에 대한 React용 adapter의 일종이라고 봐도 무관하다.
react-dom
을 불러오기 위해서는 아래 코드와 같이 작성한다.
import 무언가 from "react-dom";
이 무언가는 ReactDom이라고 써도 되고 이외의 원하는 이름을 써도 된다.
위와 같이 import하게 되면 이제 ReactDom의 createPortal
method를 호출할 수 있게 된다.
createPortal method
해당 method는 두가지 parameter를 취한다.
(렌더링되어야 하는 React 노드, 렌더링되어야 하는 실제 DOM의 컨테이너를 가르키는 포인터)
여기서 중요한 것은 렌더링되어야 하는 React 노드는 JSX코드여야하는데, component 자체를 삽입해주면 된다.
component 자체를 삽입함으로써 props를 그대로 전달하 수 있다는 장점이 있다.
2번째 parameter인 포인터는 component를 이동시킬 장소를 나타내줘야하기 때문에 DOM API를 사용한다.
예를 들어 document.getElementById
를 사용하여 포인터를 넘겨줄 수 있다.
이 API를 사용하여 실제 DOM 요소에 접근하는 것이다.
일반적으로 React에서는 React가 대신 수행하기 때문에 사용하지 않지만 여기서는 명시적으로 사용되어야 한다.
마찬가지로 앞에서 살펴본 index.js
파일도 같은 구문이 들어간 것을 볼 수 있다.
// 📂 index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
거기서도 getElementById
로 선택한 장소를 포인터로 넘겨줬었다.
완성된 코드
import React from "react";
import ReactDOM from "react-dom";
import Card from "./Card";
import Button from "./Button";
import styles from "./ErrorModal.module.css";
const Backdrop = (props) => {
return <div className={styles.backdrop} onClick={props.onConfirm} />;
};
const ModalOverlay = (props) => {
return (
<Card className={styles.modal}>
<header className={styles.header}>
<h2>{props.title}</h2>
</header>
<div className={styles.content}>
<p>{props.message}</p>
</div>
<footer className={styles.actions}>
<Button onClick={props.onConfirm}>Okay</Button>
</footer>
</Card>
);
};
const ErrorModal = (props) => {
return (
<React.Fragment>
{ReactDOM.createPortal(
<Backdrop onConfirm={props.onConfirm} />,
document.getElementById("backdrop-root")
)}
{ReactDOM.createPortal(
<ModalOverlay
title={props.title}
message={props.message}
onConfirm={props.onConfirm}
/>,
document.getElementById("overlay-root")
)}
</React.Fragment>
);
};
export default ErrorModal;
마무리
정리하자면 portal의 핵심은 렌더링된 HTML의 내용을 다른 곳으로 옮기는 것이다.
JSX코드 안에서 이전과 마찬가지로 component를 사용할 수 있다.
개발자가 느끼는 변동사항은 없으며 이전에 사용했던 것처럼 사용할 수 있다.
이게 portal의 멋진 점이다. 어디든지 사용할 수 있으며 렌더링되는 실제 DOM 안에서만 이동시킬 수 있다.