2일 9월 2020

좌표

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

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

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

스크롤을 움직이기 바로 전에는 window 상단/왼쪽 모서리가 document 상단/왼쪽 모서리와 정확히 일치합니다. 그런데 스크롤이 움직이기 시작하면 document가 움직이기 때문에 document 기준 좌표는 그대로이지만 요소의 window 기준 좌표는 바뀝니다.

다음 그림은 document 내 한 지점을 잡고 나서 스크롤 전(왼쪽)과 후(오른쪽)의 좌표를 보여줍니다.

document가 스크롤 될 때

  • pageY – document 기준 좌표는 document 상단(스크롤 되어 밀려남)에서부터 계산되어 동일합니다.
  • clientY – window 기준 좌표는 해당 지점이 window 상단과 가까워지면서 변화(화살표가 짧아짐)되었습니다.

getBoundingClientRect로 요소 좌표 얻기

elem.getBoundingClientRect() 메서드는 elem을 감쌀 수 있는 가장 작은 네모영역의 window 기준 좌표를 DOMRect 클래스의 객체로 반환합니다.

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

  • x/y – 네모영역의 window 기준 X/Y 좌표
  • width/height – 네모영역의 너비/높이(음수 가능)

x/y, width/height 말고 다음과 같은 프로퍼티도 있습니다.

  • top/bottom – 네모영역 상단/하단 모서리의 Y 좌표
  • left/right – 네모영역 왼쪽/오른쪽 모서리의 X 좌표

이 버튼을 누르면 버튼의 window 기준 좌표를 예시로 보여줍니다.

페이지를 스크롤 하여 반복해보면 버튼의 위치가 달라지면서 window 기준 좌표(수직 스크롤 시 y/top/bottom)도 바뀌는 것을 알 수 있습니다.

다음 그림은 elem.getBoundingClientRect()의 결과입니다.

보이는 것처럼 x/ywidth/height 만으로 네모 영역을 완전히 묘사할 수 있습니다. 파생된 프로퍼티는 이들로부터 쉽게 계산 가능한 값입니다.

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

다음을 주의하세요.

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

수학적으로 사각형은 시작 지점인 (x,y)와 방향 벡터 (width,height)만으로도 정의됩니다. 즉 파생된 프로퍼티는 편의를 위한 목적으로 사용합니다.

이론적으로 width/height는 ‘방향이 있는’ 사각형을 나타낼 때 음수가 될 수 있습니다. 예를 들어 마우스로 시작과 끝 지점을 지정하여 선택 영역을 표시할 때 사용할 수 있습니다.

사각형이 하단 오른쪽에서 시작하여 상단 왼쪽으로 ‘올라가면’ width/height는 음수 값입니다.

다음은 widthheight가 음수 값인 사각형입니다(width=-200, height=-100).

보이는 것처럼 이런 예시에서는 left/topx/y와 같은 값이 아닙니다.

그러나 실제로는 elem.getBoundingClientRect()는 항상 양수의 width/height 값을 반환하며 여기에서는 같아 보이는 프로퍼티가 왜 중복이 아닌지를 설명하기 위해 width/height 음수 값을 언급했습니다.

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

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

그래서 DomRect.prototype에 getter를 추가해 폴리필을 만들거나 elem.getBoundingClientRect()를 호출하면 그 결괏값의 width/height가 양수라는 특징을 이용해 x/y대신 top/left을 이용합니다.

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

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

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

위에 있는 그림만 보더라도 자바스크립트에서는 의미가 다름을 알 수 있습니다. right, bottom 프로퍼티를 포함한 모든 window 기준 좌표는 상단 왼쪽 시작점에서부터 계산됩니다.

elementFromPoint(x, y)

document.elementFromPoint(x, y)을 호출하면 window 기준 좌표 (x, y)에 있는 요소를 중첩 최하위에 있는 요소로 반환합니다.

문법은 다음과 같습니다.

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

예를 들어 아래 코드는 지금 window 정중앙에 있는 요소의 태그를 강조해서 표시하고 태그 이름을 출력합니다.

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);

window 좌표를 사용하기 때문에 요소는 현재 스크롤하고 있는 위치에 따라 다를 수 있습니다.

window 밖으로 나간 좌표에서 elementFromPointnull을 반환합니다.

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

좌표 중 하나라도 음수이거나 window의 너비/넓이를 벗어나면 null을 반환합니다.

다음은 확인하지 않았을 때 나타날 수 있는 전형적인 에러입니다.

let elem = document.elementFromPoint(x, y);
// 좌표가 window 밖으로 나가면 elem = null
elem.style.background = ''; // 에러!

‘고정(fixed)’ 위치 지정에 사용하기

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

요소 근처에 무언가를 표시할 때에는 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;
}

// 쓰임새 :
// document 안에 5초 동안만 추가합니다.
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);

실행하려면 버튼을 누르세요.

코드를 수정하면 왼쪽, 오른쪽, 아래쪽에 메시지를 표시할 수 있고 CSS 애니메이션을 적용하여 ‘페이드 인’ 등도 할 수 있습니다. 모든 좌푯값과 요소의 크기만 알면 쉽게 할 수 있습니다.

그러나 중요한 주의사항이 있습니다. 페이지를 스크롤 하면 메시지가 버튼에서 떨어지게 됩니다.

이유는 분명합니다. 메시지 요소가 position:fixed이기 때문에 페이지가 스크롤 되어도 window 기준으로 같은 지점에 남아 있게 됩니다.

이것을 바꾸고 싶으면 document 기반 좌표와 position:absolute를 사용해야 합니다.

document 좌표

document 기준 좌표는 window가 아닌 document 상단 왼쪽 모서리에서부터 시작합니다.

CSS로 따지면 window 좌표는 position:fixed에 해당하고 document 좌표는 상단에서부터의 position:absolute와 비슷합니다.

document의 특정 지점에 무언가를 위치시킬 때 position:absolutetop/left를 사용하면 페이지를 스크롤 해도 한자리에 있게 할 수 있습니다. 그러려면 우선 정확한 좌표가 있어야 합니다.

요소의 document 좌표를 얻는 표준 메서드는 없습니다. 하지만 쉽게 작성할 수 있습니다.

두 좌표 체계는 다음 수식에 의해 연관됩니다.

  • pageY = clientY + document에서 스크롤 되어 수직으로 밀려난 부분의 높이
  • pageX = clientX + document에서 스크롤 되어 수평으로 밀려난 부분의 너비

다음 getCoords(elem) 함수는 elem.getBoundingClientRect()에서 window 좌표를 얻고 여기에 현재 스크롤 된 위치를 더합니다.

// 요소의 document 좌표를 얻습니다
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. window 기준 – elem.getBoundingClientRect()
  2. document 기준 – elem.getBoundingClientRect() + 현재 페이지 스크롤 위치

window 좌표는 position:fixed와 사용하기 좋고 document 좌표는 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).

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.