Prologue: 픽셀 계산과의 끝없는 전쟁
프론트엔드 개발을 하면서 가장 다루기 짜증 나는 컴포넌트를 꼽으라면 저는 단연코 툴팁(Tooltip), 드롭다운(Dropdown), 팝오버(Popover) 같은 '부유형(Floating) UI'를 선택할 것입니다.
사학과 시절 고문서를 읽다가 모르는 단어 위에 주석을 덧붙이는 작업처럼, 화면의 특정 요소(버튼 등) 바로 옆에 찰떡같이 붙어서 부가 정보를 띄워주는 일은 단순해 보이지만 코딩을 하면 온갖 버그의 온상이 되기 십상이었습니다.
기존에 이 위치를 맞추기 위해 겪었던 삽질은 대략 이랬습니다.
position: absolute로 위치 잡기: 하지만 부모 요소 중에overflow: hidden이 걸려 있으면 툴팁이 뚝 잘려 나가는 문제가 생겨서 포기.position: fixed로 띄우고 자바스크립트의getBoundingClientRect()로 버튼의 절대 좌표를 구해 수동으로top,left할당하기: 스크롤을 내릴 때마다 툴팁이 버튼을 따라가지 못하고 둥둥 떠다니거나 레이아웃 리플로우(Reflow) 지연 때문에 화면이 버벅거림.- 결국
Popper.js나Floating UI같은 무거운 자바스크립트 외부 라이브러리를 설치해 해결하기.
라이브러리를 쓰면 잘 해결되지만, 고작 말풍선 하나 띄우겠다고 수십 킬로바이트짜리 자바스크립트 번들을 추가하고 복잡한 리액트 훅을 감싸야 하는 현실이 늘 찜찜했습니다.
그러다 크롬 등 모던 브라우저에 네이티브 표준으로 탑재되기 시작한 CSS Anchor Positioning(앵커 포지셔닝) API를 보고, 드디어 자바스크립트의 도움 없이 순수 CSS만으로 이 영원한 골칫거리를 우아하게 풀 수 있겠다는 확신이 들었습니다.
Concept: 자바스크립트 없이 앵커를 묶는다
CSS 앵커 포지셔닝의 핵심은 **"브라우저가 레이아웃 엔진 레벨에서 특정 두 요소의 기하학적 관계를 인지하고 연결해 주는 기능"**입니다.
기존에는 앵커(기준점이 되는 버튼)와 대상(띄워지는 툴팁)이 DOM 트리상에서 완전히 떨어져 있으면 서로의 위치를 CSS만으로는 알 길이 없었습니다. 하지만 앵커 포지셔닝을 사용하면 CSS 속성만으로 두 요소를 논리적으로 묶어버릴 수 있습니다.
/* 1. 기준이 되는 요소에 이름표 붙이기 */
.anchor-button {
anchor-name: --my-anchor;
}
/* 2. 띄울 요소를 기준 요소 옆에 고정하기 */
.tooltip-popover {
position: absolute;
position-anchor: --my-anchor;
top: anchor(bottom); /* 앵커의 아래쪽에 딱 붙임 */
left: anchor(center); /* 앵커의 수평 중앙에 맞춤 */
}
이 코드를 처음 작성하고 테스트해 보았을 때의 해방감은 대단했습니다.
- 자바스크립트 리스너가 필요 없습니다.
- 화면 스크롤이나 브라우저 리사이징이 일어나도 브라우저가 알아서 렌더링 프레임 단위로 초고속으로 위치를 보정해 줍니다.
- 부모의
overflow: hidden에 의해 잘리는 문제도 겪지 않습니다.
Deep Dive: 네이티브 앵커 포지셔닝의 핵심 스펙
실무에 앵커 포지셔닝을 도입하기 위해 알아야 할 세 가지 핵심 문법과 기능을 짚어보았습니다.
1. anchor() 함수를 통한 정밀한 매핑
anchor() 함수는 기준 요소의 좌표(top, bottom, left, right, center 등)를 반환하는 특수 함수입니다. 이를 위치 지정 속성에 대입하여 툴팁의 좌표를 정교하게 결정합니다.
.dropdown-menu {
position: absolute;
position-anchor: --menu-trigger;
/* 드롭다운의 윗변을 트리거 버튼의 아랫변에 일치 */
top: anchor(bottom);
/* 드롭다운의 왼쪽을 트리거 버튼의 왼쪽선에 일치 */
left: anchor(left);
}
또한 퍼센트(%) 연산도 가능하여 anchor(left 20%)와 같이 미세한 마진이나 오프셋을 줄 수도 있습니다.
2. 공간 부족 시 자동 반전: position-try-options
부유형 UI의 가장 구현하기 까다로운 영역은 '화면 경계선 처리'입니다. 툴팁을 버튼 아래에 띄우도록 설정했는데, 버튼이 화면 가장 하단에 배치되어 있다면 툴팁은 화면 아래로 삐져나가 안 보이게 됩니다.
기존에는 Popper.js가 화면 공간을 계산해서 자동으로 위쪽으로 툴팁을 튕겨주었습니다. CSS에서는 이를 position-try-options 속성으로 네이티브 지원합니다.
.tooltip {
position: absolute;
position-anchor: --tooltip-trigger;
top: anchor(bottom);
left: anchor(center);
/* 아래쪽에 공간이 없으면 위쪽(flip-block)에 띄우도록 자동 감지 시도 */
position-try-options: flip-block;
}
flip-block은 Y축 반전(아래 <-> 위), flip-inline은 X축 반전(오른쪽 <-> 왼쪽)을 뜻합니다. 브라우저가 레이아웃을 그릴 때 여유 공간을 미리 계산해 찰나의 순간에 뷰포트 내 최적의 위치로 전환해 줍니다.
3. 모던 Popover API <div popover> 와의 궁합
앵커 포지셔닝은 HTML5 표준 스펙인 Popover API(popover 속성)와 결합할 때 폭발적인 시너지를 냅니다. Popover API를 사용하면 자바스크립트의 상태 없이도 최상위 레이어(Top Layer)에 드롭다운을 띄워 z-index 싸움을 영원히 끝낼 수 있습니다.
<!-- HTML 구조 -->
<button id="my-btn" class="trigger">메뉴 열기</button>
<div id="my-menu" popover class="menu-content">
<ul>
<li>설정</li>
<li>로그아웃</li>
</ul>
</div>
/* CSS 매칭 */
.trigger {
anchor-name: --my-btn-anchor;
}
.menu-content {
/* popover가 열릴 때 자동으로 최상위 레이어에 뜨고, 위치는 앵커를 기준으로 고정 */
position-anchor: --my-btn-anchor;
top: anchor(bottom);
left: anchor(left);
margin: 0; /* popover 기본 마진 제거 */
}
Application: 리액트 팝오버 컴포넌트 리팩토링
이 강력한 스펙을 활용해 내 프로젝트의 지저분한 '사용자 프로필 메뉴 드롭다운' 컴포넌트를 리팩토링했습니다.
기존 코드는 useRef로 버튼 엘리먼트를 잡고, useEffect에서 좌표 계산 이벤트 리스너를 달아 스크롤 속도를 갉아먹는 대표적인 원인이었습니다.
// 리팩토링 후 순수 CSS 기반 팝오버 컴포넌트
import { useId } from 'react';
function UserProfileDropdown() {
const uniqueId = useId();
// 앵커 이름을 고유하게 만들기 위해 리액트 ID 활용
const anchorName = `--profile-btn-${uniqueId.replace(/:/g, '')}`;
return (
<div className="relative">
{/* 앵커 트리거 버튼 */}
<button
popovertarget="profile-menu"
style={{ anchorName } as React.CSSProperties}
className="w-10 h-10 rounded-full bg-slate-200"
>
👤
</button>
{/* 팝오버 메뉴 */}
<div
id="profile-menu"
popover="auto"
style={{ positionAnchor: anchorName } as React.CSSProperties}
className="absolute p-4 bg-white rounded-lg shadow-xl border border-slate-100 hidden-popover"
>
<p className="font-bold text-slate-800">김코딩 님</p>
<hr className="my-2 border-slate-100" />
<button className="text-sm text-red-500">로그아웃</button>
</div>
</div>
);
}
/* 전역 CSS 파일 추가 설정 */
[popover] {
border: none;
padding: 0;
overflow: visible;
/* 앵커 위치 설정 */
top: anchor(bottom);
left: anchor(right);
transform: translate(-100%, 8px); /* 우측 정렬 후 아래 마진 */
position-try-options: flip-block;
}
리팩토링의 결과는 놀라웠습니다.
- 드롭다운 상태를 여닫기 위한 리액트의
isOpen상태(useState)가 완전히 사라졌습니다. 브라우저의 네이티브popover속성이 화면의 클릭 여부를 추적해 알아서 여닫아 줍니다. - 자바스크립트로 계산하던 오프셋 좌표 로직이 통째로 날아갔습니다.
- 스크롤을 마구 내려도 브라우저가 GPU 가속을 통해 한 치의 오차도 없이 버튼 바로 아래에 드롭다운 메뉴를 밀착시켜 보여주어 화면 성능이 눈에 띄게 부드러워졌습니다.
Summary: 브라우저가 똑똑해질 때의 축복
소프트웨어 공학의 발전 역사는 언제나 자바스크립트나 애플리케이션 영역에서 끙끙 앓으며 처리하던 복잡한 작업들을 저수준 플랫폼(브라우저 엔진) 영역으로 이전시켜 오는 여정이었습니다. 그리드(Grid) 레이아웃이 도입되면서 복잡한 플로트(Float) 연산이 사라졌던 것처럼 말입니다.
CSS 앵커 포지셔닝과 Popover API의 결합은 드롭다운과 모달 디자인의 종착역입니다.
자바스크립트 번들 용량을 단 1바이트도 쓰지 않고, 단 한 줄의 이벤트 리스너도 등록하지 않은 채, 오직 선언적인 HTML 마크업과 CSS만으로 최상위 레이어 팝오버를 제어할 수 있다는 사실은 프론트엔드 개발의 복잡성을 믿을 수 없을 만큼 걷어내 줍니다.
아직 구형 브라우저 대응을 위해 폴리필(Polyfill) 검토가 필요한 영역도 있지만, 크롬과 엣지, 오페라 등 주요 브라우저에서 기본 동작하는 이 스펙은 우리가 UI를 구현할 때 겪는 불필요한 고통을 덜어주는 현대 웹 표준의 가장 아름다운 선물 중 하나임이 분명합니다.