2021년 12월 15일

좌표

요소를 움직이려면 좌표(coordinates)에 익숙해져야 합니다.

대부분의 자바스크립트 메서드는 다음 두 좌표 체계 중 하나를 이용합니다.

  1. 창 기준position:fixed와 유사하게 창(window) 맨 위 왼쪽 모서리를 기준으로 좌표를 계산합니다.
    • 앞으로 우리는 이 좌표를 clientX/clientY로 표시할 예정인데, 왜 이런 이름을 쓰는지는 나중에 event 프로퍼티를 공부할 때 명확해집니다.
  2. 문서 기준 – 문서(document) 최상단(root)에서 position:absolute를 사용하는 것과 비슷하게 문서 맨 위 왼쪽 모서리를 기준으로 좌표를 계산합니다.
    • 이 좌표는 pageX/pageY로 표시할 예정입니다.

스크롤을 움직이기 전에는 창의 맨 위 왼쪽 모서리가 문서의 맨 위 왼쪽 모서리와 정확히 일치합니다. 그런데 스크롤이 움직이면서 문서가 이동하면 문서 기준 좌표는 변경되지 않지만, 창 내 요소는 움직이기 때문에 창 기준 요소 좌표가 변경됩니다.

다음 그림은 문서 내 한 지점의 스크롤이 움직이기 전(왼쪽)과 후(오른쪽) 좌표를 보여줍니다.

문서가 스크롤 되었을 때:

  • pageY – 문서 기준 좌표는 문서 맨 위(오른쪽 그림에선 스크롤 되어 보이지 않음)부터 계산되기 때문에 스크롤 후 값은 전과 동일합니다.
  • clientY – 문서가 스크롤 되면서 해당 지점이 창 상단과 가까워졌기 때문에 창 기준 좌표가 변했습니다(화살표가 짧아짐).

getBoundingClientRect로 요소 좌표 얻기

elem.getBoundingClientRect() 메서드는 elem을 감싸는 가장 작은 네모의 창 기준 좌표를 DOMRect 클래스의 객체 형태로 반환합니다.

DOMRect의 주요 프로퍼티는 다음과 같습니다.

  • xy – 요소를 감싸는 네모의 창 기준 X, Y 좌표
  • widthheight – 요소를 감싸는 네모의 너비, 높이(음수도 가능)

xy, widthheight 이외에 다음과 같은 파생 프로퍼티도 있습니다.

  • topbottom – 요소를 감싸는 네모의 위쪽 모서리, 아래쪽 모서리의 Y 좌표
  • leftright – 요소를 감싸는 네모의 왼쪽 모서리, 오른쪽 모서리의 X 좌표

아래 버튼을 눌러 창 기준 버튼 좌표를 확인해봅시다.

페이지를 조금씩 스크롤 하면서 창 기준 버튼 위치를 변경하고 버튼을 누르면 좌푯값이 바뀌는 것을 확인할 수 있습니다(수직 스크롤 시 y, top, bottom 값이 변함).

elem.getBoundingClientRect()의 각 프로퍼티를 그림으로 표현하면 다음과 같습니다.

그림을 통해 우리는 x, ywidth, height 만으로 네모 영역을 완전히 묘사할 수 있다는 사실을 알 수 있습니다. 아래 파생 프로퍼티들은 x, y, width, height를 사용해 쉽게 계산 가능합니다.

  • left = x
  • top = y
  • right = x + width
  • bottom = y + height

elem.getBoundingClientRect()를 사용할 때 주의사항은 다음과 같습니다.

  • 좌표는 10.5처럼 소수일 수 있습니다. 브라우저는 좌표 계산에 소수를 사용하기 때문에 이는 정상입니다. 따라서 style.left/top을 사용할 때 값을 반올림할 필요가 없습니다.
  • 좌표는 음수일 수 있습니다. 페이지가 스크롤 되어 elem이 window 위로 밀려났을 때 elem.getBoundingClientRect().top은 음수가 됩니다.
왜 파생 프로퍼티가 필요한가요? x, y가 있는데 top, left는 왜 존재하나요?

수학적으로 사각형은 시작 지점인 (x,y)와 방향 벡터 (width,height)만으로도 정의할 수 있는데, 파생 프로퍼티는 편의를 위해 존재합니다.

이론상 widthheight는 ‘방향이 있는’ 사각형을 나타낼 때 음수가 될 수 있습니다(예시: 시작과 끝 지점을 지정하고 마우스로 드래그해 영역을 표시할 때).

사각형이 오른쪽 아래에서 시작해 왼쪽 위로 ‘올라가면’ widthheight는 음수가 되죠.

widthheight가 음수인 사각형을 그림으로 나타내면 다음과 같습니다(width=-200, height=-100).

그림과 같은 사례에서 lefttopxy와 다릅니다.

이론상 차이가 있긴 하지만 실제 elem.getBoundingClientRect()widthheight는 항상 양수입니다. 여기선 파생 프로퍼티가 왜 따로 존재하는지를 설명하기 위해 widthheight가 음수인 사례를 살펴보았습니다.

Internet Explorer는 x, y를 지원하지 않습니다.

