프론트엔드/Javascript

요소의 좌표 계산하기

syleemomo 2024. 2. 26. 14:34
728x90

* 요소의 좌표를 계산하는 방법 

첫번째 방법은 브라우저 좌측상단 꼭지점을 기준으로 좌표를 계산한다. 캡쳐화면에서는 해당 좌표를 clientX/clientY 로 표시하고 있다. 두번째 방법은 문서(document) 의 좌측상단 꼭지점을 기준으로 좌표를 계산한다. 캡쳐화면에서는 해당 좌표를 pageX/pageY로 표시하고 있다. 

요소의 좌표 계산하는 방법

캡쳐화면에서 좌측그림은 스크롤로 페이지를 이동하기 전의 모습이다. 이때는 pageX 와 clientX 가 동일하고, pageY와 clientY 도 동일한 값이다. 왜냐하면 스크롤을 이동하기 전에 브라우저와 상단과 문서의 상단이 정확히 일치하기 때문이다. 하지만 우측그림과 같이 스크롤이 움직이면 문서가 이동하면서 문서 기준 좌표(pageY)는 스크롤을 이동하기 전과 동일하지만, 브라우저 기준 좌표(clientY)는 변경된다. 

 

* 요소의 위치와 크기에 대한 좌표값 구하기 - getBoundingClientRect 메서드

요소의 getBoundingClientRect 메서드는 요소의 위치 좌표를 브라우저 창을 기준으로 계산한다. 

getBoundingClientRect 함수로 얻은 요소의 위치좌표

x, y 는 요소의 좌측상단 모서리가 브라우저 창의 좌측상단 모서리를 기준으로 얼마나 떨어져있는지 계산한다. width, height 은 요소의 너비와 높이이다. 다만 width, height 은 음수도 가능하다. 

top, bottom 은 요소의 위쪽 모서리나 아래쪽 모서리가 브라우저 상단으로부터 얼마나 떨어져 있는지 계산한다. left, right 은 요소의 왼쪽 모서리나 오른쪽 모서리가 브라우저 좌측으로부터 얼마나 떨어져 있는지 계산한다.  

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="box"></div>
    <button>좌표값 확인하기</button>
    <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

body{
    margin: 0; padding: 0;
    height: 200vh;
}
.box{
    width: 300px;
    height: 200px;
    margin-left: 300px;
    margin-top: 700px;
    background-color: orange;
}

style.css 파일을 위와 같이 작성하자!

const box = document.querySelector('.box')
const button = document.querySelector('button')

function getElementCoords(e){
    console.log("x좌표: ", box.getBoundingClientRect().x)
    console.log("y좌표: ", box.getBoundingClientRect().y)
    console.log("너비: ", box.getBoundingClientRect().width)
    console.log("높이: ", box.getBoundingClientRect().height)
    console.log("top: ", box.getBoundingClientRect().top)
    console.log("bottom: ", box.getBoundingClientRect().bottom)
    console.log("left: ", box.getBoundingClientRect().left)
    console.log("right: ", box.getBoundingClientRect().right)
    console.log("\n\n\n")
}

button.addEventListener('click', getElementCoords)

app.js 파일을 위와 같이 작성하자! 버튼을 클릭할때 box의 getBoundingClientRect 함수를 이용하여 요소의 좌표를 얻는다.

스크롤 하기전 좌표
스크롤 이후 좌표

페이지를 스크롤하면서 버튼을 클릭해보면 y, top, bottom 좌표값이 변경되는 것을 확인할 수 있다. 박스가 브라우저 상단에 가까워지기 때문이다. 

요소의 위치와 너비는 x, y, width, height 값만으로 충분히 계산이 가능하다. left, top, bottom, right 은 앞서 4가지 값으로 손쉽게 계산이 가능하다. left = x, top = y, right = x + width, bottom = y + height 이다. 

박스가 브라우저 상단 위로 올라간 경우

