728x90
목차
Popper 컴포넌트 소개
Material UI의 Popper 컴포넌트는 @popperjs/core 라이브러리를 기반으로 구현된 컴포넌트로, 툴팁, 드롭다운, 메뉴 등 다양한 팝오버 UI 요소를 화면에 표시하는 데 사용됩니다. Popper는 포털(Portal)을 통해 DOM의 계층 구조와 독립적으로 렌더링되며, 지정된 앵커 요소(anchorEl)를 기준으로 위치가 계산됩니다.
import { Popper } from '@mui/material';
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-end"
transition
disablePortal={false}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper>Popper 내용</Paper>
</Fade>
)}
</Popper>
버그 케이스별 대응 기록
위치 설정 오류
문제 상황:
Popper가 초기에 좌측 상단에 렌더링된 후 우측 하단으로 이동하는 현상
원인 분석:
requestAnimationFrame
타이밍 문제로 인한 초기 렌더링과 위치 계산 사이의 지연- 가상 요소(virtualElement) 설정이 프레임 간격으로 지연되어 실행됨
- 상태 업데이트(
setAnchorEl
) 후 리렌더링까지의 시간차로 인한 깜빡임 현상
해결 방안:
// 문제가 있는 코드
useLayoutEffect(() => {
if (progressOpen) {
requestAnimationFrame(() => {
const x = window.innerWidth - 10;
const y = window.innerHeight - 10;
const virtualElement = {
getBoundingClientRect: () => ({
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON: () => {},
}),
};
setAnchorEl(virtualElement as any);
});
} else {
setAnchorEl(null);
}
}, [progressOpen, location.pathname]);
// 개선된 코드
useLayoutEffect(() => {
if (progressOpen) {
// virtualElement를 즉시 설정
const x = window.innerWidth - 10;
const y = window.innerHeight - 10;
const virtualElement = {
getBoundingClientRect: () => ({
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON: () => {},
}),
};
// 즉시 anchorEl 설정
setAnchorEl(virtualElement as any);
// 레이아웃 재계산 강제(필요한 경우)
document.body.offsetHeight;
} else {
setAnchorEl(null);
}
}, [progressOpen, location.pathname]);
추가 대응:
1. Popper 컴포넌트에 명시적 배치 설정 추가:
<Popper
open={progressOpen}
anchorEl={anchorEl}
placement="bottom-end" // 우측 하단 배치 강제
modifiers={[
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
},
},
]}
>
{/* 내용 */}
</Popper>
2. useLayoutEffect
사용으로 DOM 업데이트 전 위치 계산 보장
3. 초기 위치를 컴포넌트 마운트 시점에 계산하도록 설정
렌더링 타이밍 이슈
문제 상황:
Popper가 열릴 때 내용이 완전히 로드되기 전에 위치가 계산되어 잘못된 위치에 표시되는 현상
원인 분석:
- Popper 내부 컨텐츠의 크기가 비동기적으로 변경됨
- 초기 위치 계산 시 최종 컨텐츠 크기가 반영되지 않음
- 리플로우/리페인트 타이밍 문제
해결 방안:
// 문제 있는 접근법
const [open, setOpen] = useState(false);
const [content, setContent] = useState(null);
useEffect(() => {
setOpen(true);
fetchContent().then(data => {
setContent(data);
});
}, []);
// 개선된 접근법
const [open, setOpen] = useState(false);
const [content, setContent] = useState(null);
const [isContentLoaded, setIsContentLoaded] = useState(false);
useEffect(() => {
fetchContent().then(data => {
setContent(data);
setIsContentLoaded(true);
});
}, []);
useEffect(() => {
if (isContentLoaded) {
setOpen(true);
}
}, [isContentLoaded]);
// 컴포넌트에 업데이트 훅 추가
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-end"
modifiers={[
{
name: 'updateOnContentChange',
enabled: true,
phase: 'afterWrite',
fn: ({ state }) => {
// 내용 변경 후 위치 업데이트 로직
},
},
]}
>
{content}
</Popper>
리사이징 및 스크롤 문제
문제 상황:
윈도우 리사이징이나 스크롤 시 Popper 위치가 앵커 요소를 따라가지 않는 문제
원인 분석:
- 윈도우 이벤트(resize, scroll)에 대한 대응 부재
- 가상 요소 사용 시 위치 업데이트 로직 누락
- 포지셔닝 업데이트 최적화 부족
해결 방안:
useLayoutEffect(() => {
if (!progressOpen) {
setAnchorEl(null);
return;
}
// 초기 위치 설정
const updatePosition = () => {
const x = window.innerWidth - 10;
const y = window.innerHeight - 10;
const virtualElement = {
getBoundingClientRect: () => ({
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON: () => {},
}),
};
setAnchorEl(virtualElement as any);
};
// 초기 위치 설정
updatePosition();
// 이벤트 리스너 등록
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true); // capture phase
// 클린업
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}, [progressOpen]);
최적화 방안:
- 디바운스/스로틀 적용으로 과도한 업데이트 방지
import { debounce } from 'lodash';
// 디바운스 적용된 위치 업데이트 함수
const debouncedUpdatePosition = useCallback(
debounce(() => {
// 위치 업데이트 로직
updatePosition();
}, 16), // 약 60fps에 맞춤
[]
);
z-index 및 오버레이 이슈
문제 상황:
다른 고정 위치(fixed) 요소나 오버레이 뒤에 Popper가 가려지는 현상
원인 분석:
- z-index 값이 충분히 높지 않음
- 스택 컨텍스트(stacking context) 문제
- Portal 사용 시 DOM 구조상 위치 문제
해결 방안:
// Popper 스타일 조정
<Popper
open={progressOpen}
anchorEl={anchorEl}
style={{ zIndex: 1500 }} // Material UI 기본값보다 높게 설정
// 또는
sx={{ zIndex: theme => theme.zIndex.modal + 1 }}
>
{/* 내용 */}
</Popper>
// Portal 타겟 설정으로 DOM 최상위에 렌더링
<Popper
open={progressOpen}
anchorEl={anchorEl}
container={document.body} // body에 직접 렌더링
>
{/* 내용 */}
</Popper>
Popper 동작 구조 도식화
렌더링 프로세스
+--------------------------+
| 컴포넌트 마운트 |
+--------------------------+
|
v
+--------------------------+
| anchorEl 유효성 확인 |
| (null이면 렌더링 중단) |
+--------------------------+
|
v
+--------------------------+
| Portal 렌더링 준비 |
| (disablePortal 옵션 확인)|
+--------------------------+
|
v
+--------------------------+
| Popper.js 인스턴스 생성 |
| 및 초기 위치 계산 |
+--------------------------+
|
v
+--------------------------+
| DOM에 컨텐츠 렌더링 |
| (Portal 통해 분리) |
+--------------------------+
|
v
+--------------------------+
| 위치 조정 및 적용 |
| (modifiers 실행) |
+--------------------------+
|
v
+--------------------------+
| 이벤트 리스너 등록 |
| (리사이즈, 스크롤 등) |
+--------------------------+
|
v
+--------------------------+
| 변화 감지 및 업데이트 |
| (위치, 사이즈 변경 시) |
+--------------------------+
포지셔닝 메커니즘
Popper는 placement
속성을 통해 기본 배치 위치를 설정하고, 다양한 modifiers
를 통해 위치를 세밀하게 조정합니다.
기본 배치 옵션:
top
,bottom
,left
,right
(기본 방향)-start
,-end
접미사 추가 가능 (예:top-start
,bottom-end
)
주요 Modifiers:
- preventOverflow: 뷰포트 밖으로 넘어가지 않도록 방지
- flip: 공간이 부족할 경우 반대쪽으로 위치 변경
- arrow: 화살표 요소 위치 조정
- offset: 기본 위치에서 오프셋 적용
<Popper
placement="bottom-end"
modifiers={[
{
name: 'offset',
options: {
offset: [0, 10], // [skidding, distance]
},
},
{
name: 'flip',
options: {
fallbackPlacements: ['top-end', 'left-end'],
},
},
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 8,
},
},
]}
>
{/* 내용 */}
</Popper>
가상 요소 활용
실제 DOM 요소 대신 가상 요소를 사용하여 Popper 배치 방법:
+-------------------------+
| 가상 요소 객체 생성 |
+-------------------------+
|
v
+-------------------------+
| getBoundingClientRect() |
| 메서드 구현 |
+-------------------------+
|
v
+-------------------------+
| 위치 좌표 계산 및 |
| 오브젝트 반환 |
+-------------------------+
|
v
+-------------------------+
| 가상 요소를 anchorEl로 |
| Popper에 전달 |
+-------------------------+
|
v
+-------------------------+
| Popper가 가상 요소의 |
| 위치 기준으로 배치 |
+-------------------------+
가상 요소 구현 예시:
// 화면 우측 하단에 배치하는 가상 요소
const virtualElement = {
getBoundingClientRect: () => {
const x = window.innerWidth - 10;
const y = window.innerHeight - 10;
return {
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON: () => {}, // Popper.js가 필요로 함
};
},
};
최적화 전략
성능 최적화
1. 메모이제이션 활용:
// 가상 요소 메모이제이션
const virtualElement = useMemo(() => ({
getBoundingClientRect: () => ({
// 위치 계산
}),
}), [dependencies]);
2. 업데이트 최적화:
// 위치 업데이트 함수 최적화
const updatePosition = useCallback(() => {
// 위치 업데이트 로직
}, [dependencies]);
// 이벤트 핸들러에 스로틀 적용
const throttledUpdatePosition = throttle(updatePosition, 16);
안정성 향상
1. 조건부 렌더링 처리:
// anchorEl 유효성 검사
{anchorEl && (
<Popper
open={open}
anchorEl={anchorEl}
// 기타 속성
>
{/* 내용 */}
</Popper>
)}
2. 오류 처리 및 폴백:
// 위치 계산 중 오류 처리
const getPosition = () => {
try {
// 위치 계산 로직
return { x, y };
} catch (error) {
console.error('위치 계산 오류:', error);
// 기본 위치 반환 (폴백)
return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
}
};
접근성 개선
1. 키보드 네비게이션 지원:
<Popper
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => {
if (e.key === 'Escape') {
handleClose();
}
}}
>
{/* 내용 */}
</Popper>
2. 포커스 관리:
const popperRef = useRef(null);
useEffect(() => {
if (open && popperRef.current) {
// Popper 열릴 때 포커스 이동
popperRef.current.focus();
}
}, [open]);
<Popper
ref={popperRef}
// 기타 속성
>
{/* 내용 */}
</Popper>
728x90
댓글