Internet Explorer는 예전부터 x, y 프로퍼티를 지원하지 않았습니다.

Internet Explorer에선 DomRect.prototype에 getter를 추가해 폴리필을 만들거나 elem.getBoundingClientRect()width, height가 양수인 경우에 top, leftx, y와 같다는 사실을 이용해 대신 x, y 대신 top, left를 사용합니다.

right, bottom 좌표는 CSS position 프로퍼티와 다릅니다.

창 기준 좌표와 CSS position:fixed 사이에는 명백한 유사점이 있습니다.

그러나 CSS에서 right 프로퍼티는 오른쪽 모서리로부터의 거리, bottom 프로퍼티는 아래 모서리로부터의 거리를 의미합니다.

위 그림을 보면 그 차이를 한 번에 볼 수 있죠. 그러니 right, bottom을 포함한 창 기준 좌표를 사용할 땐 측정 기준이 왼쪽 위 모서리라는 사실에 주의해야 합니다.

elementFromPoint(x, y)

document.elementFromPoint(x, y)을 호출하면 창 기준 좌표 (x, y)에서 가장 가까운 중첩 요소를 반환합니다.

문법은 다음과 같습니다.

let elem = document.elementFromPoint(x, y);

아래 예시를 실행하면 창 정중앙에 있는 요소의 태그가 얼럿창에 출력되고, 해당 요소가 붉은색으로 강조됩니다.

let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let elem = document.elementFromPoint(centerX, centerY);

elem.style.background = "red";
alert(elem.tagName);

document.elementFromPoint(x, y)는 창 기준 좌표를 사용하기 때문에 현재 스크롤 위치에 강조되는 요소는 다를 수 있습니다.

창밖 좌표를 대상으로 elementFromPoint를 호출하면 null이 반환됩니다.

document.elementFromPoint(x,y) 메서드는 (x,y)가 보이는 영역 안(창 안)에 있을 때만 동작합니다.

좌표 중 하나라도 음수이거나 창의 너비, 높이를 벗어나면 null이 반환됩니다.

이런 특징을 모르고 코드를 짜면 다음과 같은 전형적인 실수를 하게 됩니다.

let elem = document.elementFromPoint(x, y);
// 요소가 창 밖으로 나가면 lem = null
elem.style.background = ''; // 에러!

요소를 창 내 특정 좌표에 고정하기

좌표는 대부분 무언가를 위치시키려는 목적으로 사용합니다.

요소 근처에 무언가를 표시할 때에는 getBoundingClientRect를 사용해 요소의 좌표를 얻고 CSS positionleft/top(또는 right/bottom)과 함께 사용해서 표시하죠.

예를 들어 아래 createMessageUnder(elem, html) 함수는 elem 아래쪽에 메시지를 표시합니다.

let elem = document.getElementById("coords-show-mark");