getBoundingClientRect 함수를 사용할때 주의할 점이 있는데 좌표값은 정수가 아니라 소수가 될수도 있다. 브라우저는 좌표 계산에 소수를 사용하므로 이는 정상이다. 따라서 elem.style.left 나 elem.style.top을 설정할때 정수로 바꾸거나 반올림할 필요는 없다. 좌표는 음수일수 있다. 페이지가 스크롤되어 박스가 브라우저 상단보다 위로 올라가면 top 은 음수값이 된다. 

 

* x, y 와 top, left 와의 차이점 

수학적으로 사각형은 시작 지점인 (x, y)와 방향벡터 (width, height) 으로 정의할 수 있다. 

시작점(x, y)으로부터 끝점으로 마우스를 드래그한 경우

예를 들어 캡쳐화면과 같이 마우스로 드래그한 경우 시작점은 (x, y) 지점이고, 끝점은 (left, top) 지점이다. 즉, 해당 경우에는 (x, y) 좌표와 (left, top) 은 서로 다르다. 또한, width, height 은 음수값이 된다. 하지만 실제로 getBoundingClientRect 메서드로 구한 width, height 값은 항상 양수이므로 걱정하지 않아도 된다. 단지 (x, y) 와 (left, top)이 왜 따로 존재하는지 차이를 설명하기 위해 해당 예시를 든것이다. 

아울러 인터넷 익스플로러에서는 x, y 값을 지원하지 않으므로 이때는 해당값 대신에 top, left 를 사용하면 된다. 또한, 브라우저 기준 좌표의 right, bottom 은 css 프로퍼티의 right, bottom 과 다르다. css 프로퍼티의 right 은 브라우저 오른쪽 모서리로부터 요소의 오른쪽 모서리까지의 거리이지만, 브라우저 기준 좌표의 right 은 브라우저 왼쪽 모서리부터 요소의 오른쪽 모서리까지의 거리이다. 

 

* 특정 좌표지점에 위치한 요소 검색하기 - elementFromPoint(x, y)

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

elementFromPoint 메서드는 특정 좌표 (x, y)를 포함하는 요소를 검색한다. 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="box"></div>
    <button>좌표값 확인하기</button>
    <script src="app.js"></script>
</body>
</html>
body{
    margin: 0; padding: 0;
    height: 200vh;
}
.box{
    width: 300px;
    height: 200px;
    position: absolute;
    top: 80%;
    left: 50%; transform: translateX(-50%);
    background-color: #eee;
}
function getMousePoints(e){
    const findedElement = document.elementFromPoint(e.clientX, e.clientY)
    findedElement.style.background = 'red'
}

window.addEventListener('click', getMousePoints)

해당 코드는 화면 클릭시 마우스 포인트를 포함하는 요소의 배경색을 붉은색으로 변경한다. 

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

하지만 위와 같이 요소가 브라우저 밖으로 벗어나면 요소 안에 포함된 (x, y)는 음수값을 가지거나 브라우저의 너비/.높이를 벗어난 값을 가지므로 이때는 elem 값은 null 이 된다. 즉, (x, y) 지점은 항상 브라우저 내에 있는 점이어야 한다. 

 

* 요소를 브라우저의 특정지점에 고정하기 

좌표는 대부분 뭔가를 위치시키기 위해 존재한다. 요소 근처에 어떤 다른 요소를 위치시킬때 css 에서는 좌표기준이 되는 요소에 position: relative 를 주고, 위치시키려는 요소는 position: absolute 를 적용한 다음 left, top 을 변경해서 요소 근처에 다른 요소를 위치시켰다. 자바스크립트에서는 getBoundingClientRect 함수를 이용하여 요소의 좌표를 구하고, 해당 좌표를 이용하여 다른 요소를 근처에 위치시킬수 있다. 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="coords-show-mark"></div>
    <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

#coords-show-mark{
    width: 500px; height: 500px;
    background-color: orange;
}

style.css 파일을 위와 같이 작성하자!

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, 'Hello World!')
document.body.append(message)
setTimeout(() => message.remove(), 5000)

