대시보드 화면에서 지도를 렌더링하고, 그 위에 여러 위젯을 iframe 형태로 배치하는 구조를 작업한 적이 있다. 지도 위에는 OpenLayers Overlay를 사용해 마커 클릭 시 팝업을 띄우고 있었다.
처음에는 단순한 레이어 문제라고 생각했다. 팝업이 위젯 아래로 가려졌기 때문에 z-index를 조정하면 해결될 것이라고 생각했지만 실제 구조를 확인해보니 그 문제가 아닌걸 알 수 있었다.
대시보드 상위 페이지에 지도를 렌더링하고, 그 위에 iframe 형태의 위젯 UI가 함께 배치된 구조였다.
지도 위에는 OpenLayers Overlay를 사용해 마커 클릭 시 팝업을 띄우고 있었다. 그런데 위젯을 접거나 펼칠 때 iframe 상단 타이틀 영역이 레이아웃을 차지하면서, 지도 Overlay 팝업이 iframe 아래로 가려지는 현상이 발생했다.
대시보드 상위 페이지
├─ 지도 영역
│ └─ OpenLayers Overlay 팝업
└─ iframe 위젯 영역
└─ 위젯 UI
지도 자체는 정상적으로 렌더링되고 있었지만, 팝업 일부가 보이지 않거나 클릭할 수 없는 상태가 되었다.
구조를 뜯어보면 지도와 Overlay는 대시보드 상위 문서의 DOM 안에서 OpenLayers 기준으로 렌더링되고 있었다. 반면 위젯은 지도 위에 겹쳐진 iframe 레이어이다.
지도 + Overlay
→ 대시보드 DOM 안에서 OpenLayers 기준으로 위치 계산
iframe 위젯
→ 지도 위에 겹쳐진 별도의 레이어
즉, 지도 Overlay는 지도 좌표 기준으로는 정상 위치에 있었다. 하지만 iframe은 화면 상단 기준으로 레이아웃을 차지하고 있었고, 이 둘의 표시 기준이 달라 Overlay가 iframe 아래로 가려지는 문제가 발생했다.
기존에는 마커를 클릭하면 팝업 DOM을 생성하고, Vue 컴포넌트를 마운트한 뒤 OpenLayers Overlay로 지도에 추가하는 방식이었다.
/**
* 클릭된 장비 마커에 팝업을 표시합니다.
* - 동일 장비(eqpmSn)에 대해 중복 팝업을 방지합니다.
* @param {IEquipment} item - 클릭된 장비 정보
* @param {[number, number]} coordinate - 팝업 표시 좌표
*/
function showClickPopup(item: IEquipment, coordinate: [number, number]) {
// 이미 열린 팝업이면 중복 생성 방지
if (clickPopupOverlays.has(item.eqpmtSn)) return;
const map = imgMapRef.value?.getImgMapInstance();
if (!map) return;
const el = document.createElement('div');
el.className = 'marker-popup-container click-popup';
const app = createApp(PopupContent, {
data: item,
onClose: () => clearClickPopup(item.eqpmtSn)
});
app.mount(el);
const overlay = new Overlay({
element: el,
positioning: 'center-center',
stopEvent: true,
offset: [0, 0],
position: coordinate
});
map.addOverlay(overlay);
makeOverlayDraggable(overlay, map);
clickPopupOverlays.set(item.eqpmtSn, overlay);
clickPopupApps.set(item.eqpmtSn, app);
}
이 구조에서 팝업 DOM은 Vue로 생성했지만, 최종적으로는 OpenLayers Overlay로 지도에 추가된다.
즉, Overlay는 항상 OpenLayers가 생성한 overlay container 안에서 렌더링된다.
처음에는 이 문제를 레이어 문제로 판단했다. 그래서 Overlay UI를 지도 컨테이너 밖으로 빼면 iframe 레이아웃의 영향을 덜 받지 않을까 생각했다.
이때 검토했던 방법이 Vue의 Teleport였다.
Overlay 내부의 Vue 컴포넌트를 body 같은 다른 DOM 위치로 이동시키면, iframe 아래로 가려지는 문제를 피할 수 있지 않을까 생각했다.
Vue의 Teleport는 컴포넌트를 현재 컴포넌트 트리 위치와 상관없이 지정한 다른 DOM 위치로 렌더링할 수 있게 해주는 기능이다.
대표적인 사용 예시는 다음과 같다.
부모 컴포넌트의 overflow, z-index, stacking context 영향을 피하고 싶을 때 자주 사용한다.
<Teleport to="body">
<Modal />
</Teleport>
Teleport는 Vue 컴포넌트의 DOM 위치만 이동시킬 뿐, 브라우저 렌더링 컨텍스트나 외부 라이브러리의 좌표 계산 방식까지 바꾸지는 않는다.
OpenLayers Overlay는 지도 컨테이너를 기준으로 좌표를 계산하고 렌더링한다. 반면 Teleport는 Vue 컴포넌트가 어느 DOM 위치에 그려질지만 바꾼다.
Teleport
→ Vue 컴포넌트 DOM 위치 이동
OpenLayers Overlay
→ 지도 좌표와 Overlay container 기준으로 위치 계산
즉, 두 기능은 역할과 책임 레벨이 다르다.
Teleport를 사용하면 DOM 위치는 이동시킬 수 있지만, OpenLayers가 Overlay를 지도 좌표 기준으로 관리한다는 사실은 변하지 않는다. 결과적으로 Teleport를 사용해도 Overlay가 가지는 근본적인 구조는 바뀌지 않았다.
그래서 이 방식은 문제를 해결하기보다는 화면에 보이는 DOM만 우회해서 옮기려는 시도에 가깝다고 판단했다.
Overlay를 지도 컨테이너 밖으로 억지로 옮기기보다 Overlay가 표시되는 기준을 조정하는 방향으로 접근을 바꿨다.
Overlay가 iframe 상단 영역과 겹치는 위치에 표시되면서 가려지는 것이었기 때문에 팝업을 띄울 때 현재 위젯이 접혀 있는지, 펼쳐져 있는지에 따라 iframe이 차지하는 영역을 확인하고 Overlay가 해당 영역과 겹치지 않도록 표시 위치를 조정했다.
기존에는 마커 좌표를 그대로 Overlay 위치로 사용했다.
const overlay = new Overlay({
element: el,
positioning: 'center-center',
stopEvent: true,
offset: [0, 0],
position: coordinate
});
하지만 iframe 타이틀 영역이나 위젯 영역이 지도 위를 덮는 경우에는 팝업이 그 영역 안으로 들어가지 않도록 offset 또는 표시 기준을 조정해야 했다.
예를 들어 팝업이 위젯 상단 영역과 겹칠 수 있는 경우에는 Overlay의 위치를 그대로 사용하지 않고, 위젯이 차지하는 높이만큼 화면 안쪽으로 내려주거나, 팝업이 보이는 방향으로 offset을 조정하는 식으로 처리했다.
const overlay = new Overlay({
element: el,
positioning: 'bottom-center',
stopEvent: true,
offset: [0, adjustedOffsetY],
position: coordinate
});
OpenLayers Overlay는 지도 좌표와 지도 컨테이너를 기준으로 동작iframe은 별도의 문서와 레이어 컨텍스트를 가짐 Teleport는 Vue 컴포넌트의 DOM 위치를 이동시키는 도구