function createMessageUnder(elem, html) {
  // 메시지가 담길 요소를 만듭니다.
  let message = document.createElement('div');
  // 요소를 스타일링 할 땐 css 클래스를 사용하는 게 좋습니다.
  message.style.cssText = "position:fixed; color: red";

  // 좌표를 지정합니다. 이때 "px"을 함께 써주는 걸 잊지 마세요!
  let coords = elem.getBoundingClientRect();

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

// 사용법:
// 문서 안에 메시지를 띄우고, 5초 동안만 보여줍니다.
let message = createMessageUnder(elem, '독도는 우리땅!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);

직접 버튼을 눌러 위 예시를 실행해 봅시다.

위 예시를 응용하면 메시지를 왼쪽 이나 오른쪽, 아래에 표시할 수도 있고 CSS 애니메이션을 적용하면 ‘fade-in’ 등의 효과도 줄 수 있습니다. 좌푯값과 요소의 크기만 알면 손쉽게 원하는 것을 할 수 있죠.

그런데 예시에서 뭔가 부자연스러운 게 보입니다. 페이지를 스크롤 하면 메시지가 버튼에서 떨어지네요.

메시지가 버튼에서 떨어지는 이유는 아주 명확합니다. 메시지 요소가 position:fixed이기 때문에 페이지가 스크롤 되어도 창 기준 동일한 위치에 있기 때문입니다.

이런 부자연스러운 현상을 개선하려면 문서 기준 좌표와 position:absolute를 함께 사용해야 합니다.

문서 기준 좌표

문서 기준 좌표는 창이 아닌 문서 왼쪽 위 모서리부터 시작합니다.

CSS와 비교하자면 창 기준 좌표는 position:fixed에 해당하고 문서 기준 좌표는 맨 위 기준 position:absolute와 비슷합니다.

문서 내 특정 좌표에 무언가를 위치시키고 싶을 땐 position:absolutetop,left`를 사용하면 스크롤 이동에 상관없이 해당 요소를 한 좌표에 머물게 할 수 있습니다. 그러려면 우선 정확한 좌표가 필요합니다.

그런데 요소의 문서 기준 좌표를 제공하는 표준 메서드가 아직 없습니다. 하지만 아주 쉽게 코드를 작성할 수 있습니다.

두 좌표 체계(창 기준 좌표와 문서 기준 좌표)는 다음 수식을 통해 연관시킬 수 있습니다.

  • pageY = clientY + 문서에서 세로 방향 스크롤에 의해 밀려난 부분의 높이
  • pageX = clientX + 문서에서 가로 방향 스크롤에 의해 밀려난 부분의 너비

다음 함수 getCoords(elem)elem.getBoundingClientRect()을 사용해 창 기준 좌표를 얻고 여기에 스크롤에 의해 가려진 영역의 너비나 높이를 더합니다.

// 요소의 문서 기준 좌표를 얻습니다.
function getCoords(elem) {
  let box = elem.getBoundingClientRect();

  return {
    top: box.top + window.pageYOffset,
    right: box.right + window.pageXOffset,
    bottom: box.bottom + window.pageYOffset,
    left: box.left + window.pageXOffset
  };
}

그런데 위 예시에서 position:absolute을 사용했다면 스크롤을 해도 메시지가 버튼 요소 근처에 머물렀을 겁니다.

이를 반영한 함수 createMessageUnder를 같이 살펴봅시다.

function createMessageUnder(elem, html) {
  let message = document.createElement('div');
  message.style.cssText = "position:absolute; color: red";

  let coords = getCoords(elem);

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

요약

페이지 내 모든 점은 다음과 같은 좌표를 갖습니다.

  1. 창 기준 – elem.getBoundingClientRect()
  2. 문서 기준 – elem.getBoundingClientRect()와 현재 스크롤 상태

창 기준 좌표는 position:fixed와 사용하면 좋고 문서 기준 좌표는 position:absolute와 사용하면 좋습니다.

두 좌표 체계 모두 장단점이 있습니다. CSS의 position, absolute, fixed처럼 이게 필요할 때도 있고 저게 필요할 때도 있습니다.

과제

중요도: 5

In the iframe below you can see a document with the green “field”.

Use JavaScript to find window coordinates of corners pointed by with arrows.

There’s a small feature implemented in the document for convenience. A click at any place shows coordinates there.

Your code should use DOM to get window coordinates of:

  1. Upper-left, outer corner (that’s simple).
  2. Bottom-right, outer corner (simple too).
  3. Upper-left, inner corner (a bit harder).
  4. Bottom-right, inner corner (there are several ways, choose one).

The coordinates that you calculate should be the same as those returned by the mouse click.

P.S. The code should also work if the element has another size or border, not bound to any fixed values.

샌드박스를 열어 정답을 작성해보세요.

Outer corners

Outer corners are basically what we get from elem.getBoundingClientRect().

Coordinates of the upper-left corner answer1 and the bottom-right corner answer2:

let coords = elem.getBoundingClientRect();

let answer1 = [coords.left, coords.top];
let answer2 = [coords.right, coords.bottom];

Left-upper inner corner

That differs from the outer corner by the border width. A reliable way to get the distance is clientLeft/clientTop:

let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop];

Right-bottom inner corner

In our case we need to substract the border size from the outer coordinates.

We could use CSS way:

let answer4 = [
  coords.right - parseInt(getComputedStyle(field).borderRightWidth),
  coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
];

An alternative way would be to add clientWidth/clientHeight to coordinates of the left-upper corner. That’s probably even better:

let answer4 = [
  coords.left + elem.clientLeft + elem.clientWidth,
  coords.top + elem.clientTop + elem.clientHeight
];

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Create a function positionAt(anchor, position, elem) that positions elem, depending on position near anchor element.

The position must be a string with any one of 3 values:

  • "top" – position elem right above anchor
  • "right" – position elem immediately at the right of anchor
  • "bottom" – position elem right below anchor

It’s used inside function showNote(anchor, position, html), provided in the task source code, that creates a “note” element with given html and shows it at the given position near the anchor.

Here’s the demo of notes:

샌드박스를 열어 정답을 작성해보세요.

In this task we only need to accurately calculate the coordinates. See the code for details.

Please note: the elements must be in the document to read offsetHeight and other properties. A hidden (display:none) or out of the document element has no size.

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Modify the solution of the previous task so that the note uses position:absolute instead of position:fixed.

That will prevent its “runaway” from the element when the page scrolls.

Take the solution of that task as a starting point. To test the scroll, add the style <body style="height: 2000px">.

The solution is actually pretty simple:

  • Use position:absolute in CSS instead of position:fixed for .note.
  • Use the function getCoords() from the chapter 좌표 to get document-relative coordinates.

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Extend the previous task Show a note near the element (absolute): teach the function positionAt(anchor, position, elem) to insert elem inside the anchor.

New values for position:

  • top-out, right-out, bottom-out – work the same as before, they insert the elem over/right/under anchor.
  • top-in, right-in, bottom-in – insert elem inside the anchor: stick it to the upper/right/bottom edge.

For instance:

// shows the note above blockquote
positionAt(blockquote, "top-out", note);

// shows the note inside blockquote, at the top
positionAt(blockquote, "top-in", note);

The result:

As the source code, take the solution of the task Show a note near the element (absolute).

튜토리얼 지도