22일 8월 2020

요소 사이즈와 스크롤

자바스크립트는 요소의 너비, 높이와 같은 기하 정보를 알려주는 다양한 프로퍼티를 지원합니다.

이런 프로퍼티들은 요소를 움직이거나 특정 좌표에 위치시킬 때 사용됩니다.

샘플 요소

프로퍼티 사용법을 알아보기 위해 아래와 같은 샘플 요소를 사용할 예정입니다.

<div id="example">
  ...텍스트...
</div>
<style>
  #example {
    width: 300px;
    height: 200px;
    border: 25px solid #E8C48F;
    padding: 20px;
    overflow: auto;
  }
</style>

example이라는 id가 붙은 이 요소는 border와 padding, 스크롤바 갖습니다. 기하 관련 기능을 테스트 하기 위해 적절한 조합이죠. margin은 요소 자체에 포함되지 않고, 관련 특수 프로퍼티가 없기 때문에 CSS 프로퍼티에 추가하지 않았습니다.

요소의 생김새는 다음과 같습니다.

샌드박스를 열면 요소를 직접 확인해 볼 수 있습니다.

스크롤바를 잊지 마세요.

샘플로 사용하게 될 요소는 스크롤 바가 있는 가장 복잡한 상황을 나타냅니다. 모든 브라우저가 그런 건 아니지만 몇몇 브라우저는 콘텐츠 영역 너비('content width’로 표시한 영역) 일부를 빌려 스크롤바를 위치시킵니다.

스크롤바가 없었다면 content width는 300px이 되었을 겁니다. 그런데 스크롤바의 너비가 16px이기 때문에 content width는 284px(300 – 16)가 됩니다(스크롤바 너비는 브라우저나 디바이스마다 다릅니다). 개발자는 요소를 다룰 때 스크롤바가 차지하는 공간을 항상 염두하고 있어야 합니다. 스크롤바가 없다면 계산이 쉬웠겠지만, 연습을 위해 샘플 요소에 스크롤바를 필수적으로 포함시켰습니다.

padding-bottom 영역으로 텍스트가 넘칠 수 있습니다.

패딩은 대개 비어있습니다. 그런데 요소 내 텍스트가 길어 넘치게 될 경우엔 브라우저가 이 텍스트들을 padding-bottom에도 표시하게 됩니다. 이는 정상적인 동작입니다.

기하 프로퍼티

기하 프로퍼티(geometry property)를 그림으로 나타내면 다음과 같습니다.

기하 프로퍼티의 값은 숫자인데 그 단위는 '픽셀’입니다. 기하 프로퍼티는 픽셀 단위로 측정된다고 보시면 됩니다.

이제 요소 제일 밖부터 시작해 기하 프로퍼티를 하나씩 살펴보도록 합시다.

offsetParent와 offsetLeft, offsetTop

잘 쓰이는 프로퍼티는 아니지만 가장 바깥에 있는 기하 프로퍼티이므로 offsetParent와 offsetLeft, offsetTop부터 알아보겠습니다(offset은 요소가 화면에서 차지하는 영역 전체 크기를 나타냅니다 – 옮긴이).

offsetParent 프로퍼티는 해당 요소를 렌더링할 때, 좌표 계산에 사용되는 가장 가까운 조상 요소(the closest positioned ancestor element)의 참조를 반환합니다.

CSS position 프로퍼티가 설정되어있는 조상 요소가 없는 경우엔 가장 가까운 조상 <td><th>, 혹은 <table>, 아니면 <body>가 반환되기 때문에 offsetParent에 의해 반환되는 가장 가까운 조상 요소는 아래 셋 중 하나에 속하게 됩니다.

  1. CSS position 프로퍼티가 absoluterelative, fixed, sticky인 가장 가까운 조상 요소
  2. <td><th>, 혹은 <table>
  3. <body>

offsetLeft, offsetTop 프로퍼티는 offsetParent를 기준으로 각각 요소가 오른쪽으로, 아래쪽으로 얼마나 떨어져 있는지를 나타냅니다.

예시를 살펴봅시다. 안쪽에 있는 <div>offsetParent<main>이고 offsetLeftoffsetTop은 각각 180입니다.

<main style="position: relative" id="main">
  <article>
    <div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
  </article>
</main>
<script>
  alert(example.offsetParent.id); // main
  alert(example.offsetLeft); // 180 (문자열 '180px'이 아닌 숫자가 반환됩니다.)
  alert(example.offsetTop); // 180
</script>

다음과 같은 경우에 offsetParentnull입니다.

  1. 화면에 보이지 않는 요소(CSS display 프로퍼티가 none이거나 문서 내에 있지 않은 요소)
  2. <body><html>
  3. position 프로퍼티가 fixed인 요소

offsetWidth와 offsetHeight

이제 본격적으로 요소 자체에 집중해 봅시다.

offsetWidthoffsetHeight는 가장 간단한 프로퍼티입니다. 두 프로퍼티는 각각 요소 '전체’가 차지하는 너비와 높이 정보를 제공합니다. '전체’이기 때문에 테두리 역시 포함됩니다.

샘플 요소를 대상으로 offsetWidthoffsetHeight를 계산하면 다음과 같습니다.

  • offsetWidth = 390 – 안쪽 너비(콘텐츠 너비 + 스크롤바)에 패딩(2 * 20px)과 테두리(2 * 25px)를 더한 값
  • offsetHeight = 290 – 전체 높이
화면에 표시되지 않는 요소의 기하 프로퍼티는 0 또는 null입니다.

기하 프로퍼티는 오직 보이는 요소(displayed element)를 대상으로 계산됩니다.

따라서 요소(혹은 이 요소의 조상 요소 중 어떤 것이든)의 CSS display 프로퍼티가 none이거나 문서 내에 해당 요소가 없는 경우, 모든 기하 프로퍼티 값은 0이 됩니다(offsetParent 프로퍼티의 값은 null).

요소를 만들긴 했지만 아직 문서에 삽입하기 전이라던가, 새롭게 만든 요소의 display 프로퍼티가 none이면 기하 프로퍼티 값은 0, offsetParent 프로퍼티의 값은 null이 되는 것이죠.

이런 특징을 이용하면 요소의 숨김 상태 여부를 아래와 같은 방법을 사용해 확인할 수 있습니다.

function isHidden(elem) {
  return !elem.offsetWidth && !elem.offsetHeight;
}

그런데 위 함수 isHidden은 요소가 화면에 있긴 하지만 사이즈가 0일 때(비어있는 <div> 등)도 true를 반환하기 때문에 주의해서 사용해야 합니다.

clientTop과 clientLeft

요소 내엔 테두리(border)가 있습니다.

테두리 두께는 clientTopclientLeft를 사용해 측정할 수 있습니다.

샘플 예시의 프로퍼티 값은 다음과 같습니다.

  • clientLeft = 25 – 왼쪽 테두리 너비
  • clientTop = 25 – 위쪽 테두리 높이

그런데 사실 이 두 프로퍼티는 테두리의 너비, 높이와 정확히 일치하진 않습니다. 정확히는 테두리 바깥쪽을 기준으로 하는 요소 안쪽의 상대 좌표를 나타냅니다.

조금 헷갈릴 수 있으니 자세히 설명해보겠습니다.

두 프로퍼티의 차이는 운영 체제 언어가 아랍어나 히브리어처럼 오른쪽에서 왼쪽으로 글이 전개되는 언어로 세팅되어 있을 때 드러납니다. 이때는 스크롤바가 오른쪽이 아닌 왼쪽에 나타나게 되는데, 그럼 clientLeft에 스크롤바의 너비가 포함됩니다.

따라서 clientLeft25가 아닌 스크롤바 너비를 포함한 41(25 + 16)이 됩니다.

히브리어 예시를 그림으로 직접 살펴봅시다.

clientWidth와 clientHeight

clientWidth와 clientHeight 프로퍼티는 테두리 안에 있는 영역의 사이즈 정보를 제공합니다.

여기엔 콘텐츠 너비에 더불어 패딩이 포함되는데, 스크롤바 너비는 포함되지 않습니다.

그림에서 clientHeight로 표시된 부분을 먼저 살펴봅시다.

수평 스크롤바가 없기 때문에 clientHeight는 테두리 안쪽에 있는 영역 전체를 더한 값이 됩니다. 높이 200px에 위, 아래 패딩(2 * 20px)을 더한 값인 240px이 되는 것이죠.

이제 clientWidth를 계산해 봅시다. clientWidth를 계산할 때 주의할 점은 수직 스크롤바가 차지하는 너비 16px 때문에 콘텐츠 너비는 300px이 아닌 284px이 된다는 점입니다. 따라서 clientWidth는 콘텐츠 너비 284px에 왼쪽, 오른쪽 패딩(2 * 20px)을 더한 값인 324px가 됩니다.

패딩이 없었다면 clientWidthclientHeight는 테두리와 스크롤바 안쪽에 있는 콘텐츠 영역의 너비, 높이와 정확히 일치했을 겁니다.

따라서 패딩이 없는 경우엔 clientWidthclientHeight를 사용해 콘텐츠 영역 크기를 구할 수 있습니다.

scrollWidth와 scrollHeight

scrollWidthscrollHeight 프로퍼티는 clientWidthclientHeight 유사한데, 스크롤바에 의해 감춰져 있는 숨겨진 영역도 포함한다는 점에서 차이가 있습니다.

그림을 살펴봅시다.

  • scrollWidth = 324 – 수평 스크롤바가 없기 때문에 안쪽 영역 전체를 나타내는 clientWidth와 동일합니다.
  • scrollHeight = 723 – 수직 스크롤바에 가려진 부분을 포함하는 콘텐츠 영역 안쪽 전체의 높이입니다.

scrollWidthscrollHeight는 요소 크기를 콘텐츠가 차지하는 만큼 늘리고자 할 때 사용할 수 있습니다.

예시:

// 콘텐츠가 차지하는 높이만큼 요소 높이를 늘
element.style.height = `${element.scrollHeight}px`;

버튼을 눌러 직접 요소 크기를 확장해 봅시다.

text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text

scrollLeft와 scrollTop

scrollLeftscrollTop 프로퍼티는 수평 스크롤이 오른쪽, 수직 스크롤이 아래로 움직임에 따라 가려진 영역의 너비와 높이를 나타냅니다.

수직 스크롤바를 아래로 조금 내린 경우를 가정한 아래 그림을 살펴봅시다. scrollHeight에서 scrollTop이 얼마만큼의 영역을 차지하는지를 살펴볼 수 있습니다.

scrollTop은 ‘수직 스크롤 바에 의해 가려져 보이지 않는’ 위쪽 콘텐츠의 높이라고 이해하시면 됩니다.

scrollLeftscrollTop은 수정할 수 있습니다.

대부분의 기하 프로퍼티는 읽기전용이지만 scrollLeftscrollTop은 변경이 가능합니다. 스크립트를 사용해 프로퍼티를 수정하면 자동으로 요소 내 스크롤이 움직이죠.

요소를 클릭하면 스크롤바가 10px 아래로 내려가도록 elem.scrollTop += 10을 스크립트에 추가해 놓았습니다.

여기를
클릭
1
2
3
4
5
6
7
8
9

이런 특징을 이용하면 scrollTop0이나 1e9같이 아주 큰 숫자로 설정해 스크롤 바를 최상단이나 최하단으로 옮길 수 있습니다.

CSS를 사용해 너비와 높이를 얻지 마세요

지금까지 요소 너비와 높이, 요소 포지셔닝 관련 거리를 구하는 데 사용되는 다양한 기하프로퍼티에 대해 알아보았습니다.

그런데 우리는 앞서 getComputedStyle를 사용해 CSS가 적용된 요소의 높이와 너비를 구할 수 있다는 것을 스타일과 클래스 챕터에서 알아본 바 있습니다.

그렇다면 왜 getComputedStyle를 사용해 요소 너비와 높이를 얻지 말라고 하는 걸까요?

let elem = document.body;

alert( getComputedStyle(elem).width ); // CSS가 적용된 elem의 너비

getComputedStyle가 아닌 기하 프로퍼티를 사용해 너비와 높이 정보를 얻어야 하는 데는 다음과 같은 이유가 있습니다.

  1. CSS widthheight는 다른 CSS 프로퍼티의 영향을 받습니다. 요소의 너비와 높이 계산 방법을 '지정’하는 box-sizing이 이런 프로퍼티의 대표적인 예이죠. box-sizing을 변경하면 getComputedStyle로 구한 값이 부정확 할 수 있습니다.

  2. CSS widthheightauto일 수 있습니다. 인라인 요소(inline element)가 이런 경우에 속합니다.

    <span id="elem">안녕하세!</span>
    
    <script>
      alert( getComputedStyle(elem).width ); // auto
    </script>

    CSS 관점에서 보면 width:auto는 전혀 이상할 것 없어 보입니다. 그런데 자바스크립트 입장에선 정확한 px값이 있어야 계산을 할 수 있기 때문에 auto라는 값은 쓸모가 없습니다.

이외에도 다른 한 가지 이유가 더 있습니다. 바로 스크롤바입니다. 스크롤바가 없으면 정상 동작하는데, 스크롤바가 생기면 의도한 대로 동작하지 않는 코드들이 있습니다. 이런 일은 스크롤바가 콘텐츠 영역을 차지하는 몇몇 브라우저에서 발생합니다. 이런 브라우저들에선 콘텐츠가 실제 차지하는 영역이 CSS로 설정한 너비보다 좁은데, clientWidthclientHeight는 이를 고려해 클라이언트 요소가 차지하는 공간을 잽니다.

그런데 getComputedStyle(elem).width를 사용하면 상황이 달라집니다. Chrome 같은 브라우저는 스크롤바 너비를 제외한 진짜 내부 너비를 반환하는데 Firefox 같은 브라우저는 스크롤바를 무시하고 CSS로 설정한 너비를 반환합니다. 이런 브라우저 간 차이 때문에 getComputedStyle이 아닌 기하 프로퍼티를 사용해야 합니다.

스크롤바가 차지하는 영역을 따로 두는 브라우저(Windows에서 돌아가는 대부분의 브라우저)에서 아래 예시를 실행하면 300px이 아닌 다른 값이 출력되는 것을 확인할 수 있습니다.

텍스트가 들어있는 위 요소의 CSS엔 width:300px가 설정되어 있는 상태입니다.

Windows가 설치된 데스크톱 PC의 Firefox, Chrome, Edge 브라우저는 모두 스크롤바 영역을 따로 떼어놓습니다. 그런데 Windows 이외의 OS가 설치된 PC의 Firefox에서 예시를 실행하면 300px이 출력되는 것을 확인할 수 있습니다. 다른 브라우저들은 ‘진짜’ 너비를 출력하는 반면 Firefox는 CSS로 설정한 너비를 출력하기 때문입니다.

getComputedStyle과 기하 프로퍼티의 차이는 자바스크립트를 사용해 getComputedStyle(...).width로 값을 얻고자 할 때만 발생합니다. 눈으로 봤을 땐 전혀 문제가 없으니 이 점에 유의하시기 바랍니다.

요약

요소는 다음과 같은 기하 프로퍼티를 지원합니다.

  • offsetParent – 위치 계산에 사용되는 가장 가까운 조상 요소나 td, th, table, body
  • offsetLeftoffsetTopoffsetParent 기준으로 요소가 각각 오른쪽, 아래쪽으로 얼마나 떨어져 있는지를 나타내는 값
  • offsetWidthoffsetHeight – 테두리를 포함 요소 '전체’가 차지하는 너비와 높이
  • clientLeftclientTop – 요소 제일 밖을 감싸는 영역과 요소 안(콘텐츠 + 패딩)을 감싸는 영역 사이의 거리를 나타냄. 대부분의 경우 왼쪽, 위쪽 테두리 두께와 일치하지만, 오른쪽에서 왼쪽으로 글을 쓰는 언어가 세팅된 OS에선 clientLeft에 스크롤바 두께가 포함됨
  • clientWidthclientHeight – 콘텐츠와 패딩을 포함한 영역의 너비와 높이로, 스크롤바는 포함되지 않음
  • scrollWidthscrollHeightclientWidth, clientHeight 같이 콘텐츠와 패딩을 포함한 영역의 너비와 높이를 나타내는데, 스크롤바에 의해 숨겨진 콘텐츠 영역까지 포함됨
  • scrollLeftscrollTop – 스크롤바가 오른쪽, 아래로 움직임에 따라 가려지게 되는 요소 콘텐츠의 너비와 높이

스크롤바를 움직일 수 있게 해주는 scrollLeftscrollTop을 제외한 모든 프로퍼티는 읽기 전용입니다.

과제

중요도: 5

The elem.scrollTop property is the size of the scrolled out part from the top. How to get the size of the bottom scroll (let’s call it scrollBottom)?

Write the code that works for an arbitrary elem.

P.S. Please check your code: if there’s no scroll or the element is fully scrolled down, then it should return 0.

The solution is:

let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight;

In other words: (full height) minus (scrolled out top part) minus (visible part) – that’s exactly the scrolled out bottom part.

중요도: 3

Write the code that returns the width of a standard scrollbar.

For Windows it usually varies between 12px and 20px. If the browser doesn’t reserve any space for it (the scrollbar is half-translucent over the text, also happens), then it may be 0px.

P.S. The code should work for any HTML document, do not depend on its content.

To get the scrollbar width, we can create an element with the scroll, but without borders and paddings.

Then the difference between its full width offsetWidth and the inner content area width clientWidth will be exactly the scrollbar:

// create a div with the scroll
let div = document.createElement('div');

div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';

// must put it in the document, otherwise sizes will be 0
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;

div.remove();

alert(scrollWidth);
중요도: 5

Here’s how the source document looks:

What are coordinates of the field center?

Calculate them and use to place the ball into the center of the green field:

  • The element should be moved by JavaScript, not CSS.
  • The code should work with any ball size (10, 20, 30 pixels) and any field size, not be bound to the given values.

P.S. Sure, centering could be done with CSS, but here we want exactly JavaScript. Further we’ll meet other topics and more complex situations when JavaScript must be used. Here we do a “warm-up”.

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

The ball has position:absolute. It means that its left/top coordinates are measured from the nearest positioned element, that is #field (because it has position:relative).

The coordinates start from the inner left-upper corner of the field:

The inner field width/height is clientWidth/clientHeight. So the field center has coordinates (clientWidth/2, clientHeight/2).

…But if we set ball.style.left/top to such values, then not the ball as a whole, but the left-upper edge of the ball would be in the center:

ball.style.left = Math.round(field.clientWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2) + 'px';

Here’s how it looks:

To align the ball center with the center of the field, we should move the ball to the half of its width to the left and to the half of its height to the top:

ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px';

Now the ball is finally centered.

Attention: the pitfall!

The code won’t work reliably while <img> has no width/height:

<img src="ball.png" id="ball">

When the browser does not know the width/height of an image (from tag attributes or CSS), then it assumes them to equal 0 until the image finishes loading.

So the value of ball.offsetWidth will be 0 until the image loads. That leads to wrong coordinates in the code above.

After the first load, the browser usually caches the image, and on reloads it will have the size immediately. But on the first load the value of ball.offsetWidth is 0.

We should fix that by adding width/height to <img>:

<img src="ball.png" width="40" height="40" id="ball">

…Or provide the size in CSS:

#ball {
  width: 40px;
  height: 40px;
}

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

중요도: 5

What’s the difference between getComputedStyle(elem).width and elem.clientWidth?

Give at least 3 differences. The more the better.

Differences:

  1. clientWidth is numeric, while getComputedStyle(elem).width returns a string with px at the end.
  2. getComputedStyle may return non-numeric width like "auto" for an inline element.
  3. clientWidth is the inner content area of the element plus paddings, while CSS width (with standard box-sizing) is the inner content area without paddings.
  4. If there’s a scrollbar and the browser reserves the space for it, some browser substract that space from CSS width (cause it’s not available for content any more), and some do not. The clientWidth property is always the same: scrollbar size is substracted if reserved.
튜토리얼 지도

댓글

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