jQuery / Method / .offset() - 선택한 요소의 좌표를 가져오거나 특정 좌표로 이동시키는 메서드
jQuery의 .offset()은 선택한 첫 요소의 테두리 박스가 문서의 왼쪽 위에서 얼마나 떨어져 있는지 { top, left }로 반환합니다. .offset({ top, left })는 선택한 요소들을 문서 기준의 지정 좌표로 옮깁니다. 화면에 보이는 영역인 뷰포트 기준 좌표나 부모 기준 좌표와 혼동하지 않습니다.
offset·position·뷰포트 좌표 구분하기
.offset() 공식 문서의 기준은 문서이며, 대상의 margin을 제외한 border box, 즉 테두리 바깥 경계의 위치를 읽습니다. top·left는 CSS 픽셀 단위 숫자이고 소수일 수도 있습니다. 좌표를 읽은 것만으로 요소가 이동하지는 않습니다.
| 방법 | 기준 | 대상 경계 |
|---|---|---|
| .offset() | 문서 | 테두리 박스 |
| .position() | offset 부모의 패딩 박스 | 대상의 마진 박스 |
| getBoundingClientRect() | 뷰포트 | 테두리를 포함한 사각형 |
.position()의 offset 부모는 단순히 HTML의 바로 위 요소와 같다고 단정할 수 없습니다. getBoundingClientRect()의 top·left는 스크롤에 따라 바뀌며, 일반적인 문서 좌표로 바꾸려면 현재 window.scrollY·scrollX를 더합니다. 서로 다른 기준의 값을 CSS top·left로 그대로 넣지 않습니다.
세 좌표와 스크롤을 비교하는 전체 예제
전체 코드를 UTF-8의 offset-demo.html로 저장하고 브라우저에서 엽니다. 공식 jQuery 파일 4.0.0을 CDN(Content Delivery Network, 콘텐츠 전송 네트워크)에서 읽을 인터넷 연결이 필요하며 별도 설치는 없습니다. 좌표 읽기·스크롤·이동·숨김·초기화와 아래 함수형 설정을 3.7.1과 4.0.0에서 실행했습니다.
stage는 문서 top 500·left 20에 있고 테두리는 5입니다. target은 그 안의 top 40·left 50에 margin 10을 가집니다. body margin은 0이고 변형 효과는 없으므로 시작 좌표를 직접 계산해 비교할 수 있습니다. 조작부는 화면 위에 고정합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document and viewport coordinates</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { margin: 0; min-height: 1800px; font: 16px/1.6 sans-serif; }
#controls { position: fixed; inset: 0 0 auto; padding: 12px; background: #fff; z-index: 10; max-height: 42vh; overflow: auto; }
button { font: inherit; margin: 3px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
#stage { position: absolute; top: 500px; left: 20px; width: 300px; height: 260px; border: 5px solid #bbc; }
#target { position: absolute; top: 40px; left: 50px; margin: 10px; border: 2px solid teal; padding: 8px; background: #d9f4ef; }
</style>
</head>
<body>
<div id="controls">
<strong>Measure the target border box</strong>
<button type="button" id="read">Read</button>
<button type="button" id="scroll">Scroll 200px</button>
<button type="button" id="move">Move to (top 620, left 130)</button>
<button type="button" id="visibility">Toggle visibility</button>
<button type="button" id="display">Toggle display</button>
<button type="button" id="reset">Reset</button>
<pre id="result" aria-live="polite"></pre>
</div>
<div id="stage"><div id="target">Target</div></div>
<script>
$(function () {
const $target = $("#target");
function n(value) { return Number(value.toFixed(2)); }
function report() {
const node = $target.get(0);
if (!node) { $("#result").text("Target missing"); return; }
if (!node.getClientRects().length) { $("#result").text("Not rendered; reveal before measuring"); return; }
const offset = $target.offset();
const rect = node.getBoundingClientRect();
const position = $target.position();
$("#result").text(`document top=${n(offset.top)}, left=${n(offset.left)}\nviewport top=${n(rect.top)}, left=${n(rect.left)}\nposition top=${n(position.top)}, left=${n(position.left)}\nscrollY=${n(window.scrollY)}`);
}
$("#read").on("click", report);
$("#scroll").on("click", function () { window.scrollTo(0, 200); });
$("#move").on("click", function () { if ($target.get(0).getClientRects().length) $target.offset({ top: 620, left: 130 }); report(); });
$("#visibility").on("click", function () { $target.css("visibility", $target.css("visibility") === "hidden" ? "visible" : "hidden"); report(); });
$("#display").on("click", function () { $target.toggle(); report(); });
$("#reset").on("click", function () { $target.css({ top: 40, left: 50, display: "", visibility: "" }); window.scrollTo(0, 0); report(); });
$(window).on("scroll.offsetLesson resize.offsetLesson", report);
report();
});
</script>
</body>
</html>
스크롤 0에서의 초기 결과입니다. 다른 페이지에 코드 일부만 넣으면 주변 배치가 달라 이 수치와 같지 않을 수 있습니다.
document top=555, left=85
viewport top=555, left=85
position top=40, left=50
scrollY=0
- Read로 좌표를 다시 읽습니다. target의 문서 top은 500 + 5 + 40 + 10 = 555이고 left는 20 + 5 + 50 + 10 = 85입니다.
- Scroll 200px를 누르면 문서 top은 555지만 viewport top은 355, scrollY는 200입니다. .position()은 top 40·left 50을 유지합니다.
- Move to (top 620, left 130)을 누르면 문서 좌표는 620·130, 스크롤 200에서 viewport는 420·130입니다. .position()은 105·95가 되어 부모 기준 CSS 값과 문서 좌표가 다름을 보여 줍니다.
- Toggle visibility로 숨기면 target이 안 보이지만 레이아웃이 남아 좌표를 계속 측정합니다.
- Toggle display로 display:none이 되면 수치를 제시하지 않고 Not rendered 안내가 나옵니다. 한 번 더 누르면 다시 측정합니다.
- Reset은 CSS 위치와 숨김 상태를 복원하고 스크롤을 0으로 되돌립니다.
이 실습의 target은 일반 절대 위치 요소입니다. 위치가 그대로라면 창 스크롤만으로 문서 좌표는 바뀌지 않지만, fixed·sticky나 레이아웃 변화가 있는 요소에도 이를 무조건 적용하지 않습니다. 이미지를 읽거나 글꼴·창 크기가 바뀐 뒤에는 필요한 시점에 다시 측정합니다.
좌표 설정과 함수형 인수
.offset({ top: 620, left: 130 })에 넣는 값은 부모 안의 CSS top·left가 아니라 문서 기준입니다. jQuery는 필요한 위치 값을 계산하며, static 요소의 설정에서는 위치 조정을 위해 relative로 바꿀 수 있습니다. 기존 배치와 CSS에 어떤 변화가 생기는지도 확인합니다.
함수형 .offset(function(index, current))은 각 요소의 현재 문서 좌표를 받아 새 top·left를 반환합니다. 개발자 도구 Console에서 다음 완전한 부분 코드를 실행합니다. 임시 두 요소를 만든 뒤 첫째·둘째의 세로 간격을 다르게 주며 마지막에 정리합니다.
const $items = $("<div>A</div><div>B</div>")
.css({ position: "absolute", top: 1000, left: 20 }).appendTo("body");
$items.offset(function (index, current) {
return { top: current.top + index * 30, left: current.left + 40 };
});
console.log($items.map(function () {
const point = $(this).offset();
return `${point.top},${point.left}`;
}).get().join(" | "));
console.log(`empty getter=${typeof $().offset()}`);
$items.remove();
1000,60 | 1030,60
empty getter=undefined
읽기는 첫 요소만, 설정은 선택한 모든 요소에 적용됩니다. index는 집합에서 0부터 시작합니다. 예제의 출력 반올림은 화면 가독성을 위한 것이며, 실제 계산에서 소수 좌표를 무조건 정수로 버리지 않습니다.
숨김·빈 선택·배치 변화 확인하기
display:none 요소는 렌더링 박스가 없으므로 의미 있는 현재 화면 위치를 측정하는 대상으로 쓰지 않습니다. 일부 환경에서 0 같은 값이 나와도 실제 위치라고 받아들이지 말고 먼저 표시한 후 측정합니다. visibility:hidden은 박스가 남는 상태라 좌표 측정이 가능합니다.
예제는 노드 존재와 getClientRects().length를 검사한 다음 읽습니다. 빈 jQuery 선택의 .offset()은 undefined이므로 .offset().top을 바로 사용하면 오류가 납니다. DOM(Document Object Model, 문서 객체 모델)에 연결되지 않은 요소도 의도한 문서 위치를 얻는 대상으로 가정하지 않습니다.
브라우저 확대, transform, 바뀐 margin·border와 상위 배치를 함께 확인합니다. 공식 문서는 html 요소의 margin을 계산에 반영하는 것을 지원하지 않는다고 안내합니다. 값이 어긋나면 기준 좌표계부터 맞추고, 읽기와 CSS 쓰기를 필요 이상 반복하는 대신 실제 배치가 바뀌는 시점에 갱신합니다.




