createPortal
{createPortal(children, domNode, key?)}
createPortal이란 게임 포탈에 나오는 것처럼 '선택된 dom 노드'에 자식요소를 렌더링할 수 있게 해줍니다.
첫번째인자에는 렌더링하고싶은 자식 요소를, 두번째인자에는 렌더링할 dom 노드를 입력합니다.
(세번째 인자는 포탈의 구별 key인데 react query의 key같은 역할을 합니다. 선택사항이기도 하고 보통은 안쓰이는 듯 합니다.)
구현과정
Modal.tsx 작성
/* eslint-disable react/jsx-no-useless-fragment */
import { Dispatch, SetStateAction } from 'react';
import { createPortal } from 'react-dom';
import ModalWrapper from './ModalContentWrapper';
interface ModalProps {
children: React.ReactNode;
showModal: boolean;
setShowModal: Dispatch<SetStateAction<boolean>>;
opacity?: boolean;
}
export default function Modal({
children,
showModal,
setShowModal,
opacity = true,
}: ModalProps) {
return (
<>
{showModal &&
createPortal(
<ModalWrapper
showModal={showModal}
setShowModal={setShowModal}
opacitiyClass={opacity}
>
{children}
</ModalWrapper>,
document.body,
)}
</>
);
}
- 사용처에서 실질적으로 사용될 컴포넌트입니다. createPortal hook을 이용해 렌더링될 요소와, dom node를 지정합니다.
- 또한 선택사항으로 모달 opacity값을 props로 전달받어, 배경의 불투명도를 조절할 수 있습니다. 가장 많이 쓰이는 opacity를 기본값으로 지정하여, opacity값을 전달하지않았을시에는 기본불투명도가 적용됩니다.
ESC키 입력, 모달외부컨텐츠영역 클릭할때 모달이 닫히게 할 커스텀훅
import { RefObject, useEffect } from 'react';
const useEscKeyClose = (
ref: RefObject<HTMLElement | boolean>,
close: () => void,
) => {
const handleEscKeyClose = (event: KeyboardEvent) => {
if (ref.current && event.key === 'Escape') {
close();
}
};
useEffect(() => {
document.addEventListener('keydown', handleEscKeyClose);
return () => {
document.removeEventListener('keydown', handleEscKeyClose);
};
}, []);
};
export default useEscKeyClose;
import { useEffect, RefObject } from 'react';
const useOutSideClick = (ref: RefObject<HTMLElement>, close: () => void) => {
useEffect(() => {
const handleClickOutSide = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
close();
}
};
window.addEventListener('mousedown', handleClickOutSide);
return () => {
window.removeEventListener('mousedown', handleClickOutSide);
};
}, [ref, close]);
};
export default useOutSideClick;
ModalWrapper.tsx 작성
import { Dispatch, SetStateAction, useRef } from 'react';
import useEscKeyClose from '@/hooks/common/useEscKeyClose';
import useOutSideClick from '@/hooks/common/useOutSideClick';
interface ModalContentWrapperProps {
showModal: boolean;
children: React.ReactNode;
setShowModal: Dispatch<SetStateAction<boolean>>;
opacitiyClass: boolean;
}
export default function ModalContentWrapper({
children,
showModal,
setShowModal,
opacitiyClass,
}: ModalContentWrapperProps) {
const modalKeyRef = useRef<HTMLElement | boolean>(showModal);
const modalClickRef = useRef<HTMLDivElement>(null);
useOutSideClick(modalClickRef, () => setShowModal(false));
useEscKeyClose(modalKeyRef, () => setShowModal(false));
return (
<div
className={`fixed inset-0 bg-black ${
opacitiyClass ? 'bg-opacity-75' : 'bg-opacity-0'
} z-50`}
>
<div
ref={modalClickRef}
className="absolute transform -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2"
>
{children}
</div>
</div>
);
}
- 렌더링될 자식요소를 감싸주는 div태그가 있습니다. 이곳에서 배경의 불투명도와 children의 위치를 가운데로 지정해줍니다.
- useRef를 이용해 모달바깥영역이 클리될 경우, ESC버튼이 눌렸을 경우 모달이 닫히도록 세팅했습니다.
App.tsx
"use client";
import { useState } from "react";
import Modal from "./Modal";
import ModalContent from "./ModalContent";
export default function PortalExample() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>Show modal using a portal</button>
<Modal showModal={showModal} setShowModal={setShowModal}>
<ModalContent showModal={showModal} setShowModal={setShowModal} />
</Modal>
</>
);
}
- 사용처입니다. useState로 state와 setState를 지정해준 후, 각 컴포넌트에 props로 전달합니다.
사용방법 요약
1. [필수]사용처에서 초기값 false를 입력한 usestate를 작성합니다.
2. [필수]컴포넌트를 호출 후 props로 showModal={사용처의State} setShowModal={사용처의setstate}를 전달합니다.
3. [선택]컴포넌트의 props로 opacity=false를 전달시 기존투명도 유지됩니다. 전달하지않을시 기본값으로 불투명배경 적용됩니다.
ex) case1과 case2는 동일한 결과를 나타냅니다.
case1. 일반 childeren
case2. 컴포넌트 childeren
//ModalContent 컴포넌트
import { Dispatch, SetStateAction } from 'react';
import AuthButton from '../../pages/auth/AuthComponent/AuthButton';
interface ModalProps {
showModal: boolean;
setShowModal: Dispatch<SetStateAction<boolean>>;
}
export default function LocationModal({ showModal, setShowModal }: ModalProps) {
return (
<div>
<div className="w-[376px] h-[216px] rounded-[10px] bg-white flex flex-row justify-center flex-wrap p-6">
<div className="w-[274px] h-[26px] mt-[10px]">
<div className="text-[#08995C] font-bold text-lg text-center">
위치정보 이용권한 설정이 필요합니다.
</div>
<div className="mt-5 text-[#474C51] text-sm text-center">
<div>내주변 동네를 설정하려면</div>
<span className="font-bold">
사용 중인 브라우저의 위치 권한을 허용
</span>
해주세요.
</div>
</div>
<AuthButton
size="w_340"
color="white_green"
className="mt-[90px] font-bold"
onClickAction={() => setShowModal(!showModal)}
>
닫기
</AuthButton>
</div>
</div>
);
}