DOM 크기·좌표·충돌 처리 마스터

offsetWidth, getBoundingClientRect(), clientX, innerHeight를 조합하여 Tooltip, Dropdown, Context Menu, Drag UI를 직접 구현합니다.

1. UI 위치 계산의 전체 흐름

위치형 UI는 아래 네 단계로 생각하면 거의 모두 해결됩니다.

① 기준 요소 측정 target.getBoundingClientRect()

버튼의 위·아래·좌·우 좌표와 크기를 가져옵니다.

② 떠 있는 UI 크기 측정 layer.offsetWidth

Tooltip이나 메뉴가 차지할 실제 너비와 높이를 확인합니다.

③ 화면의 남은 공간 계산 window.innerHeight - rect.bottom

버튼 아래쪽 또는 위쪽에 공간이 충분한지 비교합니다.

④ 최종 위치 적용 style.left / style.top

화면 밖으로 나가지 않도록 보정한 좌표를 적용합니다.

const rect = target.getBoundingClientRect();
const layerHeight = layer.offsetHeight;

const spaceTop = rect.top;
const spaceBottom = window.innerHeight - rect.bottom;

const placement =
  spaceBottom >= layerHeight || spaceBottom >= spaceTop
    ? "bottom"
    : "top";

이 코드에서 말하는 “위·아래”는 어디인가?

placement = "bottom"
[ 버튼 ]
   ↓
[ Tooltip ]

Tooltip 또는 Dropdown 레이어가 버튼 아래쪽에 표시된다는 뜻입니다.

placement = "top"
[ Tooltip ]
   ↑
[ 버튼 ]

Tooltip 또는 Dropdown 레이어가 버튼 위쪽에 표시된다는 뜻입니다.

한 줄씩 해석하기

const rect = target.getBoundingClientRect();

기준이 되는 버튼의 현재 화면 좌표를 가져옵니다. rect.top은 버튼 위쪽 좌표, rect.bottom은 버튼 아래쪽 좌표입니다.

const layerHeight = layer.offsetHeight;

실제로 띄울 Tooltip이나 Dropdown의 높이를 구합니다. 예를 들어 레이어 높이가 80px이면 layerHeight는 80입니다.

const spaceTop = rect.top;

화면 맨 위는 0이므로, 버튼의 top 값 자체가 버튼 위쪽에 남아 있는 공간입니다.

const spaceBottom = window.innerHeight - rect.bottom;

브라우저 화면 전체 높이에서 버튼의 아래쪽 좌표를 빼면 버튼 아래에 남아 있는 공간이 됩니다.

숫자로 보면 더 쉽습니다

버튼이 화면 위쪽에 있는 경우
window.innerHeight = 800
rect.top = 40
rect.bottom = 80
layerHeight = 100

spaceTop = 40
spaceBottom = 800 - 80
            = 720

아래 공간 720px 안에 높이 100px짜리 Tooltip이 충분히 들어가므로 placement = "bottom"이 됩니다.

버튼이 화면 아래쪽에 있는 경우
window.innerHeight = 800
rect.top = 720
rect.bottom = 760
layerHeight = 100

spaceTop = 720
spaceBottom = 800 - 760
            = 40

아래 공간은 40px뿐이지만 Tooltip 높이는 100px이므로 아래에 넣을 수 없습니다. 위 공간이 더 넓기 때문에 placement = "top"이 됩니다.

삼항 연산자 조건의 의미

spaceBottom >= layerHeight
||
spaceBottom >= spaceTop

아래 공간에 레이어가 완전히 들어가거나, 또는 위쪽보다 아래쪽 공간이 더 넓다면 아래에 표시합니다.

두 조건이 모두 거짓이면 위쪽 공간이 더 유리하므로 "top"을 선택합니다.

중요: 현재 코드는 위치를 결정만 합니다

placement에 "top" 또는 "bottom" 문자열을 저장할 뿐, 아직 Tooltip을 실제로 이동시키지는 않습니다.

