MUI(Material-UI)는 React 애플리케이션에서 UI 컴포넌트를 구축할 때 매우 유용한 라이브러리입니다. MUI에서 제공하는 Popper와 Popover 컴포넌트는 둘 다 위치 지정과 관련된 컴포넌트지만, 그 사용 방법과 구현 방식에는 몇 가지 중요한 차이점이 있습니다. 이 글에서는 MUI의 Popper와 Popover의 차이점을 비교하고, 각 컴포넌트의 사용 사례와 장단점을 살펴보겠습니다.
createPortal을 사용하여 렌더링됩니다. 이를 통해 컴포넌트는 DOM 계층 구조에서 벗어나 다른 컨테이너에 렌더링될 수 있습니다. Portal을 사용하면 이벤트 버블링 문제를 해결하고, z-index 관리를 쉽게 할 수 있습니다.아래는 MUI의 Popper 컴포넌트 코드입니다.
// @mui/base/Popper
import * as React from 'react';
import {
chainPropTypes,
HTMLElementType,
refType,
unstable_ownerDocument as ownerDocument,
unstable_useEnhancedEffect as useEnhancedEffect,
unstable_useForkRef as useForkRef,
} from '@mui/utils';
import { createPopper, Instance, Modifier, Placement, State, VirtualElement } from '@popperjs/core';
import PropTypes from 'prop-types';
import { unstable_composeClasses as composeClasses } from '../composeClasses';
import { Portal } from '../Portal';
import { getPopperUtilityClass } from './popperClasses';
import { PolymorphicComponent, useSlotProps, WithOptionalOwnerState } from '../utils';
import {
PopperPlacementType,
PopperTooltipProps,
PopperTooltipTypeMap,
PopperChildrenProps,
PopperProps,
PopperRootSlotProps,
PopperTransitionProps,
PopperTypeMap,
} from './Popper.types';
import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
function flipPlacement(placement?: PopperPlacementType, direction?: 'ltr' | 'rtl') {
if (direction === 'ltr') {
return placement;
}
switch (placement) {
case 'bottom-end':
return 'bottom-start';
case 'bottom-start':
return 'bottom-end';
case 'top-end':
return 'top-start';
case 'top-start':
return 'top-end';
default:
return placement;
}
}
function resolveAnchorEl(
anchorEl:
| VirtualElement
| (() => VirtualElement)
| HTMLElement
| (() => HTMLElement)
| null
| undefined,
): HTMLElement | VirtualElement | null | undefined {
return typeof anchorEl === 'function' ? anchorEl() : anchorEl;
}
function isHTMLElement(element: HTMLElement | VirtualElement): element is HTMLElement {
return (element as HTMLElement).nodeType !== undefined;
}
function isVirtualElement(element: HTMLElement | VirtualElement): element is VirtualElement {
return !isHTMLElement(element);
}
const useUtilityClasses = () => {
const slots = {
root: ['root'],
};
return composeClasses(slots, useClassNamesOverride(getPopperUtilityClass));
};
const defaultPopperOptions = {};
const PopperTooltip = React.forwardRef(function PopperTooltip<
RootComponentType extends React.ElementType,
>(props: PopperTooltipProps<RootComponentType>, forwardedRef: React.ForwardedRef<HTMLDivElement>) {
const {
anchorEl,
children,
direction,
disablePortal,
modifiers,
open,
placement: initialPlacement,
popperOptions,
popperRef: popperRefProp,
slotProps = {},
slots = {},
TransitionProps,
// @ts-ignore internal logic
ownerState: ownerStateProp, // prevent from spreading to DOM, it can come from the parent component e.g. Select.
...other
} = props;
const tooltipRef = React.useRef<HTMLElement>(null);
const ownRef = useForkRef(tooltipRef, forwardedRef);
const popperRef = React.useRef<Instance | null>(null);
const handlePopperRef = useForkRef(popperRef, popperRefProp);
const handlePopperRefRef = React.useRef(handlePopperRef);
useEnhancedEffect(() => {
handlePopperRefRef.current = handlePopperRef;
}, [handlePopperRef]);
React.useImperativeHandle(popperRefProp, () => popperRef.current!, []);
const rtlPlacement = flipPlacement(initialPlacement, direction);
/**
* placement initialized from prop but can change during lifetime if modifiers.flip.
* modifiers.flip is essentially a flip for controlled/uncontrolled behavior
*/
const [placement, setPlacement] = React.useState<Placement | undefined>(rtlPlacement);
const [resolvedAnchorElement, setResolvedAnchorElement] = React.useState<
HTMLElement | VirtualElement | null | undefined
>(resolveAnchorEl(anchorEl));
React.useEffect(() => {
if (popperRef.current) {
popperRef.current.forceUpdate();
}
});
React.useEffect(() => {
if (anchorEl) {
setResolvedAnchorElement(resolveAnchorEl(anchorEl));
}
}, [anchorEl]);
useEnhancedEffect(() => {
if (!resolvedAnchorElement || !open) {
return undefined;
}
const handlePopperUpdate = (data: State) => {
setPlacement(data.placement);
};
if (process.env.NODE_ENV !== 'production') {
if (
resolvedAnchorElement &&
isHTMLElement(resolvedAnchorElement) &&
resolvedAnchorElement.nodeType === 1
) {
const box = resolvedAnchorElement.getBoundingClientRect();
if (
process.env.NODE_ENV !== 'test' &&
box.top === 0 &&
box.left === 0 &&
box.right === 0 &&
box.bottom === 0
) {
console.warn(
[
'MUI: The `anchorEl` prop provided to the component is invalid.',
'The anchor element should be part of the document layout.',
"Make sure the element is present in the document or that it's not display none.",
].join('\n'),
);
}
}
}
let popperModifiers: Partial<Modifier<any, any>>[] = [
{
name: 'preventOverflow',
options: {
altBoundary: disablePortal,
},
},
{
name: 'flip',
options: {
altBoundary: disablePortal,
},
},
{
name: 'onUpdate',
enabled: true,
phase: 'afterWrite',
fn: ({ state }) => {
handlePopperUpdate(state);
},
},
];
if (modifiers != null) {
popperModifiers = popperModifiers.concat(modifiers);
}
if (popperOptions && popperOptions.modifiers != null) {
popperModifiers = popperModifiers.concat(popperOptions.modifiers);
}
const popper = createPopper(resolvedAnchorElement, tooltipRef.current!, {
placement: rtlPlacement,
...popperOptions,
modifiers: popperModifiers,
});
handlePopperRefRef.current!(popper);
return () => {
popper.destroy();
handlePopperRefRef.current!(null);
};
}, [resolvedAnchorElement, disablePortal, modifiers, open, popperOptions, rtlPlacement]);
const childProps: PopperChildrenProps = { placement: placement! };
if (TransitionProps !== null) {
childProps.TransitionProps = TransitionProps;
}
const classes = useUtilityClasses();
const Root = slots.root ?? 'div';
const rootProps: WithOptionalOwnerState<PopperRootSlotProps> = useSlotProps({
elementType: Root,
externalSlotProps: slotProps.root,
externalForwardedProps: other,
additionalProps: {
role: 'tooltip',
ref: ownRef,
},
ownerState: props,
className: classes.root,
});
return (
<Root {...rootProps}>{typeof children === 'function' ? children(childProps) : children}</Root>
);
}) as PolymorphicComponent<PopperTooltipTypeMap>;
/**
* Poppers rely on the 3rd party library [Popper.js](https://popper.js.org/docs/v2/) for positioning.
*
* Demos:
*
* - [Popper](https://mui.com/base-ui/react-popper/)
*
* API:
*
* - [Popper API](https://mui.com/base-ui/react-popper/components-api/#popper)
*/
const Popper = React.forwardRef(function Popper<RootComponentType extends React.ElementType>(
props: PopperProps<RootComponentType>,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const {
anchorEl,
children,
container: containerProp,
direction = 'ltr',
disablePortal = false,
keepMounted = false,
modifiers,
open,
placement = 'bottom',
popperOptions = defaultPopperOptions,
popperRef,
style,
transition = false,
slotProps = {},
slots = {},
...other
} = props;
const [exited, setExited] = React.useState(true);
const handleEnter = () => {
setExited(false);
};
const handleExited = () => {
setExited(true);
};
if (!keepMounted && !open && (!transition || exited)) {
return null;
}
// If the container prop is provided, use that
// If the anchorEl prop is provided, use its parent body element as the container
// If neither are provided let the Modal take care of choosing the container
let container;
if (containerProp) {
container = containerProp;
} else if (anchorEl) {
const resolvedAnchorEl = resolveAnchorEl(anchorEl);
container =
resolvedAnchorEl && isHTMLElement(resolvedAnchorEl)
? ownerDocument(resolvedAnchorEl).body
: ownerDocument(null).body;
}
const display = !open && keepMounted && (!transition || exited) ? 'none' : undefined;
const transitionProps: PopperTransitionProps | undefined = transition
? {
in: open,
onEnter: handleEnter,
onExited: handleExited,
}
: undefined;
return (
<Portal disablePortal={disablePortal} container={container}>
<PopperTooltip
anchorEl={anchorEl}
direction={direction}
disablePortal={disablePortal}
modifiers={modifiers}
ref={forwardedRef}
open={transition ? !exited : open}
placement={placement}
popperOptions={popperOptions}
popperRef={popperRef}
slotProps={slotProps}
slots={slots}
{...other}
style={{
// Prevents scroll issue, waiting for Popper.js to add this style once initiated.
position: 'fixed',
// Fix Popper.js display issue
top: 0,
left: 0,
display,
...style,
}}
TransitionProps={transitionProps}
>
{children}
</PopperTooltip>
</Portal>
);
}) as PolymorphicComponent<PopperTypeMap>;
export { Popper };
// @mui/base/Portal
'use client';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {
exactProp,
HTMLElementType,
unstable_useEnhancedEffect as useEnhancedEffect,
unstable_useForkRef as useForkRef,
unstable_setRef as setRef,
} from '@mui/utils';
import { PortalProps } from './Portal.types';
function getContainer(container: PortalProps['container']) {
return typeof container === 'function' ? container() : container;
}
/**
* Portals provide a first-class way to render children into a DOM node
* that exists outside the DOM hierarchy of the parent component.
*
* Demos:
*
* - [Portal](https://mui.com/base-ui/react-portal/)
*
* API:
*
* - [Portal API](https://mui.com/base-ui/react-portal/components-api/#portal)
*/
const Portal = React.forwardRef(function Portal(
props: PortalProps,
forwardedRef: React.ForwardedRef<Element>,
) {
const { children, container, disablePortal = false } = props;
const [mountNode, setMountNode] = React.useState<ReturnType<typeof getContainer>>(null);
// @ts-expect-error TODO upstream fix
const handleRef = useForkRef(React.isValidElement(children) ? children.ref : null, forwardedRef);
useEnhancedEffect(() => {
if (!disablePortal) {
setMountNode(getContainer(container) || document.body);
}
}, [container, disablePortal]);
useEnhancedEffect(() => {
if (mountNode && !disablePortal) {
setRef(forwardedRef, mountNode);
return () => {
setRef(forwardedRef, null);
};
}
return undefined;
}, [forwardedRef, mountNode, disablePortal]);
if (disablePortal) {
if (React.isValidElement(children)) {
const newProps = {
ref: handleRef,
};
return React.cloneElement(children, newProps);
}
return <React.Fragment>{children}</React.Fragment>;
}
return (
<React.Fragment>
{mountNode ? ReactDOM.createPortal(children, mountNode) : mountNode}
</React.Fragment>
);
}) as React.ForwardRefExoticComponent<PortalProps & React.RefAttributes<Element>>;
Portal.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* The children to render into the `container`.
*/
children: PropTypes.node,
/**
* An HTML element or function that returns one.
* The `container` will have the portal children appended to it.
*
* You can also provide a callback, which is called in a React layout effect.
* This lets you set the container from a ref, and also makes server-side rendering possible.
*
* By default, it uses the body of the top-level document object,
* so it's simply `document.body` most of the time.
*/
container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
HTMLElementType,
PropTypes.func,
]),
/**
* The `children` will be under the DOM hierarchy of the parent component.
* @default false
*/
disablePortal: PropTypes.bool,
} as any;
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line
(Portal as any)['propTypes' + ''] = exactProp((Portal as any).propTypes);
}
export { Portal };
createPortal을 사용하지 않고, CSS의 position: absolute와 top, left 속성을 사용하여 위치를 지정합니다. 따라서 Popper보다 z-index 관리가 어려울 수 있습니다.아래는 MUI의 Popover 컴포넌트 코드입니다.
https://github.com/mui/material-ui/blob/next/packages/mui-material/src/Popover/Popover.js
// packages/mui-material/src/Popover/Popover.js
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { isHostComponent } from '@mui/base/utils';
import composeClasses from '@mui/utils/composeClasses';
import HTMLElementType from '@mui/utils/HTMLElementType';
import refType from '@mui/utils/refType';
import elementTypeAcceptingRef from '@mui/utils/elementTypeAcceptingRef';
import integerPropType from '@mui/utils/integerPropType';
import chainPropTypes from '@mui/utils/chainPropTypes';
import { styled, createUseThemeProps } from '../zero-styled';
import debounce from '../utils/debounce';
import ownerDocument from '../utils/ownerDocument';
import ownerWindow from '../utils/ownerWindow';
import useForkRef from '../utils/useForkRef';
import Grow from '../Grow';
import Modal from '../Modal';
import PaperBase from '../Paper';
import { getPopoverUtilityClass } from './popoverClasses';
import useSlot from '../utils/useSlot';
const useThemeProps = createUseThemeProps('MuiPopover');
export function getOffsetTop(rect, vertical) {
let offset = 0;
if (typeof vertical === 'number') {
offset = vertical;
} else if (vertical === 'center') {
offset = rect.height / 2;
} else if (vertical === 'bottom') {
offset = rect.height;
}
return offset;
}
export function getOffsetLeft(rect, horizontal) {
let offset = 0;
if (typeof horizontal === 'number') {
offset = horizontal;
} else if (horizontal === 'center') {
offset = rect.width / 2;
} else if (horizontal === 'right') {
offset = rect.width;
}
return offset;
}
function getTransformOriginValue(transformOrigin) {
return [transformOrigin.horizontal, transformOrigin.vertical]
.map((n) => (typeof n === 'number' ? `${n}px` : n))
.join(' ');
}
function resolveAnchorEl(anchorEl) {
return typeof anchorEl === 'function' ? anchorEl() : anchorEl;
}
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
paper: ['paper'],
};
return composeClasses(slots, getPopoverUtilityClass, classes);
};
export const PopoverRoot = styled(Modal, {
name: 'MuiPopover',
slot: 'Root',
overridesResolver: (props, styles) => styles.root,
})({});
export const PopoverPaper = styled(PaperBase, {
name: 'MuiPopover',
slot: 'Paper',
overridesResolver: (props, styles) => styles.paper,
})({
position: 'absolute',
overflowY: 'auto',
overflowX: 'hidden',
// So we see the popover when it's empty.
// It's most likely on issue on userland.
minWidth: 16,
minHeight: 16,
maxWidth: 'calc(100% - 32px)',
maxHeight: 'calc(100% - 32px)',
// We disable the focus ring for mouse, touch and keyboard users.
outline: 0,
});
const Popover = React.forwardRef(function Popover(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiPopover' });
const {
action,
anchorEl,
anchorOrigin = {
vertical: 'top',
horizontal: 'left',
},
anchorPosition,
anchorReference = 'anchorEl',
children,
className,
container: containerProp,
elevation = 8,
marginThreshold = 16,
open,
PaperProps: PaperPropsProp = {},
slots = {},
slotProps = {},
transformOrigin = {
vertical: 'top',
horizontal: 'left',
},
TransitionComponent = Grow,
transitionDuration: transitionDurationProp = 'auto',
TransitionProps: { onEntering, ...TransitionProps } = {},
disableScrollLock = false,
...other
} = props;
const externalPaperSlotProps = slotProps?.paper ?? PaperPropsProp;
const paperRef = React.useRef();
const ownerState = {
...props,
anchorOrigin,
anchorReference,
elevation,
marginThreshold,
externalPaperSlotProps,
transformOrigin,
TransitionComponent,
transitionDuration: transitionDurationProp,
TransitionProps,
};
const classes = useUtilityClasses(ownerState);
// Returns the top/left offset of the position
// to attach to on the anchor element (or body if none is provided)
const getAnchorOffset = React.useCallback(() => {
if (anchorReference === 'anchorPosition') {
if (process.env.NODE_ENV !== 'production') {
if (!anchorPosition) {
console.error(
'MUI: You need to provide a `anchorPosition` prop when using ' +
'<Popover anchorReference="anchorPosition" />.',
);
}
}
return anchorPosition;
}
const resolvedAnchorEl = resolveAnchorEl(anchorEl);
// If an anchor element wasn't provided, just use the parent body element of this Popover
const anchorElement =
resolvedAnchorEl && resolvedAnchorEl.nodeType === 1
? resolvedAnchorEl
: ownerDocument(paperRef.current).body;
const anchorRect = anchorElement.getBoundingClientRect();
if (process.env.NODE_ENV !== 'production') {
const box = anchorElement.getBoundingClientRect();
if (
process.env.NODE_ENV !== 'test' &&
box.top === 0 &&
box.left === 0 &&
box.right === 0 &&
box.bottom === 0
) {
console.warn(
[
'MUI: The `anchorEl` prop provided to the component is invalid.',
'The anchor element should be part of the document layout.',
"Make sure the element is present in the document or that it's not display none.",
].join('\n'),
);
}
}
return {
top: anchorRect.top + getOffsetTop(anchorRect, anchorOrigin.vertical),
left: anchorRect.left + getOffsetLeft(anchorRect, anchorOrigin.horizontal),
};
}, [anchorEl, anchorOrigin.horizontal, anchorOrigin.vertical, anchorPosition, anchorReference]);
// Returns the base transform origin using the element
const getTransformOrigin = React.useCallback(
(elemRect) => {
return {
vertical: getOffsetTop(elemRect, transformOrigin.vertical),
horizontal: getOffsetLeft(elemRect, transformOrigin.horizontal),
};
},
[transformOrigin.horizontal, transformOrigin.vertical],
);
const getPositioningStyle = React.useCallback(
(element) => {
const elemRect = {
width: element.offsetWidth,
height: element.offsetHeight,
};
// Get the transform origin point on the element itself
const elemTransformOrigin = getTransformOrigin(elemRect);
if (anchorReference === 'none') {
return {
top: null,
left: null,
transformOrigin: getTransformOriginValue(elemTransformOrigin),
};
}
// Get the offset of the anchoring element
const anchorOffset = getAnchorOffset();
// Calculate element positioning
let top = anchorOffset.top - elemTransformOrigin.vertical;
let left = anchorOffset.left - elemTransformOrigin.horizontal;
const bottom = top + elemRect.height;
const right = left + elemRect.width;
// Use the parent window of the anchorEl if provided
const containerWindow = ownerWindow(resolveAnchorEl(anchorEl));
// Window thresholds taking required margin into account
const heightThreshold = containerWindow.innerHeight - marginThreshold;
const widthThreshold = containerWindow.innerWidth - marginThreshold;
// Check if the vertical axis needs shifting
if (marginThreshold !== null && top < marginThreshold) {
const diff = top - marginThreshold;
top -= diff;
elemTransformOrigin.vertical += diff;
} else if (marginThreshold !== null && bottom > heightThreshold) {
const diff = bottom - heightThreshold;
top -= diff;
elemTransformOrigin.vertical += diff;
}
if (process.env.NODE_ENV !== 'production') {
if (elemRect.height > heightThreshold && elemRect.height && heightThreshold) {
console.error(
[
'MUI: The popover component is too tall.',
`Some part of it can not be seen on the screen (${
elemRect.height - heightThreshold
}px).`,
'Please consider adding a `max-height` to improve the user-experience.',
].join('\n'),
);
}
}
// Check if the horizontal axis needs shifting
if (marginThreshold !== null && left < marginThreshold) {
const diff = left - marginThreshold;
left -= diff;
elemTransformOrigin.horizontal += diff;
} else if (right > widthThreshold) {
const diff = right - widthThreshold;
left -= diff;
elemTransformOrigin.horizontal += diff;
}
return {
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`,
transformOrigin: getTransformOriginValue(elemTransformOrigin),
};
},
[anchorEl, anchorReference, getAnchorOffset, getTransformOrigin, marginThreshold],
);
const [isPositioned, setIsPositioned] = React.useState(open);
const setPositioningStyles = React.useCallback(() => {
const element = paperRef.current;
if (!element) {
return;
}
const positioning = getPositioningStyle(element);
if (positioning.top !== null) {
element.style.top = positioning.top;
}
if (positioning.left !== null) {
element.style.left = positioning.left;
}
element.style.transformOrigin = positioning.transformOrigin;
setIsPositioned(true);
}, [getPositioningStyle]);
React.useEffect(() => {
if (disableScrollLock) {
window.addEventListener('scroll', setPositioningStyles);
}
return () => window.removeEventListener('scroll', setPositioningStyles);
}, [anchorEl, disableScrollLock, setPositioningStyles]);
const handleEntering = (element, isAppearing) => {
if (onEntering) {
onEntering(element, isAppearing);
}
setPositioningStyles();
};
const handleExited = () => {
setIsPositioned(false);
};
React.useEffect(() => {
if (open) {
setPositioningStyles();
}
});
React.useImperativeHandle(
action,
() =>
open
? {
updatePosition: () => {
setPositioningStyles();
},
}
: null,
[open, setPositioningStyles],
);
React.useEffect(() => {
if (!open) {
return undefined;
}
const handleResize = debounce(() => {
setPositioningStyles();
});
const containerWindow = ownerWindow(anchorEl);
containerWindow.addEventListener('resize', handleResize);
return () => {
handleResize.clear();
containerWindow.removeEventListener('resize', handleResize);
};
}, [anchorEl, open, setPositioningStyles]);
let transitionDuration = transitionDurationProp;
if (transitionDurationProp === 'auto' && !TransitionComponent.muiSupportAuto) {
transitionDuration = undefined;
}
// If the container prop is provided, use that
// If the anchorEl prop is provided, use its parent body element as the container
// If neither are provided let the Modal take care of choosing the container
const container =
containerProp || (anchorEl ? ownerDocument(resolveAnchorEl(anchorEl)).body : undefined);
const externalForwardedProps = {
slots,
slotProps: {
...slotProps,
paper: externalPaperSlotProps,
},
};
const [PaperSlot, paperProps] = useSlot('paper', {
elementType: PopoverPaper,
externalForwardedProps,
additionalProps: {
elevation,
className: clsx(classes.paper, externalPaperSlotProps?.className),
style: isPositioned
? externalPaperSlotProps.style
: { ...externalPaperSlotProps.style, opacity: 0 },
},
ownerState,
});
const [RootSlot, { slotProps: rootSlotPropsProp, ...rootProps }] = useSlot('root', {
elementType: PopoverRoot,
externalForwardedProps,
additionalProps: {
slotProps: { backdrop: { invisible: true } },
container,
open,
},
ownerState,
className: clsx(classes.root, className),
});
const handlePaperRef = useForkRef(paperRef, paperProps.ref);
return (
<RootSlot
{...rootProps}
{...(!isHostComponent(RootSlot) && { slotProps: rootSlotPropsProp, disableScrollLock })}
{...other}
ref={ref}
>
<TransitionComponent
appear
in={open}
onEntering={handleEntering}
onExited={handleExited}
timeout={transitionDuration}
{...TransitionProps}
>
<PaperSlot {...paperProps} ref={handlePaperRef}>
{children}
</PaperSlot>
</TransitionComponent>
</RootSlot>
);
});
export default Popover;
position: absolute와 top, left 값을 사용하여 위치를 지정합니다. 상대적으로 단순한 위치 지정을 지원합니다.createPortal을 사용하여 DOM 계층 구조 외부에 렌더링됩니다. 이를 통해 이벤트 버블링 문제를 해결하고, z-index 관리를 쉽게 할 수 있습니다.createPortal을 사용하지 않고, CSS 위치 지정을 사용하여 현재 DOM 계층 구조 내에 렌더링됩니다.// packages/mui-material/src/Popover/Popover.js
export const PopoverRoot = styled(Modal, {
name: 'MuiPopover',
slot: 'Root',
overridesResolver: (props, styles) => styles.root,
})({});MUI의 Popper와 Popover는 각각 다른 장점과 단점을 가지고 있으며, 특정 요구 사항에 따라 적절한 컴포넌트를 선택하여 사용할 수 있습니다. 복잡한 위치 지정이 필요하고 z-index 관리가 중요한 경우에는 Popper를 사용하는 것이 좋습니다. 반면, 단순한 팝업이나 메뉴를 구현할 때는 Popover를 사용하는 것이 더 간편할 수 있습니다. 각 컴포넌트의 특성과 사용 사례를 잘 이해하고, 프로젝트에 맞는 최적의 선택을 하시길 바랍니다.