😍createPortal은..?
https://react.dev/reference/react-dom/createPortal
공식문서를 참고할 수 있습니다.
createPortal은 일부 자식들을 DOM의 다른 부분으로 렌더링할 수 있는 기능을 제공합니다.
편의상 vite, cra 등 보일러플레이트를 사용해 리액트 프로젝트를 구성했다고 가정하고 진행하겠습니다.
리액트로 구성한 프로젝트는 기본적으로 보통 index.jsx 혹은 main.jsx 와 같은 엔트리포인트에서
위와 같이 렌더를 시켜줍니다.
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />
);
자세히 보면 코드의 의미를 쉽게 알아챌 수 있습니다.
as HTMLElement는 TypeScript의 문법으로 신경쓰시지 않으셔도 괜찮습니다.
이것은 확실히 HTMLElement라고 알려주는 코드라고 생각해주시면 되겠습니다.
ReactDOM 라이브러리가 제공하는 reateRoot함수가 반환하는 render 함수를 이용하는데
createRoot 함수는 인자로 먼저 HTMLElement를 받습니다.
이 Element는 Id가 root인 HTML 요소를 의미합니다.
id가 root인 HTML 요소는 어디에 있을까요?
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
프로젝트를 잘 살펴보면 index.html과 같이 미리 정의되어있는 html 파일을 찾을 수 있습니다.
이 html 파일이 개발서버를 구동하고 나면 화면에 보이게되는 html 파일이라고 생각할 수 있습니다.
ReactDOM 라이브러리가 제공하는 함수이며 사용방법은 다음과 같습니다.
createPortal(children, domNode, key?)
첫번째 인자로는 domNode의 children이 될 컴포넌트 혹은 jsx 문법으로 작성한 html 태그가 들어갑니다.
리액트로 렌더링할 수 있는 모든것이 들어갈 수 있다라고 생각해주시면 되겠습니다.
두번째 인자로는 children의 부모가 될 domNode를 셀렉해줍니다.
이것은 미리 생성되어있는 domNode를 셀렉해주어야합니다.
아까 위에서 index.html을 살펴보았던것이 기억나신다면 쉽게 예상할 수 있습니다.
그 index.html에 portal을 태우게할 html 요소를 미리 만들어놓으면 되겠네요!
세번째는 선택인자입니다.
포털의 키로 사용할 고유한 문자열 혹은 숫자를 넣어주면 됩니다.
import { createPortal } from 'react-dom';
// ...
<div>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
예제를 보겠습니다.
포털을 만들려면 react-dom에서 createPortal을 import해주는 것으로 시작합니다.
그리고 첫번째에는 html 태그를 두번째에는 부모요소가 될 html 요소를 셀렉팅해줍니다.
즉 위 코드는 body 태그에 p태그를 자식요소로 달아주는 코드라고 볼 수 있습니다.
🤡모달 만들어보기
https://codesandbox.io/s/tsdml4?file=%2FApp.js&utm_medium=sandpack
리액트 공식문서에서 제공하는 모달에 대한 예제도 존재합니다!
솔직히 저거만 참고하시면서 만들어도 충분하시겠지만
css 측면에서 집중하면서 한번 만들어보겠습니다.
한 컴포넌트에서 모든걸 처리해도 좋지만 역할을 조금 나누어 주겠습니다.
모달창이 띄워졌을 때 뒷배경을 흐리게 만들어주는 backDrop효과를 위한
backDrop 컴포넌트와 실제 Modal의 내용이 들어갈 ModalImage 컴포넌트
그리고 그 둘을 합쳐서 하나의 컴포넌트로 만들 Modal 컴포넌트를 만들어주겠습니다.
index.html
<body class="darkmode lightmode">
<div id="modal"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
index.html에 id가 modal인 div 태그를 만들어주었습니다.
ModalImage.tsx
import BookMarkStar from '../../icons/BookMarkStar';
import { starActiveColor, starNonActiveColor } from '../../colors/colors';
import Delete from '../../icons/Delete';
import ModalProps from '../../types/ModalProps';
import useBookmark from '../../hooks/useBookmark';
import React from 'react';
const ModalImageWidth = '46.5rem';
const ModalImageHeight = '30rem';
const ModalImage = ({ data, src, title, setIsOpen }: ModalProps) => {
const bookMarkHandler = useBookmark();
return (
<figure className="fixed bottom-0 left-0 right-0 top-0 z-20 flex items-center justify-center">
<div className={`relative h-[30rem] w-[46.5rem]`}>
<button
onClick={(event: React.MouseEvent) => {
setIsOpen((state) => !state);
}}
className=" absolute right-[2.875rem] top-[3.125rem] cursor-pointer"
>
<Delete />
</button>
<img
className={` rounded-3xl h-[${ModalImageHeight}] w-[${ModalImageWidth}]`}
src={src}
width={ModalImageWidth}
height={ModalImageHeight}
/>
<div className="absolute bottom-[3.125rem] left-[3.375rem]">
<div className=" flex">
<button onClick={() => bookMarkHandler(data)}>
<BookMarkStar
className=" cursor-pointer"
fill={data.bookmark ? starActiveColor : starNonActiveColor}
/>
</button>
<span className=" px-2 text-xl font-bold text-white">{title}</span>
</div>
</div>
</div>
</figure>
);
};
export default ModalImage;
export { ModalImageWidth, ModalImageHeight };
대부분의 로직은 무시하셔도 좋습니다.
다만 중요한 부분은 figure 태그의 css입니다.
<figure className="fixed bottom-0 left-0 right-0 top-0 z-20 flex items-center justify-center">
position:fixed 이후 top, bottom, left, right의 위치를 모두 0으로 주어 초기화시켜줍니다.
그렇게 해둔 뒤 디스플레이는 flex를 주겠습니다.
display: flex
align item : center
justify content : center
를 통해 가운데에 배치해줍니다.
이렇게 코드를 작성하면 어떤 환경에서든 뷰포트의 항상 가운데에 위치하게됩니다.
z-index의 경우에는 backdrop보다 앞에 위치시켜주기 위함입니다.
BackDrop.tsx
const BackDrop = () => {
return (
<div className=" fixed left-0 top-0 z-20 h-full w-full bg-slate-200 bg-opacity-40"></div>
);
};
export default BackDrop;
다음은 BackDrop의 css입니다.
h-full, w-full로 가로세로를 모두 채워주게 코드를 작성합니다.
그리고 opacity를 낮춰주는 코드입니다.
이 역시 fixed와 left, top 0으로 위치를 고정시켜 주는것이 중요합니다.
Modal.tsx
import BackDrop from './BackDrop';
import { createPortal } from 'react-dom';
import ModalImage from './ModalImage';
import ModalProps from '../../types/ModalProps';
const portalElement = document.getElementById('modal') as HTMLElement;
const Modal = ({ setIsOpen, src, data, title }: ModalProps) => {
return (
<>
{createPortal(
<>
<BackDrop />
<ModalImage
setIsOpen={setIsOpen}
src={src}
data={data}
title={title}
/>
</>,
portalElement,
)}
</>
);
};
export default Modal;
다음은 Modal.tsx입니다.
createPortal을 통해 우리가 만든 backdrop과 modalimage를 취합해서 렌더시킵니다.
그리고 아까 만들어둔 modal이 아이디인 element를 셀렉해서
빈 꺽쇠태그로 감싸주는것은 Fragment라고 생각해주시면 되겠습니다.
🙀모달 닫고 끄는 로직
useState를 이용해 작성하면 됩니다.
조건부 렌더를 이용하여 useState가 true일때에는 모달을 보여주고
state가 false일때는 모달을 안보여주는 식으로 하는거죠!
그리고 setState를 모달에게 전달해주면 되겠습니다.
🤗마치며
항상 이런 모달, 드롭다운과 같은 기능들은 css를 구현하는것이 귀찮은 것 같습니다.
핵심적인 부분은 기억해두거나 포스팅해두고 참고하시면서 작성하시면
생산성이 높아지시지 않을까 합니다.
다음은 완성된 모달입니다.
'react' 카테고리의 다른 글
[react-query]@tanstack/react-query를 사용해보자 (1) | 2023.06.05 |
---|---|
useRef와 forwardRef 사용법을 동시에 배울 수 있는 글이 있다? (2) | 2023.05.22 |
React.lazy와 코드스플리팅 (0) | 2023.04.18 |
immer를 이용해 깊은 복사를 케이크처럼 간단히 먹자 (1) | 2023.04.08 |
React Router를 이용해보자 근데 실습을 곁들인 (0) | 2023.04.07 |