if (placement === "bottom") {
  layer.style.top = rect.bottom + 8 + "px";
} else {
  layer.style.top =
    rect.top - layer.offsetHeight - 8 + "px";
}
최종 처리 순서

① 버튼 좌표 측정 → ② 레이어 크기 측정 → ③ 위·아래 남은 공간 계산 → ④ top 또는 bottom 결정 → ⑤ style.top과 style.left에 실제 좌표 적용

2. offset / client / scroll 크기 비교

아래 박스는 width, padding, border, overflow가 모두 들어 있습니다. 버튼을 눌러 실제 브라우저 측정값을 확인하세요.

측정 대상 박스
offsetWidth padding + border를 포함한 보이는 전체 너비
clientWidth padding 포함, border와 세로 스크롤바 제외
scrollWidth 현재 가려진 영역까지 포함한 전체 콘텐츠 너비

3. getBoundingClientRect()

요소가 현재 브라우저 화면(Viewport)의 어디에 있는지를 알려주는 가장 중요한 메서드입니다.

이 카드의 top, bottom, left, right, width, height를 측정합니다.
innerWidth -
innerHeight -
scrollX -
scrollY -

4. 마우스 좌표 이해

아래 영역에서 마우스를 움직이면 clientX, pageX, offsetX 차이를 볼 수 있습니다.

영역 안에서 마우스를 움직이세요.
stage.addEventListener("mousemove", function (event) {
  console.log(event.clientX); // 현재 화면 기준
  console.log(event.pageX);   // 문서 전체 기준
  console.log(event.offsetX); // 이벤트 대상 요소 내부 기준
});

5. 화면 위치에 따라 위·아래가 바뀌는 Tooltip

상단 버튼은 아래로, 하단 버튼은 위로 나타납니다. 중앙 버튼은 남는 공간을 비교해 자동으로 결정합니다.

const spaceTop = rect.top;
const spaceBottom = window.innerHeight - rect.bottom;

if (spaceBottom >= tooltipHeight || spaceBottom >= spaceTop) {
  // 버튼 아래 표시
} else {
  // 버튼 위 표시
}

7. 화면 밖으로 나가지 않는 Context Menu

회색 영역 어디서든 마우스 오른쪽 버튼을 눌러 보세요. 오른쪽 또는 아래 공간이 부족하면 메뉴가 반대쪽으로 이동합니다.

이 영역에서 우클릭하세요.

8. 좌표 계산으로 만드는 Drag UI

박스를 드래그해 보세요. 부모 영역 밖으로 나가지 않도록 좌표를 제한합니다.

나를 드래그하세요
const maxLeft = stage.clientWidth - box.offsetWidth;
const maxTop = stage.clientHeight - box.offsetHeight;

left = Math.max(0, Math.min(left, maxLeft));
top = Math.max(0, Math.min(top, maxTop));

9. ResizeObserver

아래 박스 우측 하단을 드래그해 크기를 바꾸면 JavaScript가 변경된 크기를 감지합니다.

이 박스는 CSS resize: both가 적용되어 있습니다.
크기 변경 대기 중
const observer = new ResizeObserver(function (entries) {
  const rect = entries[0].contentRect;
  console.log(rect.width, rect.height);
});

observer.observe(target);

10. 최종 학습 체크리스트

  • offsetWidth / offsetHeight
  • clientWidth / clientHeight
  • scrollWidth / scrollHeight
  • offsetLeft / offsetTop
  • offsetParent
  • getBoundingClientRect()
  • window.innerWidth / innerHeight
  • scrollX / scrollY
  • clientX / clientY
  • pageX / pageY
  • offsetX / offsetY
  • style.left / style.top
  • position: fixed / absolute
  • 화면 좌·우 경계 충돌 처리
  • 화면 위·아래 공간 비교
  • Drag 범위 제한
  • ResizeObserver
  • requestAnimationFrame()