app.js 파일을 위와 같이 작성하자! getBoundingClientRect 함수를 이용하여 오렌지색 박스의 좌표를 구하고, 해당 박스의 좌측 하단에 메세지가 위치하도록 message 요소의 left, top 을 설정한다.

오렌지 박스 바로 하단에 위치한 메세지 요소

body{
    height: 200vh;
}
#coords-show-mark{
    width: 500px; height: 500px;
    background-color: orange;
}

style.css 파일을 위와 같이 수정하자! body 높이를 변경해서 화면이 스크롤되게 한다. 

스크롤해서 오렌지 박스의 위치가 변경된 경우

새로고침하고 스크롤하면 오렌지 박스 좌측하단에 보여준 메세지는 그대로 있는데 오렌지 박스는 계속 위로 올라간다. 이유는 메세지의 position 을 fixed 로 설정하였기 때문이다. 따라서 아래와 같이 position 을 absolute 로 수정하고 다시 스크롤해보면 항상 오렌지 박스 하단에 메세지가 위치한다. 

message.style.cssText = "position:absolute; color: red"

 

* 문서 기준 좌표 

문서 기준 좌표는 브라우저 창이 아닌 문서의 좌측상단 모서리를 기준으로 요소의 좌표를 계산한다. css 와 비교하면 브라우저 기준좌표는 position: fixed 에 해당하고, 문서기준 좌표는 position: absolute 와 유사하다. 문서의 특정 위치에 요소를 고정하려면 position: absolute 와 top, left 를 함께 사용하면 된다. 이렇게 하면 스크롤 이동과 상관없이 해당 요소는 문서의 특정 위치에 고정된다. 

하지만 아직 문서를 기준으로 요소의 위치좌표를 구해주는 메서는 존재하지 않는다. 그럼에도 불구하고 브라우저 기준 좌표와 문서기준 좌표는 아래의 수식을 통해 손쉽게 문서기준 좌표를 구할 수 있다.  

pageY = clientY + 세로방향 스크롤에 의해 밀려난 문서의 위쪽 부분 높이 (세로 스크롤바 위치와 동일)
pageX = clientX + 가로방향 스크롤에 의해 밀려난 문서의 왼쪽 부분 너비 (가로 스크롤바 위치와 동일)

위의 수식에 의하여 문서기준 좌표는 아래 코드와 같이 먼저 요소의 브라우저 기준좌표를 구하고, 해당 좌표에 스크롤에 의해 밀려난 부분의 너비나 높이를 더해주면 된다. pageYOffset 은 앞선 수업에서 세로 스크롤바 위치와 동일하고, pageXOffset 은 가로 스크롤바 위치와 동일한 값이다. 

// 요소의 문서 기준 좌표를 얻습니다.
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
  };
}

문서 기준 좌표를 구하는 getCoords 함수를 활용하여 메세지를 생성하고 위치시켜주는 createMessageUnder 함수를 다시 작성하면 아래와 같다. 

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

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

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

  message.innerHTML = html

  return message
}

elem 의 문서기준 좌표를 구하기 위하여 앞서 살펴본 getCoords 함수를 이용하였다. 

 

연습과제 1

아래와 같이 헤더의 네비게이션 메뉴에는 서브메뉴가 존재한다. 네비게이션 메뉴의 서브메뉴를 구현할때 일반적인 방법은 HTML 문서에서 모든 메뉴 하단에 서브메뉴를 넣어두고, 메뉴 클릭시 display: none 에서 display: block 으로 클래스를 변경하여 서브메뉴를 보여준다. 하지만 리액트에서는 서브메뉴 UI를 한번 만들어놓고, 재활용하지 못한다. 서브메뉴 UI를 재활용하기 위해서는 HTML 문서에 서브메뉴를 넣어두는 것이 아니라 메뉴를 클릭할때마다 서브메뉴를 동적으로 생성해서 보여주는 것이 좋다. 

헤더의 네비게이션 메뉴

 

해당 서브메뉴를 아래의 몇가지 가이드라인을 참고하여 구현해보세요!

