CSS display 한 줄이 hit-test를 깨뜨린 이야기
웹 개발을 하다 보면 사용자가 클릭한 좌표에서 어떤 DOM 노드를 클릭했는지 정확히 알아야 할 때가 있습니다. 에디터, 커스텀 드래그 앤 드롭, 좌표 기반 인터랙션 등 다양한 상황에서 필요한데요.
브라우저는 caretRangeFromPoint, elementFromPoint 같은 API로 좌표 기반 노드 탐색을 지원합니다. 대부분은 잘 동작하지만, DOM이 여러 겹 중첩되어 있을 때 부모 요소의 display 속성 하나가 결과를 완전히 바꿔버리는 경우가 있었습니다.
분명 안쪽 노드를 클릭했는데 부모 노드가 반환되는 상황. 원인은 CSS 한 줄이었습니다.
브라우저의 hit-test 동작 방식
caretRangeFromPoint(x, y)를 예로 들어보겠습니다. 이 API는 내부적으로 두 단계를 거칩니다.
1단계 — hitTest
주어진 (x, y) 좌표에서 가장 가까운 노드를 찾습니다. 이 단계에서는 대부분 기대한 대로 동작합니다.
2단계 — 정규화
hitTest 결과를 기반으로, 해당 노드가 속한 block 컨테이너 안에서 inline element를 찾아 최종 결과를 결정합니다.
핵심은 이 정규화 단계입니다. 브라우저는 "이 노드가 어떤 block 안에 있는가"를 판단하고, 그 block 내부의 inline 요소 중 적절한 것을 골라 반환하는데요. 바로 여기서 문제가 발생했습니다.
문제 — display 속성이 hit-test 결과를 바꾼다
다음과 같은 중첩 구조를 생각해보겠습니다.
<div class="container">
<div class="wrapper">
<span>Hello World</span>
</div>
</div>wrapper가 display: block일 때
<div class="container">
<div class="wrapper" style="display: block;">
<span>Hello World</span>
</div>
</div>hitTest 결과: <span> 안의 노드
정규화: wrapper(block) 안의 inline element → <span> 반환
브라우저는 wrapper를 block 컨테이너로 인식하고, 그 안쪽의 <span>을 정상적으로 반환합니다.
wrapper가 display: inline-block일 때
<div class="container">
<div class="wrapper" style="display: inline-block;">
<span>Hello World</span>
</div>
</div>hitTest 결과: <span> 안의 노드
정규화: container(block) 안의 inline element → wrapper 반환
wrapper가 inline-block이 되면 정규화의 기준이 달라집니다. container가 block 컨테이너가 되고, 그 안의 inline element를 찾으면 wrapper 자체가 inline 레벨이므로 wrapper가 반환됩니다. 안쪽 <span>까지 도달하지 못하는 거죠.
CSS 한 줄 바꿨을 뿐인데 API의 반환값이 완전히 달라졌습니다.
해결 — position으로 레이아웃을 유지하면서 block 복원
그렇다면 왜 inline-block을 썼을까요? 대부분 위치 배치 때문입니다. 여러 요소를 나란히 놓거나 특정 위치에 배치하기 위해 inline-block을 쓰는 경우가 많은데요.
해결 방법은 display를 건드리지 않고 position으로 위치를 잡는 것이었습니다.
/* Before — hit-test 깨짐 */
.wrapper {
display: inline-block;
}
/* After — hit-test 정상 */
.wrapper {
/* display: block (기본값) */
position: absolute;
left: 0px;
top: 0px;
}
.container {
position: relative; /* absolute의 기준점 */
}wrapper는block(기본값)을 유지하므로 정규화 시 안쪽 노드가 정상 반환됩니다.position: absolute와left/top으로 원하는 위치에 배치합니다.- 부모
container에position: relative를 줘서absolute의 기준점을 잡아줍니다.
레이아웃은 그대로 유지하면서 hit-test도 정상 동작하게 되었습니다.
정리
CSS 속성 하나가 레이아웃뿐 아니라 브라우저의 내부 동작에도 영향을 끼치고, API의 결과까지 바꿀 수 있습니다.
caretRangeFromPoint,elementFromPoint등 좌표 기반 API는 내부적으로 block/inline 구조를 기반으로 정규화를 수행합니다.display가 바뀌면 레이아웃만 바뀌는 게 아니라 정규화의 기준 자체가 달라집니다.inline-block대신position: absolute로 배치하면block을 유지하면서 동일한 레이아웃을 만들 수 있습니다.