1. 서브메뉴는 주어진 서브메뉴 배열을 이용하여 자바스크립트로 생성한다. 
2. 서브메뉴는 네비게이션 메뉴를 클릭할때 자바스크립트로 생성한다.
3. 동일한 메뉴를 클릭하면 해당 서브메뉴는 메뉴를 클릭할때마다 열리고 닫힌다.
4. 다른 메뉴를 선택하면 기존에 열려있는 메뉴의 서브메뉴는 닫힌다.
5. 클릭한 메뉴의 서브메뉴 위치를 잡을때 getBoundingClientRect 함수를 사용한다.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <a href="#">logo</a>
    <nav>
      <ul>
        <li>
          <a href="#" class="menu">home</a>
          
        </li>
        <li>
          <a href="#" class="menu">about</a>
          
        </li>
        <li>
          <a href="#" class="menu">contact</a>
          
        </li>
      </ul>
    </nav>
  </header>
  <script src="app.js"></script>
</body>
</html>
:root{
  --primary-color: yellowgreen;
  --header-height: 70px;
}
*{
  box-sizing: border-box;
}
body{
  margin: 0; padding: 0;
}
a{
  text-decoration: none;
  color: var(--primary-color);
}
a:hover{
  color: yellow;
}
ul{
  list-style: none;
  margin: 0; padding: 0;
}
header{
  /* border: 1px solid red; */
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: fixed;
  left: 0; right: 0; top: 0;
  z-index: 1;
  background-color: #fff;
  box-shadow: 0 .3rem .3rem rgba(0, 0, 0, .1);
  height: var(--header-height);
}
header > nav > ul{
  display: flex;
  gap: 5rem;
  margin-right: 5rem;
}
header > nav .menu{
  position: relative;
  /* border: 1px solid red; */
}
header > nav .sub-menu{
  position: absolute;
  top: calc(var(--header-height) + 10px);
  border: 2px solid greenyellow;
  padding: .5rem 1rem;
  opacity: 1; 
  transition: .5s ease-in-out;
}
const navigation = document.querySelector('header > nav')
const homeMenus = ["home-sub-1", "home-sub-2", "home-sub-3"]      // 서브메뉴 배열
const aboutMenus = ["about-sub-1", "about-sub-2", "about-sub-3"]  // 서브메뉴 배열
const contactMenus = ["contact-sub-1", "contact-sub-2", "contact-sub-3"] // 서브메뉴 배열

// 구현하기

 

연습과제 2

스크롤을 내릴때 이미지가 브라우저 높이의 절반을 넘어가면 전체화면에 사진을 고정시켜서 보여주도록 해보자! (단, 스크롤을 내리기 전에 이미지는 브라우저 높이의 절반보다 하단에 위치해야 하고, 이미지의 크기는 브라우저 크기보다 작아야 한다. )

 

연습과제 3

브라우저 내에 랜덤한 위치에 랜덤한 색상으로 랜덤한 크기의 원이 1초마다 찍히도록 해보자! 기존에 있는 원은 남아있고, 새로운 원을 추가로 보여주도록 한다. (단, 생성된 원이 브라우저 밖을 조금이라도 벗어나면 화면에 그리지 않고, 다시 랜덤한 값을 뽑아서 보여주도록 한다.)

브라우저에 랜덤한 위치에 그려지는 원

 

연습과제 4

연습과제 3에서 랜덤한 위치에 원을 디스플레이했는데 이번에는 브라우저 내에서 랜덤한 위치에 랜덤한 크기로 동일한 텍스트를 1초 단위로 보여주도록 해보자!

브라우저에 랜덤하게 그려지는 텍스트

 

 

 

728x90

'프론트엔드 > Javascript' 카테고리의 다른 글

비동기 - 콜백  (0) 2024.03.13
프로토타입 상속  (0) 2024.03.08
요소 사이즈와 스크롤  (0) 2024.02.25
반복문 (loop)  (0) 2024.02.17
Tagged 템플릿 리터럴  (0) 2022.07.16